From 1058375a9067ecce9ad9d1b9c92429726ad1f762 Mon Sep 17 00:00:00 2001 From: killer-cf Date: Wed, 29 Oct 2025 18:57:05 -0300 Subject: [PATCH] refactor: remove unused authentication files and dependencies; update package.json to streamline dependencies and improve project structure --- apps/web/package.json | 5 +- apps/web/src/lib/auth.ts | 7 - .../web/src/lib/components/ActionGuard.svelte | 83 +++ .../web/src/routes/(dashboard)/+layout.svelte | 120 ++-- .../ti/painel-permissoes/+page.svelte | 503 +++++++++-------- .../ti/personalizar-permissoes/+page.svelte | 414 +++----------- .../src/routes/api/auth/[...all]/+server.ts | 3 - packages/auth/package.json | 5 +- packages/backend/convex/_generated/api.d.ts | 4 +- packages/backend/convex/autenticacao.ts | 45 +- packages/backend/convex/auth.ts | 54 -- packages/backend/convex/betterAuth/adapter.ts | 13 - packages/backend/convex/betterAuth/auth.ts | 5 - .../convex/betterAuth/convex.config.ts | 5 - packages/backend/convex/betterAuth/schema.ts | 70 --- packages/backend/convex/convex.config.ts | 3 - packages/backend/convex/email.ts | 165 ++++-- packages/backend/convex/funcionarios.ts | 111 +++- .../backend/convex/limparPerfisAntigos.ts | 38 +- packages/backend/convex/logsAtividades.ts | 33 +- packages/backend/convex/logsLogin.ts | 37 +- packages/backend/convex/menuPermissoes.ts | 528 ------------------ packages/backend/convex/perfisCustomizados.ts | 120 +++- packages/backend/convex/permissoesAcoes.ts | 210 +++++++ packages/backend/convex/roles.ts | 3 +- packages/backend/convex/schema.ts | 3 +- packages/backend/convex/seed.ts | 85 ++- packages/backend/convex/usuarios.ts | 292 +++++++++- packages/backend/package.json | 4 +- 29 files changed, 1426 insertions(+), 1542 deletions(-) delete mode 100644 apps/web/src/lib/auth.ts create mode 100644 apps/web/src/lib/components/ActionGuard.svelte delete mode 100644 apps/web/src/routes/api/auth/[...all]/+server.ts delete mode 100644 packages/backend/convex/auth.ts delete mode 100644 packages/backend/convex/betterAuth/adapter.ts delete mode 100644 packages/backend/convex/betterAuth/auth.ts delete mode 100644 packages/backend/convex/betterAuth/convex.config.ts delete mode 100644 packages/backend/convex/betterAuth/schema.ts delete mode 100644 packages/backend/convex/menuPermissoes.ts create mode 100644 packages/backend/convex/permissoesAcoes.ts diff --git a/apps/web/package.json b/apps/web/package.json index a33e0b1..daad171 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -27,14 +27,11 @@ "vite": "^7.1.2" }, "dependencies": { - "@convex-dev/better-auth": "^0.9.6", "@dicebear/collection": "^9.2.4", "@dicebear/core": "^9.2.4", "@internationalized/date": "^3.10.0", - "@mmailaender/convex-better-auth-svelte": "^0.2.0", "@sgse-app/backend": "*", "@tanstack/svelte-form": "^1.19.2", - "better-auth": "1.3.27", "convex": "^1.28.0", "convex-svelte": "^0.0.11", "date-fns": "^4.1.0", @@ -43,4 +40,4 @@ "jspdf-autotable": "^5.0.2", "zod": "^4.0.17" } -} +} \ No newline at end of file diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts deleted file mode 100644 index 6de9cf0..0000000 --- a/apps/web/src/lib/auth.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAuthClient } from "better-auth/client"; -import { convexClient } from "@convex-dev/better-auth/client/plugins"; - -export const authClient = createAuthClient({ - baseURL: "http://localhost:5173", - plugins: [convexClient()], -}); diff --git a/apps/web/src/lib/components/ActionGuard.svelte b/apps/web/src/lib/components/ActionGuard.svelte new file mode 100644 index 0000000..90cbe93 --- /dev/null +++ b/apps/web/src/lib/components/ActionGuard.svelte @@ -0,0 +1,83 @@ + + +{#if verificando} +
+
+ +

Verificando permissões...

+
+
+{:else if permitido} + {@render children?.()} +{:else} +
+
+
+ + + +
+

Acesso Negado

+

+ Você não tem permissão para acessar esta ação. +

+
+
+{/if} diff --git a/apps/web/src/routes/(dashboard)/+layout.svelte b/apps/web/src/routes/(dashboard)/+layout.svelte index 3c5fc04..e336b1b 100644 --- a/apps/web/src/routes/(dashboard)/+layout.svelte +++ b/apps/web/src/routes/(dashboard)/+layout.svelte @@ -1,84 +1,66 @@ -{#if getCurrentRouteConfig} - -
+{#if routeAction} + +
{@render children()}
- +
{:else} -
+
{@render children()}
{/if} diff --git a/apps/web/src/routes/(dashboard)/ti/painel-permissoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/painel-permissoes/+page.svelte index 43e1f56..130bfb5 100644 --- a/apps/web/src/routes/(dashboard)/ti/painel-permissoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/painel-permissoes/+page.svelte @@ -4,16 +4,46 @@ import ProtectedRoute from "$lib/components/ProtectedRoute.svelte"; import { goto } from "$app/navigation"; import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; + type RoleRow = { + _id: Id<"roles">; + _creationTime: number; + nome: string; + descricao: string; + nivel: number; + setor?: string; + customizado: boolean; + editavel?: boolean; + criadoPor?: Id<"usuarios">; + }; const client = useConvexClient(); - // Buscar matriz de permissões - const matrizQuery = useQuery(api.menuPermissoes.obterMatrizPermissoes, {}); + // Carregar lista de roles e catálogo de recursos/ações + const rolesQuery = useQuery(api.roles.listar, {}); + const catalogoQuery = useQuery(api.permissoesAcoes.listarRecursosEAcoes, {}); let salvando = $state(false); - let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null); + let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>( + null + ); let busca = $state(""); let filtroRole = $state(""); + let expandido: Record = $state({}); + + // Cache de permissões por role + let permissoesPorRole: Record< + string, + Array<{ recurso: string; acoes: Array }> + > = $state({}); + + async function carregarPermissoesRole(roleId: Id<"roles">) { + if (permissoesPorRole[roleId]) return; + const dados = await client.query( + api.permissoesAcoes.listarPermissoesAcoesPorRole, + { roleId } + ); + permissoesPorRole[roleId] = dados; + } function mostrarMensagem(tipo: "success" | "error", texto: string) { mensagem = { tipo, texto }; @@ -22,89 +52,50 @@ }, 3000); } - const dadosFiltrados = $derived.by(() => { - if (!matrizQuery.data) return []; - - let resultado = matrizQuery.data; - - // Filtrar por role - if (filtroRole) { - resultado = resultado.filter(r => r.role._id === filtroRole); - } - - // Filtrar por busca + const rolesFiltradas = $derived.by(() => { + if (!rolesQuery.data) return []; + let rs: Array = rolesQuery.data as Array; + if (filtroRole) + rs = rs.filter((r: RoleRow) => r._id === (filtroRole as any)); if (busca.trim()) { - const buscaLower = busca.toLowerCase(); - resultado = resultado.map(roleData => ({ - ...roleData, - permissoes: roleData.permissoes.filter(p => - p.menuNome.toLowerCase().includes(buscaLower) || - p.menuPath.toLowerCase().includes(buscaLower) - ) - })).filter(roleData => roleData.permissoes.length > 0); + const b = busca.toLowerCase(); + rs = rs.filter( + (r: RoleRow) => + r.descricao.toLowerCase().includes(b) || + r.nome.toLowerCase().includes(b) + ); } - - return resultado; + return rs; }); - async function atualizarPermissao( + async function toggleAcao( roleId: Id<"roles">, - menuPath: string, - campo: "podeAcessar" | "podeConsultar" | "podeGravar", - valor: boolean + recurso: string, + acao: string, + conceder: boolean ) { try { salvando = true; - - // Buscar a permissão atual - const roleData = matrizQuery.data?.find((r) => r.role._id === roleId); - const permissaoAtual = roleData?.permissoes.find((p) => p.menuPath === menuPath); - - if (!permissaoAtual) { - throw new Error("Permissão não encontrada"); - } - - // Inicializar com valores atuais - let podeAcessar = permissaoAtual.podeAcessar; - let podeConsultar = permissaoAtual.podeConsultar; - let podeGravar = permissaoAtual.podeGravar; - - // Aplicar lógica de dependências baseada no campo alterado - if (campo === "podeAcessar") { - podeAcessar = valor; - // Se desmarcou "Acessar", desmarcar tudo - if (!valor) { - podeConsultar = false; - podeGravar = false; - } - // Se marcou "Acessar", manter os outros valores como estão - } else if (campo === "podeConsultar") { - podeConsultar = valor; - // Se marcou "Consultar", marcar "Acessar" automaticamente - if (valor) { - podeAcessar = true; - } else { - // Se desmarcou "Consultar", desmarcar "Gravar" - podeGravar = false; - } - } else if (campo === "podeGravar") { - podeGravar = valor; - // Se marcou "Gravar", marcar "Consultar" e "Acessar" automaticamente - if (valor) { - podeAcessar = true; - podeConsultar = true; - } - // Se desmarcou "Gravar", manter os outros como estão - } - - await client.mutation(api.menuPermissoes.atualizarPermissao, { + await client.mutation(api.permissoesAcoes.atualizarPermissaoAcao, { roleId, - menuPath, - podeAcessar, - podeConsultar, - podeGravar, + recurso, + acao, + conceder, }); - + // Atualizar cache local + const atual = permissoesPorRole[roleId] || []; + const entry = atual.find((e) => e.recurso === recurso); + if (entry) { + const set = new Set(entry.acoes); + if (conceder) set.add(acao); + else set.delete(acao); + entry.acoes = Array.from(set); + } else { + permissoesPorRole[roleId] = [ + ...atual, + { recurso, acoes: conceder ? [acao] : [] }, + ]; + } mostrarMensagem("success", "Permissão atualizada com sucesso!"); } catch (e: any) { mostrarMensagem("error", e.message || "Erro ao atualizar permissão"); @@ -113,16 +104,10 @@ } } - async function inicializarPermissoes(roleId: Id<"roles">) { - try { - salvando = true; - await client.mutation(api.menuPermissoes.inicializarPermissoesRole, { roleId }); - mostrarMensagem("success", "Permissões inicializadas!"); - } catch (e: any) { - mostrarMensagem("error", e.message || "Erro ao inicializar permissões"); - } finally { - salvando = false; - } + function isConcedida(roleId: Id<"roles">, recurso: string, acao: string) { + const dados = permissoesPorRole[roleId]; + const entry = dados?.find((e) => e.recurso === recurso); + return entry ? entry.acoes.includes(acao) : false; } @@ -132,8 +117,19 @@
  • - - + + Dashboard @@ -149,17 +145,43 @@
    - - + +
    -

    Gerenciar Permissões de Acesso

    -

    Configure as permissões de acesso aos menus do sistema por função

    +

    + Gerenciar Permissões de Acesso +

    +

    + Configure as permissões de acesso aos menus do sistema por função +

    @@ -168,14 +190,38 @@ {#if mensagem} -
    +
    {#if mensagem.tipo === "success"} - - + + {:else} - - + + {/if} {mensagem.texto} @@ -189,13 +235,13 @@
    @@ -227,10 +273,10 @@ bind:value={filtroRole} > - {#if matrizQuery.data} - {#each matrizQuery.data as roleData} - {/each} {/if} @@ -272,8 +318,18 @@
    - - + +

    Como funciona o sistema de permissões:

    @@ -281,9 +337,13 @@

    Tipos de Permissão:

      -
    • Acessar: Visualizar menu e acessar página
    • +
    • + • Acessar: Visualizar menu e acessar página +
    • Consultar: Ver dados (requer "Acessar")
    • -
    • Gravar: Criar/editar/excluir (requer "Consultar")
    • +
    • + • Gravar: Criar/editar/excluir (requer "Consultar") +
    @@ -291,27 +351,39 @@
    • Admin e TI: Acesso total automático
    • Dashboard: Público para todos
    • -
    • Perfil Customizado: Permissões personalizadas
    • +
    • + • Perfil Customizado: Permissões personalizadas +
    - - {#if matrizQuery.isLoading} + + {#if rolesQuery.isLoading || catalogoQuery.isLoading}
    - {:else if matrizQuery.error} + {:else if rolesQuery.error}
    - - + + - Erro ao carregar permissões: {matrizQuery.error.message} + Erro ao carregar perfis: {rolesQuery.error.message}
    - {:else if matrizQuery.data} - {#if dadosFiltrados.length === 0} + {:else if rolesQuery.data && catalogoQuery.data} + {#if rolesFiltradas.length === 0}

    Nenhum resultado encontrado

    - {busca ? `Não foram encontrados menus com "${busca}"` : "Nenhuma permissão corresponde aos filtros aplicados"} + {busca + ? `Não foram encontrados perfis com "${busca}"` + : "Nenhum perfil corresponde aos filtros aplicados"}

    {/if} - - {#each dadosFiltrados as roleData} + + {#each rolesFiltradas as roleRow}
    -

    {roleData.role.descricao}

    -
    Nível {roleData.role.nivel}
    - {#if roleData.role.nivel <= 1} +

    {roleRow.descricao}

    +
    + Nível {roleRow.nivel} +
    + {#if roleRow.nivel <= 1}
    - - + + Acesso Total
    {/if}

    - {roleData.role.nome} + {roleRow.nome}

    - - {#if roleData.role.nivel > 1} + + {#if roleRow.nivel > 1} {/if}
    - {#if roleData.role.nivel <= 1} + {#if roleRow.nivel <= 1}
    - - + +

    Perfil Administrativo

    -
    Este perfil possui acesso total ao sistema automaticamente, sem necessidade de configuração manual.
    +
    + Este perfil possui acesso total ao sistema automaticamente, + sem necessidade de configuração manual. +
    - {:else} -
    -
    -
    Total de Menus
    -
    {roleData.permissoes.length}
    -
    -
    -
    Com Acesso
    -
    {roleData.permissoes.filter(p => p.podeAcessar).length}
    -
    -
    -
    Pode Consultar
    -
    {roleData.permissoes.filter(p => p.podeConsultar).length}
    -
    -
    -
    Pode Gravar
    -
    {roleData.permissoes.filter(p => p.podeGravar).length}
    -
    -
    - + {:else if expandido[roleRow._id]}
    - - - - + + + + + + - {#each roleData.permissoes as permissao} + {#each catalogoQuery.data as item} - - - + {#each ["ver", "listar", "criar", "editar", "excluir"] as ac} + + {/each} {/each} @@ -508,4 +542,3 @@ {/each} {/if} - diff --git a/apps/web/src/routes/(dashboard)/ti/personalizar-permissoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/personalizar-permissoes/+page.svelte index 19a3d1c..8aaa195 100644 --- a/apps/web/src/routes/(dashboard)/ti/personalizar-permissoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/personalizar-permissoes/+page.svelte @@ -1,123 +1,6 @@ @@ -126,8 +9,19 @@
    • - - + + Dashboard @@ -143,241 +37,71 @@
      - - + +
      -

      Personalizar Permissões por Matrícula

      -

      Configure permissões específicas para usuários individuais

      +

      + Funcionalidade descontinuada +

      +

      + Agora as permissões são configuradas por ação em cada perfil no painel + de permissões. +

      - - - {#if mensagem} -
      - {#if mensagem.tipo === "success"} - - - - {:else} - - - - {/if} - {mensagem.texto} -
      - {/if} - - -
      -
      -

      Buscar Usuário

      -

      Digite a matrícula do usuário para personalizar suas permissões

      - -
      -
      - - e.key === "Enter" && buscarUsuario()} - /> -
      - -
      - - - {#if usuarioEncontrado} - - {/if} -
      -
      -
      +
      + + + + + A personalização por usuário foi substituída por permissões por ação + por perfil. Utilize o + Painel de Permissões para configurar. +
      - - - {#if usuarioEncontrado} -
      -
      -
      -
      -
      - {usuarioEncontrado.nome.charAt(0)} -
      -
      - -
      -

      {usuarioEncontrado.nome}

      -
      - - - - - Matrícula: {usuarioEncontrado.matricula} - - - - - - Email: {usuarioEncontrado.email} - -
      -
      - -
      -
      - Nível {usuarioEncontrado.role.nivel} -
      -

      {usuarioEncontrado.role.descricao}

      -
      - {usuarioEncontrado.ativo ? "Ativo" : "Inativo"} -
      -
      -
      -
      -
      - - -
      -
      -

      Permissões Personalizadas

      -
      - - - -
      -

      - Permissões personalizadas sobrepõem as permissões da função.
      - Configure apenas os menus que deseja personalizar para este usuário. -

      -
      -
      - - {#if menusQuery.isLoading} -
      - -
      - {:else if menusQuery.data} -
      -
    Menu -
    - - - - - Acessar -
    -
    -
    - - - - Consultar -
    -
    -
    - - - - Gravar -
    -
    RecursoVerListarCriarEditarExcluir
    - {permissao.menuNome} - {permissao.menuPath} + {item.recurso}
    - - atualizarPermissao( - roleData.role._id, - permissao.menuPath, - "podeAcessar", - e.currentTarget.checked - )} - /> - - - atualizarPermissao( - roleData.role._id, - permissao.menuPath, - "podeConsultar", - e.currentTarget.checked - )} - /> - - - atualizarPermissao( - roleData.role._id, - permissao.menuPath, - "podeGravar", - e.currentTarget.checked - )} - /> - + + toggleAcao( + roleRow._id, + item.recurso, + ac, + e.currentTarget.checked + )} + /> +
    - - - - - - - - - - - {#each menusQuery.data as menu} - {@const permissao = permissoesQuery?.data?.find((p) => p.menuPath === menu.path)} - - - - - - - - {/each} - -
    Menu -
    - - - - - Acessar -
    -
    -
    - - - - Consultar -
    -
    -
    - - - - Gravar -
    -
    Status
    -
    - {menu.nome} - {menu.path} -
    -
    - - atualizarPermissao(menu.path, "podeAcessar", e.currentTarget.checked)} - /> - - - atualizarPermissao(menu.path, "podeConsultar", e.currentTarget.checked)} - /> - - - atualizarPermissao(menu.path, "podeGravar", e.currentTarget.checked)} - /> - - {#if permissao} -
    Personalizado
    - {:else} -
    Padrão da Função
    - {/if} -
    -
    - {/if} -
    -
    - {/if} - diff --git a/apps/web/src/routes/api/auth/[...all]/+server.ts b/apps/web/src/routes/api/auth/[...all]/+server.ts deleted file mode 100644 index dd7705e..0000000 --- a/apps/web/src/routes/api/auth/[...all]/+server.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createSvelteKitHandler } from "@mmailaender/convex-better-auth-svelte/sveltekit"; - -export const { GET, POST } = createSvelteKitHandler(); diff --git a/packages/auth/package.json b/packages/auth/package.json index 0cc2388..020fb6d 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -10,7 +10,6 @@ "typescript": "^5.9.2" }, "dependencies": { - "convex": "^1.28.0", - "better-auth": "1.3.27" + "convex": "^1.28.0" } -} +} \ No newline at end of file diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index bf25b80..2d5e1b0 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -28,10 +28,10 @@ import type * as limparPerfisAntigos from "../limparPerfisAntigos.js"; import type * as logsAcesso from "../logsAcesso.js"; import type * as logsAtividades from "../logsAtividades.js"; import type * as logsLogin from "../logsLogin.js"; -import type * as menuPermissoes from "../menuPermissoes.js"; import type * as migrarUsuariosAdmin from "../migrarUsuariosAdmin.js"; import type * as monitoramento from "../monitoramento.js"; import type * as perfisCustomizados from "../perfisCustomizados.js"; +import type * as permissoesAcoes from "../permissoesAcoes.js"; import type * as roles from "../roles.js"; import type * as seed from "../seed.js"; import type * as simbolos from "../simbolos.js"; @@ -76,10 +76,10 @@ declare const fullApi: ApiFromModules<{ logsAcesso: typeof logsAcesso; logsAtividades: typeof logsAtividades; logsLogin: typeof logsLogin; - menuPermissoes: typeof menuPermissoes; migrarUsuariosAdmin: typeof migrarUsuariosAdmin; monitoramento: typeof monitoramento; perfisCustomizados: typeof perfisCustomizados; + permissoesAcoes: typeof permissoesAcoes; roles: typeof roles; seed: typeof seed; simbolos: typeof simbolos; diff --git a/packages/backend/convex/autenticacao.ts b/packages/backend/convex/autenticacao.ts index 5ae4621..9a8b2a8 100644 --- a/packages/backend/convex/autenticacao.ts +++ b/packages/backend/convex/autenticacao.ts @@ -1,6 +1,12 @@ import { v } from "convex/values"; import { mutation, query } from "./_generated/server"; -import { hashPassword, verifyPassword, generateToken, validarMatricula, validarSenha } from "./auth/utils"; +import { + hashPassword, + verifyPassword, + generateToken, + validarMatricula, + validarSenha, +} from "./auth/utils"; import { registrarLogin } from "./logsLogin"; import { Id } from "./_generated/dataModel"; @@ -10,7 +16,7 @@ import { Id } from "./_generated/dataModel"; async function verificarBloqueioUsuario(ctx: any, usuarioId: Id<"usuarios">) { const bloqueio = await ctx.db .query("bloqueiosUsuarios") - .withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioId)) + .withIndex("by_usuario", (q: any) => q.eq("usuarioId", usuarioId)) .filter((q: any) => q.eq(q.field("ativo"), true)) .first(); @@ -23,7 +29,7 @@ async function verificarBloqueioUsuario(ctx: any, usuarioId: Id<"usuarios">) { async function verificarRateLimitIP(ctx: any, ipAddress: string) { // Últimas 15 minutos const dataLimite = Date.now() - 15 * 60 * 1000; - + const tentativas = await ctx.db .query("logsLogin") .withIndex("by_ip", (q: any) => q.eq("ipAddress", ipAddress)) @@ -31,7 +37,7 @@ async function verificarRateLimitIP(ctx: any, ipAddress: string) { .collect(); const falhas = tentativas.filter((t: any) => !t.sucesso).length; - + // Bloquear se 5 ou mais tentativas falhas em 15 minutos return falhas >= 5; } @@ -102,7 +108,9 @@ export const login = mutation({ } else { usuario = await ctx.db .query("usuarios") - .withIndex("by_matricula", (q) => q.eq("matricula", args.matriculaOuEmail)) + .withIndex("by_matricula", (q) => + q.eq("matricula", args.matriculaOuEmail) + ) .first(); } @@ -122,7 +130,10 @@ export const login = mutation({ } // Verificar se usuário está bloqueado - if (usuario.bloqueado || (await verificarBloqueioUsuario(ctx, usuario._id))) { + if ( + usuario.bloqueado || + (await verificarBloqueioUsuario(ctx, usuario._id)) + ) { await registrarLogin(ctx, { usuarioId: usuario._id, matriculaOuEmail: args.matriculaOuEmail, @@ -172,7 +183,9 @@ export const login = mutation({ userAgent: args.userAgent, }); - const minutosRestantes = Math.ceil((TEMPO_BLOQUEIO - tempoDecorrido) / 60000); + const minutosRestantes = Math.ceil( + (TEMPO_BLOQUEIO - tempoDecorrido) / 60000 + ); return { sucesso: false as const, erro: `Conta temporariamente bloqueada. Tente novamente em ${minutosRestantes} minutos.`, @@ -192,8 +205,9 @@ export const login = mutation({ if (!senhaValida) { // Incrementar tentativas - const novasTentativas = tempoDecorrido > TEMPO_BLOQUEIO ? 1 : tentativasRecentes + 1; - + const novasTentativas = + tempoDecorrido > TEMPO_BLOQUEIO ? 1 : tentativasRecentes + 1; + await ctx.db.patch(usuario._id, { tentativasLogin: novasTentativas, ultimaTentativaLogin: Date.now(), @@ -367,7 +381,10 @@ export const verificarSessao = query({ .first(); if (!sessao || !sessao.ativo) { - return { valido: false as const, motivo: "Sessão não encontrada ou inativa" }; + return { + valido: false as const, + motivo: "Sessão não encontrada ou inativa", + }; } // Verificar se sessão expirou @@ -380,7 +397,10 @@ export const verificarSessao = query({ // Buscar usuário const usuario = await ctx.db.get(sessao.usuarioId); if (!usuario || !usuario.ativo) { - return { valido: false as const, motivo: "Usuário não encontrado ou inativo" }; + return { + valido: false as const, + motivo: "Usuário não encontrado ou inativo", + }; } // Buscar role @@ -428,7 +448,7 @@ export const limparSessoesExpiradas = mutation({ for (const sessao of sessoes) { if (sessao.expiraEm < agora) { await ctx.db.patch(sessao._id, { ativo: false }); - + await ctx.db.insert("logsAcesso", { usuarioId: sessao.usuarioId, tipo: "sessao_expirada", @@ -511,4 +531,3 @@ export const alterarSenha = mutation({ return { sucesso: true as const }; }, }); - diff --git a/packages/backend/convex/auth.ts b/packages/backend/convex/auth.ts deleted file mode 100644 index f4be2ec..0000000 --- a/packages/backend/convex/auth.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createClient, type GenericCtx } from "@convex-dev/better-auth"; -import { convex } from "@convex-dev/better-auth/plugins"; -import { components } from "./_generated/api"; -import { type DataModel } from "./_generated/dataModel"; -import { query } from "./_generated/server"; -import { betterAuth } from "better-auth"; -import schema from "./betterAuth/schema"; - -// Configurações de ambiente para produção -const siteUrl = process.env.SITE_URL || process.env.CONVEX_SITE_URL || "http://localhost:5173"; -const authSecret = process.env.BETTER_AUTH_SECRET; - -// The component client has methods needed for integrating Convex with Better Auth, -// as well as helper methods for general use. -export const authComponent = createClient(components.betterAuth, { - local: { - schema: schema as any, - }, -}); - -export const createAuth = ( - ctx: GenericCtx, - { optionsOnly } = { optionsOnly: false } -) => { - return betterAuth({ - // Secret para criptografia de tokens - OBRIGATÓRIO em produção - secret: authSecret, - // disable logging when createAuth is called just to generate options. - // this is not required, but there's a lot of noise in logs without it. - logger: { - disabled: optionsOnly, - }, - baseURL: siteUrl, - database: authComponent.adapter(ctx), - // Configure simple, non-verified email/password to get started - emailAndPassword: { - enabled: true, - requireEmailVerification: false, - }, - plugins: [ - // The Convex plugin is required for Convex compatibility - convex(), - ], - }); -}; - -// Example function for getting the current user -// Feel free to edit, omit, etc. -export const getCurrentUser = query({ - args: {}, - handler: async (ctx) => { - return authComponent.getAuthUser(ctx as any); - }, -}); diff --git a/packages/backend/convex/betterAuth/adapter.ts b/packages/backend/convex/betterAuth/adapter.ts deleted file mode 100644 index 0741d37..0000000 --- a/packages/backend/convex/betterAuth/adapter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createApi } from "@convex-dev/better-auth"; -import schema from "./schema"; -import { createAuth } from "../auth"; - -export const { - create, - findOne, - findMany, - updateOne, - updateMany, - deleteOne, - deleteMany, -} = createApi(schema, createAuth); diff --git a/packages/backend/convex/betterAuth/auth.ts b/packages/backend/convex/betterAuth/auth.ts deleted file mode 100644 index be7e455..0000000 --- a/packages/backend/convex/betterAuth/auth.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createAuth } from "../auth"; -import { getStaticAuth } from "@convex-dev/better-auth"; - -// Export a static instance for Better Auth schema generation -export const auth = getStaticAuth(createAuth); diff --git a/packages/backend/convex/betterAuth/convex.config.ts b/packages/backend/convex/betterAuth/convex.config.ts deleted file mode 100644 index fe8c88e..0000000 --- a/packages/backend/convex/betterAuth/convex.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineComponent } from "convex/server"; - -const component = defineComponent("betterAuth"); - -export default component; diff --git a/packages/backend/convex/betterAuth/schema.ts b/packages/backend/convex/betterAuth/schema.ts deleted file mode 100644 index 167d19f..0000000 --- a/packages/backend/convex/betterAuth/schema.ts +++ /dev/null @@ -1,70 +0,0 @@ -// This file is auto-generated. Do not edit this file manually. -// To regenerate the schema, run: -// `npx @better-auth/cli generate --output undefined -y` - -import { defineSchema, defineTable } from "convex/server"; -import { v } from "convex/values"; - -export const tables = { - user: defineTable({ - name: v.string(), - email: v.string(), - emailVerified: v.boolean(), - image: v.optional(v.union(v.null(), v.string())), - createdAt: v.number(), - updatedAt: v.number(), - userId: v.optional(v.union(v.null(), v.string())), - }) - .index("email_name", ["email","name"]) - .index("name", ["name"]) - .index("userId", ["userId"]), - session: defineTable({ - expiresAt: v.number(), - token: v.string(), - createdAt: v.number(), - updatedAt: v.number(), - ipAddress: v.optional(v.union(v.null(), v.string())), - userAgent: v.optional(v.union(v.null(), v.string())), - userId: v.string(), - }) - .index("expiresAt", ["expiresAt"]) - .index("expiresAt_userId", ["expiresAt","userId"]) - .index("token", ["token"]) - .index("userId", ["userId"]), - account: defineTable({ - accountId: v.string(), - providerId: v.string(), - userId: v.string(), - accessToken: v.optional(v.union(v.null(), v.string())), - refreshToken: v.optional(v.union(v.null(), v.string())), - idToken: v.optional(v.union(v.null(), v.string())), - accessTokenExpiresAt: v.optional(v.union(v.null(), v.number())), - refreshTokenExpiresAt: v.optional(v.union(v.null(), v.number())), - scope: v.optional(v.union(v.null(), v.string())), - password: v.optional(v.union(v.null(), v.string())), - createdAt: v.number(), - updatedAt: v.number(), - }) - .index("accountId", ["accountId"]) - .index("accountId_providerId", ["accountId","providerId"]) - .index("providerId_userId", ["providerId","userId"]) - .index("userId", ["userId"]), - verification: defineTable({ - identifier: v.string(), - value: v.string(), - expiresAt: v.number(), - createdAt: v.number(), - updatedAt: v.number(), - }) - .index("expiresAt", ["expiresAt"]) - .index("identifier", ["identifier"]), - jwks: defineTable({ - publicKey: v.string(), - privateKey: v.string(), - createdAt: v.number(), - }), -}; - -const schema = defineSchema(tables); - -export default schema; diff --git a/packages/backend/convex/convex.config.ts b/packages/backend/convex/convex.config.ts index f2d05fb..3367024 100644 --- a/packages/backend/convex/convex.config.ts +++ b/packages/backend/convex/convex.config.ts @@ -1,7 +1,4 @@ import { defineApp } from "convex/server"; -import betterAuth from "./betterAuth/convex.config"; - const app = defineApp(); -app.use(betterAuth); export default app; diff --git a/packages/backend/convex/email.ts b/packages/backend/convex/email.ts index 28d9a56..8a17ac9 100644 --- a/packages/backend/convex/email.ts +++ b/packages/backend/convex/email.ts @@ -1,7 +1,14 @@ import { v } from "convex/values"; -import { mutation, query, action, internalMutation } from "./_generated/server"; +import { + mutation, + query, + action, + internalMutation, + internalQuery, +} from "./_generated/server"; import { Id } from "./_generated/dataModel"; import { renderizarTemplate } from "./templatesMensagens"; +import { internal } from "./_generated/api"; /** * Enfileirar email para envio @@ -15,7 +22,10 @@ export const enfileirarEmail = mutation({ templateId: v.optional(v.id("templatesMensagens")), enviadoPorId: v.id("usuarios"), }, - returns: v.object({ sucesso: v.boolean(), emailId: v.optional(v.id("notificacoesEmail")) }), + returns: v.object({ + sucesso: v.boolean(), + emailId: v.optional(v.id("notificacoesEmail")), + }), handler: async (ctx, args) => { // Validar email const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -51,7 +61,10 @@ export const enviarEmailComTemplate = mutation({ variaveis: v.any(), // Record enviadoPorId: v.id("usuarios"), }, - returns: v.object({ sucesso: v.boolean(), emailId: v.optional(v.id("notificacoesEmail")) }), + returns: v.object({ + sucesso: v.boolean(), + emailId: v.optional(v.id("notificacoesEmail")), + }), handler: async (ctx, args) => { // Buscar template const template = await ctx.db @@ -90,25 +103,32 @@ export const enviarEmailComTemplate = mutation({ */ export const listarFilaEmails = query({ args: { - status: v.optional(v.union( - v.literal("pendente"), - v.literal("enviando"), - v.literal("enviado"), - v.literal("falha") - )), + status: v.optional( + v.union( + v.literal("pendente"), + v.literal("enviando"), + v.literal("enviado"), + v.literal("falha") + ) + ), limite: v.optional(v.number()), }, + returns: v.array(v.any()), handler: async (ctx, args) => { - let query = ctx.db.query("notificacoesEmail"); - if (args.status) { - query = query.withIndex("by_status", (q) => q.eq("status", args.status)); - } else { - query = query.withIndex("by_criado_em"); + const emails = await ctx.db + .query("notificacoesEmail") + .withIndex("by_status", (q) => q.eq("status", args.status!)) + .order("desc") + .take(args.limite ?? 100); + return emails; } - const emails = await query.order("desc").take(args.limite || 100); - + const emails = await ctx.db + .query("notificacoesEmail") + .withIndex("by_criado_em") + .order("desc") + .take(args.limite ?? 100); return emails; }, }); @@ -141,9 +161,68 @@ export const reenviarEmail = mutation({ /** * Action para enviar email (será implementado com nodemailer) - * + * * NOTA: Este é um placeholder. Implementação real requer nodemailer. */ +export const getEmailById = internalQuery({ + args: { emailId: v.id("notificacoesEmail") }, + returns: v.union(v.any(), v.null()), + handler: async (ctx, args) => { + return await ctx.db.get(args.emailId); + }, +}); + +export const getActiveEmailConfig = internalQuery({ + args: {}, + returns: v.union(v.any(), v.null()), + handler: async (ctx) => { + return await ctx.db + .query("configuracaoEmail") + .withIndex("by_ativo", (q) => q.eq("ativo", true)) + .first(); + }, +}); + +export const markEmailEnviando = internalMutation({ + args: { emailId: v.id("notificacoesEmail") }, + returns: v.null(), + handler: async (ctx, args) => { + const email = await ctx.db.get(args.emailId); + await ctx.db.patch(args.emailId, { + status: "enviando", + tentativas: ((email as any)?.tentativas || 0) + 1, + ultimaTentativa: Date.now(), + }); + return null; + }, +}); + +export const markEmailEnviado = internalMutation({ + args: { emailId: v.id("notificacoesEmail") }, + returns: v.null(), + handler: async (ctx, args) => { + await ctx.db.patch(args.emailId, { + status: "enviado", + enviadoEm: Date.now(), + }); + return null; + }, +}); + +export const markEmailFalha = internalMutation({ + args: { emailId: v.id("notificacoesEmail"), erro: v.string() }, + returns: v.null(), + handler: async (ctx, args) => { + const email = await ctx.db.get(args.emailId); + await ctx.db.patch(args.emailId, { + status: "falha", + erroDetalhes: args.erro, + tentativas: ((email as any)?.tentativas || 0) + 1, + }); + return null; + }, +}); + export const enviarEmailAction = action({ args: { emailId: v.id("notificacoesEmail"), @@ -151,11 +230,11 @@ export const enviarEmailAction = action({ returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), handler: async (ctx, args) => { // TODO: Implementar com nodemailer quando instalado - + try { // Buscar email da fila - const email = await ctx.runQuery(async (ctx) => { - return await ctx.db.get(args.emailId); + const email = await ctx.runQuery(internal.email.getEmailById, { + emailId: args.emailId, }); if (!email) { @@ -163,52 +242,41 @@ export const enviarEmailAction = action({ } // Buscar configuração SMTP - const config = await ctx.runQuery(async (ctx) => { - return await ctx.db - .query("configuracaoEmail") - .withIndex("by_ativo", (q) => q.eq("ativo", true)) - .first(); - }); + const config = await ctx.runQuery( + internal.email.getActiveEmailConfig, + {} + ); if (!config) { return { sucesso: false, erro: "Configuração de email não encontrada" }; } // Marcar como enviando - await ctx.runMutation(async (ctx) => { - await ctx.db.patch(args.emailId, { - status: "enviando", - tentativas: (email.tentativas || 0) + 1, - ultimaTentativa: Date.now(), - }); + await ctx.runMutation(internal.email.markEmailEnviando, { + emailId: args.emailId, }); // TODO: Enviar email real com nodemailer aqui - console.log("⚠️ AVISO: Envio de email simulado (nodemailer não instalado)"); - console.log(" Para:", email.destinatario); - console.log(" Assunto:", email.assunto); + console.log( + "⚠️ AVISO: Envio de email simulado (nodemailer não instalado)" + ); + console.log(" Para:", (email as any).destinatario); + console.log(" Assunto:", (email as any).assunto); // Simular delay de envio await new Promise((resolve) => setTimeout(resolve, 500)); // Marcar como enviado - await ctx.runMutation(async (ctx) => { - await ctx.db.patch(args.emailId, { - status: "enviado", - enviadoEm: Date.now(), - }); + await ctx.runMutation(internal.email.markEmailEnviado, { + emailId: args.emailId, }); return { sucesso: true }; } catch (error: any) { // Marcar como falha - await ctx.runMutation(async (ctx) => { - const email = await ctx.db.get(args.emailId); - await ctx.db.patch(args.emailId, { - status: "falha", - erroDetalhes: error.message || "Erro desconhecido", - tentativas: (email?.tentativas || 0) + 1, - }); + await ctx.runMutation(internal.email.markEmailFalha, { + emailId: args.emailId, + erro: error.message || "Erro ao enviar email", }); return { sucesso: false, erro: error.message || "Erro ao enviar email" }; @@ -221,6 +289,7 @@ export const enviarEmailAction = action({ */ export const processarFilaEmails = internalMutation({ args: {}, + returns: v.object({ processados: v.number() }), handler: async (ctx) => { // Buscar emails pendentes (max 10 por execução) const emailsPendentes = await ctx.db @@ -255,5 +324,3 @@ export const processarFilaEmails = internalMutation({ return { processados }; }, }); - - diff --git a/packages/backend/convex/funcionarios.ts b/packages/backend/convex/funcionarios.ts index 1d7c8e1..2c63275 100644 --- a/packages/backend/convex/funcionarios.ts +++ b/packages/backend/convex/funcionarios.ts @@ -1,18 +1,49 @@ import { v } from "convex/values"; import { query, mutation } from "./_generated/server"; +import { internal } from "./_generated/api"; import { simboloTipo } from "./schema"; // Validadores para campos opcionais -const sexoValidator = v.optional(v.union(v.literal("masculino"), v.literal("feminino"), v.literal("outro"))); -const estadoCivilValidator = v.optional(v.union(v.literal("solteiro"), v.literal("casado"), v.literal("divorciado"), v.literal("viuvo"), v.literal("uniao_estavel"))); -const grauInstrucaoValidator = v.optional(v.union(v.literal("fundamental"), v.literal("medio"), v.literal("superior"), v.literal("pos_graduacao"), v.literal("mestrado"), v.literal("doutorado"))); -const grupoSanguineoValidator = v.optional(v.union(v.literal("A"), v.literal("B"), v.literal("AB"), v.literal("O"))); -const fatorRHValidator = v.optional(v.union(v.literal("positivo"), v.literal("negativo"))); -const aposentadoValidator = v.optional(v.union(v.literal("nao"), v.literal("funape_ipsep"), v.literal("inss"))); +const sexoValidator = v.optional( + v.union(v.literal("masculino"), v.literal("feminino"), v.literal("outro")) +); +const estadoCivilValidator = v.optional( + v.union( + v.literal("solteiro"), + v.literal("casado"), + v.literal("divorciado"), + v.literal("viuvo"), + v.literal("uniao_estavel") + ) +); +const grauInstrucaoValidator = v.optional( + v.union( + v.literal("fundamental"), + v.literal("medio"), + v.literal("superior"), + v.literal("pos_graduacao"), + v.literal("mestrado"), + v.literal("doutorado") + ) +); +const grupoSanguineoValidator = v.optional( + v.union(v.literal("A"), v.literal("B"), v.literal("AB"), v.literal("O")) +); +const fatorRHValidator = v.optional( + v.union(v.literal("positivo"), v.literal("negativo")) +); +const aposentadoValidator = v.optional( + v.union(v.literal("nao"), v.literal("funape_ipsep"), v.literal("inss")) +); export const getAll = query({ args: {}, handler: async (ctx) => { + // Autorização: listar funcionários + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "funcionarios", + acao: "listar", + }); const funcionarios = await ctx.db.query("funcionarios").collect(); // Retornar apenas os campos necessários para listagem return funcionarios.map((f: any) => ({ @@ -40,6 +71,11 @@ export const getAll = query({ export const getById = query({ args: { id: v.id("funcionarios") }, handler: async (ctx, args) => { + // Autorização: ver funcionário + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "funcionarios", + acao: "ver", + }); return await ctx.db.get(args.id); }, }); @@ -62,7 +98,7 @@ export const create = mutation({ admissaoData: v.optional(v.string()), desligamentoData: v.optional(v.string()), simboloTipo: simboloTipo, - + // Dados Pessoais Adicionais nomePai: v.optional(v.string()), nomeMae: v.optional(v.string()), @@ -71,7 +107,7 @@ export const create = mutation({ sexo: sexoValidator, estadoCivil: estadoCivilValidator, nacionalidade: v.optional(v.string()), - + // Documentos Pessoais rgOrgaoExpedidor: v.optional(v.string()), rgDataEmissao: v.optional(v.string()), @@ -84,14 +120,14 @@ export const create = mutation({ tituloEleitorZona: v.optional(v.string()), tituloEleitorSecao: v.optional(v.string()), pisNumero: v.optional(v.string()), - + // Formação e Saúde grauInstrucao: grauInstrucaoValidator, formacao: v.optional(v.string()), formacaoRegistro: v.optional(v.string()), grupoSanguineo: grupoSanguineoValidator, fatorRH: fatorRHValidator, - + // Cargo e Vínculo descricaoCargo: v.optional(v.string()), nomeacaoPortaria: v.optional(v.string()), @@ -100,12 +136,12 @@ export const create = mutation({ pertenceOrgaoPublico: v.optional(v.boolean()), orgaoOrigem: v.optional(v.string()), aposentado: aposentadoValidator, - + // Dados Bancários contaBradescoNumero: v.optional(v.string()), contaBradescoDV: v.optional(v.string()), contaBradescoAgencia: v.optional(v.string()), - + // Documentos Anexos (Storage IDs) certidaoAntecedentesPF: v.optional(v.id("_storage")), certidaoAntecedentesJFPE: v.optional(v.id("_storage")), @@ -130,7 +166,7 @@ export const create = mutation({ comprovanteEscolaridade: v.optional(v.id("_storage")), comprovanteResidencia: v.optional(v.id("_storage")), comprovanteContaBradesco: v.optional(v.id("_storage")), - + // Declarações (Storage IDs) declaracaoAcumulacaoCargo: v.optional(v.id("_storage")), declaracaoDependentesIR: v.optional(v.id("_storage")), @@ -140,6 +176,11 @@ export const create = mutation({ }, returns: v.id("funcionarios"), handler: async (ctx, args) => { + // Autorização: criar + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "funcionarios", + acao: "criar", + }); // Unicidade: CPF const cpfExists = await ctx.db .query("funcionarios") @@ -182,7 +223,7 @@ export const update = mutation({ admissaoData: v.optional(v.string()), desligamentoData: v.optional(v.string()), simboloTipo: simboloTipo, - + // Dados Pessoais Adicionais nomePai: v.optional(v.string()), nomeMae: v.optional(v.string()), @@ -191,7 +232,7 @@ export const update = mutation({ sexo: sexoValidator, estadoCivil: estadoCivilValidator, nacionalidade: v.optional(v.string()), - + // Documentos Pessoais rgOrgaoExpedidor: v.optional(v.string()), rgDataEmissao: v.optional(v.string()), @@ -204,14 +245,14 @@ export const update = mutation({ tituloEleitorZona: v.optional(v.string()), tituloEleitorSecao: v.optional(v.string()), pisNumero: v.optional(v.string()), - + // Formação e Saúde grauInstrucao: grauInstrucaoValidator, formacao: v.optional(v.string()), formacaoRegistro: v.optional(v.string()), grupoSanguineo: grupoSanguineoValidator, fatorRH: fatorRHValidator, - + // Cargo e Vínculo descricaoCargo: v.optional(v.string()), nomeacaoPortaria: v.optional(v.string()), @@ -220,12 +261,12 @@ export const update = mutation({ pertenceOrgaoPublico: v.optional(v.boolean()), orgaoOrigem: v.optional(v.string()), aposentado: aposentadoValidator, - + // Dados Bancários contaBradescoNumero: v.optional(v.string()), contaBradescoDV: v.optional(v.string()), contaBradescoAgencia: v.optional(v.string()), - + // Documentos Anexos (Storage IDs) certidaoAntecedentesPF: v.optional(v.id("_storage")), certidaoAntecedentesJFPE: v.optional(v.id("_storage")), @@ -250,7 +291,7 @@ export const update = mutation({ comprovanteEscolaridade: v.optional(v.id("_storage")), comprovanteResidencia: v.optional(v.id("_storage")), comprovanteContaBradesco: v.optional(v.id("_storage")), - + // Declarações (Storage IDs) declaracaoAcumulacaoCargo: v.optional(v.id("_storage")), declaracaoDependentesIR: v.optional(v.id("_storage")), @@ -260,6 +301,11 @@ export const update = mutation({ }, returns: v.null(), handler: async (ctx, args) => { + // Autorização: editar + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "funcionarios", + acao: "editar", + }); // Unicidade: CPF (excluindo o próprio registro) const cpfExists = await ctx.db .query("funcionarios") @@ -288,6 +334,11 @@ export const remove = mutation({ args: { id: v.id("funcionarios") }, returns: v.null(), handler: async (ctx, args) => { + // Autorização: excluir + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "funcionarios", + acao: "excluir", + }); // TODO: Talvez queiramos também remover os arquivos do storage await ctx.db.delete(args.id); return null; @@ -298,21 +349,27 @@ export const remove = mutation({ export const getFichaCompleta = query({ args: { id: v.id("funcionarios") }, handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "funcionarios", + acao: "ver", + }); const funcionario = await ctx.db.get(args.id); if (!funcionario) { return null; } - + // Buscar informações do símbolo const simbolo = await ctx.db.get(funcionario.simboloId); - + return { ...funcionario, - simbolo: simbolo ? { - nome: simbolo.nome, - descricao: simbolo.descricao, - valor: simbolo.valor, - } : null, + simbolo: simbolo + ? { + nome: simbolo.nome, + descricao: simbolo.descricao, + valor: simbolo.valor, + } + : null, }; }, }); diff --git a/packages/backend/convex/limparPerfisAntigos.ts b/packages/backend/convex/limparPerfisAntigos.ts index c97ec93..9ed3797 100644 --- a/packages/backend/convex/limparPerfisAntigos.ts +++ b/packages/backend/convex/limparPerfisAntigos.ts @@ -13,7 +13,7 @@ export const listarTodosRoles = query({ descricao: v.string(), nivel: v.number(), setor: v.optional(v.string()), - customizado: v.boolean(), + customizado: v.optional(v.boolean()), editavel: v.optional(v.boolean()), _creationTime: v.number(), }) @@ -35,7 +35,7 @@ export const listarTodosRoles = query({ /** * Limpar perfis antigos/duplicados - * + * * CRITÉRIOS: * - Manter apenas: ti_master (nível 0), admin (nível 2), ti_usuario (nível 2) * - Remover: admin antigo (nível 0), ti genérico (nível 1), outros duplicados @@ -61,14 +61,14 @@ export const limparPerfisAntigos = internalMutation({ }), handler: async (ctx) => { const roles = await ctx.db.query("roles").collect(); - + const removidos: Array<{ nome: string; descricao: string; nivel: number; motivo: string; }> = []; - + const mantidos: Array<{ nome: string; descricao: string; @@ -91,9 +91,10 @@ export const limparPerfisAntigos = internalMutation({ deveManter = true; perfisCorretos.set("ti_master", true); } else { - motivo = role.nivel !== 0 - ? "TI_MASTER deve ser nível 0, este é nível " + role.nivel - : "TI_MASTER duplicado"; + motivo = + role.nivel !== 0 + ? "TI_MASTER deve ser nível 0, este é nível " + role.nivel + : "TI_MASTER duplicado"; } } // ADMIN - Manter apenas o de nível 2 @@ -102,9 +103,10 @@ export const limparPerfisAntigos = internalMutation({ deveManter = true; perfisCorretos.set("admin", true); } else { - motivo = role.nivel !== 2 - ? "ADMIN deve ser nível 2, este é nível " + role.nivel - : "ADMIN duplicado"; + motivo = + role.nivel !== 2 + ? "ADMIN deve ser nível 2, este é nível " + role.nivel + : "ADMIN duplicado"; } } // TI_USUARIO - Manter apenas o de nível 2 @@ -113,14 +115,16 @@ export const limparPerfisAntigos = internalMutation({ deveManter = true; perfisCorretos.set("ti_usuario", true); } else { - motivo = role.nivel !== 2 - ? "TI_USUARIO deve ser nível 2, este é nível " + role.nivel - : "TI_USUARIO duplicado"; + motivo = + role.nivel !== 2 + ? "TI_USUARIO deve ser nível 2, este é nível " + role.nivel + : "TI_USUARIO duplicado"; } } // Perfis genéricos antigos (remover) else if (role.nome === "ti") { - motivo = "Perfil genérico 'ti' obsoleto - usar 'ti_master' ou 'ti_usuario'"; + motivo = + "Perfil genérico 'ti' obsoleto - usar 'ti_master' ou 'ti_usuario'"; } // Outros perfis específicos de setores (manter se forem nível >= 2) else if ( @@ -157,7 +161,9 @@ export const limparPerfisAntigos = internalMutation({ descricao: role.descricao, nivel: role.nivel, }); - console.log(`✅ MANTIDO: ${role.nome} (${role.descricao}) - Nível ${role.nivel}`); + console.log( + `✅ MANTIDO: ${role.nome} (${role.descricao}) - Nível ${role.nivel}` + ); } else { // Verificar se há usuários usando este perfil const usuariosComRole = await ctx.db @@ -286,5 +292,3 @@ export const verificarNiveisIncorretos = query({ return problemas; }, }); - - diff --git a/packages/backend/convex/logsAtividades.ts b/packages/backend/convex/logsAtividades.ts index 1c8109d..2a87b16 100644 --- a/packages/backend/convex/logsAtividades.ts +++ b/packages/backend/convex/logsAtividades.ts @@ -7,7 +7,7 @@ import { Doc, Id } from "./_generated/dataModel"; * Use em todas as mutations que modificam dados */ export async function registrarAtividade( - ctx: QueryCtx | MutationCtx, + ctx: MutationCtx, usuarioId: Id<"usuarios">, acao: string, recurso: string, @@ -37,21 +37,34 @@ export const listarAtividades = query({ limite: v.optional(v.number()), }, handler: async (ctx, args) => { - let query = ctx.db.query("logsAtividades"); + let atividades; - // Aplicar filtros if (args.usuarioId) { - query = query.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)); + atividades = await ctx.db + .query("logsAtividades") + .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId!)) + .order("desc") + .take(args.limite || 100); } else if (args.acao) { - query = query.withIndex("by_acao", (q) => q.eq("acao", args.acao)); + atividades = await ctx.db + .query("logsAtividades") + .withIndex("by_acao", (q) => q.eq("acao", args.acao!)) + .order("desc") + .take(args.limite || 100); } else if (args.recurso) { - query = query.withIndex("by_recurso", (q) => q.eq("recurso", args.recurso)); + atividades = await ctx.db + .query("logsAtividades") + .withIndex("by_recurso", (q) => q.eq("recurso", args.recurso!)) + .order("desc") + .take(args.limite || 100); } else { - query = query.withIndex("by_timestamp"); + atividades = await ctx.db + .query("logsAtividades") + .withIndex("by_timestamp") + .order("desc") + .take(args.limite || 100); } - let atividades = await query.order("desc").take(args.limite || 100); - // Filtrar por range de datas se fornecido if (args.dataInicio || args.dataFim) { atividades = atividades.filter((log) => { @@ -155,5 +168,3 @@ export const obterHistoricoRecurso = query({ return atividadesComUsuarios; }, }); - - diff --git a/packages/backend/convex/logsLogin.ts b/packages/backend/convex/logsLogin.ts index 4a9a1fa..acb377d 100644 --- a/packages/backend/convex/logsLogin.ts +++ b/packages/backend/convex/logsLogin.ts @@ -6,7 +6,7 @@ import { Doc, Id } from "./_generated/dataModel"; * Helper para registrar tentativas de login */ export async function registrarLogin( - ctx: QueryCtx | MutationCtx, + ctx: MutationCtx, dados: { usuarioId?: Id<"usuarios">; matriculaOuEmail: string; @@ -170,26 +170,32 @@ export const obterEstatisticasLogin = query({ // Logins por horário (hora do dia) const porHorario: Record = {}; - logs.filter((l) => l.sucesso).forEach((log) => { - const hora = new Date(log.timestamp).getHours(); - porHorario[hora] = (porHorario[hora] || 0) + 1; - }); + logs + .filter((l) => l.sucesso) + .forEach((log) => { + const hora = new Date(log.timestamp).getHours(); + porHorario[hora] = (porHorario[hora] || 0) + 1; + }); // Browser mais usado const porBrowser: Record = {}; - logs.filter((l) => l.sucesso).forEach((log) => { - if (log.browser) { - porBrowser[log.browser] = (porBrowser[log.browser] || 0) + 1; - } - }); + logs + .filter((l) => l.sucesso) + .forEach((log) => { + if (log.browser) { + porBrowser[log.browser] = (porBrowser[log.browser] || 0) + 1; + } + }); // Dispositivos mais usados const porDevice: Record = {}; - logs.filter((l) => l.sucesso).forEach((log) => { - if (log.device) { - porDevice[log.device] = (porDevice[log.device] || 0) + 1; - } - }); + logs + .filter((l) => l.sucesso) + .forEach((log) => { + if (log.device) { + porDevice[log.device] = (porDevice[log.device] || 0) + 1; + } + }); return { total: logs.length, @@ -231,4 +237,3 @@ export const verificarIPSuspeito = query({ }; }, }); - diff --git a/packages/backend/convex/menuPermissoes.ts b/packages/backend/convex/menuPermissoes.ts deleted file mode 100644 index 47dbfb2..0000000 --- a/packages/backend/convex/menuPermissoes.ts +++ /dev/null @@ -1,528 +0,0 @@ -import { v } from "convex/values"; -import { mutation, query } from "./_generated/server"; -import type { Id } from "./_generated/dataModel"; - -/** - * Lista de menus do sistema - */ -export const MENUS_SISTEMA = [ - { path: "/recursos-humanos", nome: "Recursos Humanos", descricao: "Gestão de funcionários e símbolos" }, - { path: "/recursos-humanos/funcionarios", nome: "Funcionários", descricao: "Cadastro e gestão de funcionários" }, - { path: "/recursos-humanos/simbolos", nome: "Símbolos", descricao: "Cadastro e gestão de símbolos" }, - { path: "/financeiro", nome: "Financeiro", descricao: "Gestão financeira" }, - { path: "/controladoria", nome: "Controladoria", descricao: "Controle e auditoria" }, - { path: "/licitacoes", nome: "Licitações", descricao: "Gestão de licitações" }, - { path: "/compras", nome: "Compras", descricao: "Gestão de compras" }, - { path: "/juridico", nome: "Jurídico", descricao: "Departamento jurídico" }, - { path: "/comunicacao", nome: "Comunicação", descricao: "Gestão de comunicação" }, - { path: "/programas-esportivos", nome: "Programas Esportivos", descricao: "Gestão de programas esportivos" }, - { path: "/secretaria-executiva", nome: "Secretaria Executiva", descricao: "Secretaria executiva" }, - { path: "/gestao-pessoas", nome: "Gestão de Pessoas", descricao: "Gestão de recursos humanos" }, - { path: "/ti", nome: "Tecnologia da Informação", descricao: "TI e suporte técnico" }, - { path: "/ti/painel-administrativo", nome: "Painel Administrativo TI", descricao: "Painel de administração do sistema" }, -] as const; - -/** - * Listar todas as permissões de menu para uma role - */ -export const listarPorRole = query({ - args: { roleId: v.id("roles") }, - returns: v.array( - v.object({ - _id: v.id("menuPermissoes"), - roleId: v.id("roles"), - menuPath: v.string(), - podeAcessar: v.boolean(), - podeConsultar: v.boolean(), - podeGravar: v.boolean(), - }) - ), - handler: async (ctx, args) => { - return await ctx.db - .query("menuPermissoes") - .withIndex("by_role", (q) => q.eq("roleId", args.roleId)) - .collect(); - }, -}); - -/** - * Verificar se um usuário tem permissão para acessar um menu - * Prioridade: Permissão personalizada > Permissão da role - */ -export const verificarAcesso = query({ - args: { - usuarioId: v.id("usuarios"), - menuPath: v.string(), - }, - returns: v.object({ - podeAcessar: v.boolean(), - podeConsultar: v.boolean(), - podeGravar: v.boolean(), - motivo: v.optional(v.string()), - }), - handler: async (ctx, args) => { - // Buscar o usuário - const usuario = await ctx.db.get(args.usuarioId); - if (!usuario) { - return { - podeAcessar: false, - podeConsultar: false, - podeGravar: false, - motivo: "Usuário não encontrado", - }; - } - - // Verificar se o usuário está ativo - if (!usuario.ativo) { - return { - podeAcessar: false, - podeConsultar: false, - podeGravar: false, - motivo: "Usuário inativo", - }; - } - - // Buscar a role do usuário - const role = await ctx.db.get(usuario.roleId); - if (!role) { - return { - podeAcessar: false, - podeConsultar: false, - podeGravar: false, - motivo: "Role não encontrada", - }; - } - - // Apenas TI_MASTER (nível 0) tem acesso total irrestrito - // Admin, TI_USUARIO e outros (nível >= 1) têm permissões configuráveis - if (role.nivel === 0) { - return { - podeAcessar: true, - podeConsultar: true, - podeGravar: true, - }; - } - - // Dashboard e Solicitar Acesso são públicos - if (args.menuPath === "/" || args.menuPath === "/solicitar-acesso") { - return { - podeAcessar: true, - podeConsultar: true, - podeGravar: false, - }; - } - - // 1. Verificar se existe permissão personalizada para este usuário - const permissaoPersonalizada = await ctx.db - .query("menuPermissoesPersonalizadas") - .withIndex("by_usuario_and_menu", (q) => - q.eq("usuarioId", args.usuarioId).eq("menuPath", args.menuPath) - ) - .first(); - - if (permissaoPersonalizada) { - return { - podeAcessar: permissaoPersonalizada.podeAcessar, - podeConsultar: permissaoPersonalizada.podeConsultar, - podeGravar: permissaoPersonalizada.podeGravar, - }; - } - - // 2. Se não houver permissão personalizada, verificar permissão da role - const permissaoRole = await ctx.db - .query("menuPermissoes") - .withIndex("by_role_and_menu", (q) => - q.eq("roleId", usuario.roleId).eq("menuPath", args.menuPath) - ) - .first(); - - if (!permissaoRole) { - return { - podeAcessar: false, - podeConsultar: false, - podeGravar: false, - motivo: "Sem permissão configurada para este menu", - }; - } - - return { - podeAcessar: permissaoRole.podeAcessar, - podeConsultar: permissaoRole.podeConsultar, - podeGravar: permissaoRole.podeGravar, - }; - }, -}); - -/** - * Atualizar ou criar permissão de menu para uma role - */ -export const atualizarPermissao = mutation({ - args: { - roleId: v.id("roles"), - menuPath: v.string(), - podeAcessar: v.boolean(), - podeConsultar: v.boolean(), - podeGravar: v.boolean(), - }, - returns: v.id("menuPermissoes"), - handler: async (ctx, args) => { - // Verificar se já existe uma permissão - const existente = await ctx.db - .query("menuPermissoes") - .withIndex("by_role_and_menu", (q) => - q.eq("roleId", args.roleId).eq("menuPath", args.menuPath) - ) - .first(); - - if (existente) { - // Atualizar permissão existente - await ctx.db.patch(existente._id, { - podeAcessar: args.podeAcessar, - podeConsultar: args.podeConsultar, - podeGravar: args.podeGravar, - }); - return existente._id; - } else { - // Criar nova permissão - return await ctx.db.insert("menuPermissoes", { - roleId: args.roleId, - menuPath: args.menuPath, - podeAcessar: args.podeAcessar, - podeConsultar: args.podeConsultar, - podeGravar: args.podeGravar, - }); - } - }, -}); - -/** - * Remover permissão de menu - */ -export const removerPermissao = mutation({ - args: { - permissaoId: v.id("menuPermissoes"), - }, - returns: v.null(), - handler: async (ctx, args) => { - await ctx.db.delete(args.permissaoId); - return null; - }, -}); - -/** - * Inicializar permissões padrão para uma role - */ -export const inicializarPermissoesRole = mutation({ - args: { - roleId: v.id("roles"), - }, - returns: v.null(), - handler: async (ctx, args) => { - // Buscar a role - const role = await ctx.db.get(args.roleId); - if (!role) { - throw new Error("Role não encontrada"); - } - - // Admin e TI não precisam de permissões específicas (acesso total) - if (role.nivel <= 1) { - return null; - } - - // Para outras roles, criar permissões básicas (apenas consulta) - for (const menu of MENUS_SISTEMA) { - // Verificar se já existe permissão - const existente = await ctx.db - .query("menuPermissoes") - .withIndex("by_role_and_menu", (q) => - q.eq("roleId", args.roleId).eq("menuPath", menu.path) - ) - .first(); - - if (!existente) { - // Criar permissão padrão (sem acesso) - await ctx.db.insert("menuPermissoes", { - roleId: args.roleId, - menuPath: menu.path, - podeAcessar: false, - podeConsultar: false, - podeGravar: false, - }); - } - } - - return null; - }, -}); - -/** - * Listar todos os menus do sistema - */ -export const listarMenus = query({ - args: {}, - returns: v.array( - v.object({ - path: v.string(), - nome: v.string(), - descricao: v.string(), - }) - ), - handler: async (ctx) => { - return MENUS_SISTEMA.map((menu) => ({ - path: menu.path, - nome: menu.nome, - descricao: menu.descricao, - })); - }, -}); - -/** - * Obter matriz de permissões (role x menu) para o painel de controle - */ -export const obterMatrizPermissoes = query({ - args: {}, - returns: v.array( - v.object({ - role: v.object({ - _id: v.id("roles"), - nome: v.string(), - nivel: v.number(), - descricao: v.string(), - }), - permissoes: v.array( - v.object({ - menuPath: v.string(), - menuNome: v.string(), - podeAcessar: v.boolean(), - podeConsultar: v.boolean(), - podeGravar: v.boolean(), - permissaoId: v.optional(v.id("menuPermissoes")), - }) - ), - }) - ), - handler: async (ctx) => { - // Buscar todas as roles - // TI_MASTER (nível 0) aparece mas não é editável - // Admin, TI_USUARIO e outros (nível >= 1) são configuráveis - const roles = await ctx.db.query("roles").collect(); - - const matriz = []; - - for (const role of roles) { - const permissoes = []; - - for (const menu of MENUS_SISTEMA) { - // Buscar permissão específica - const permissao = await ctx.db - .query("menuPermissoes") - .withIndex("by_role_and_menu", (q) => - q.eq("roleId", role._id).eq("menuPath", menu.path) - ) - .first(); - - // Admin e TI têm acesso total automático - if (role.nivel <= 1) { - permissoes.push({ - menuPath: menu.path, - menuNome: menu.nome, - podeAcessar: true, - podeConsultar: true, - podeGravar: true, - permissaoId: permissao?._id, - }); - } else { - permissoes.push({ - menuPath: menu.path, - menuNome: menu.nome, - podeAcessar: permissao?.podeAcessar ?? false, - podeConsultar: permissao?.podeConsultar ?? false, - podeGravar: permissao?.podeGravar ?? false, - permissaoId: permissao?._id, - }); - } - } - - matriz.push({ - role: { - _id: role._id, - nome: role.nome, - nivel: role.nivel, - descricao: role.descricao, - }, - permissoes, - }); - } - - return matriz; - }, -}); - -/** - * Criar ou atualizar permissão personalizada por matrícula - */ -export const atualizarPermissaoPersonalizada = mutation({ - args: { - matricula: v.string(), - menuPath: v.string(), - podeAcessar: v.boolean(), - podeConsultar: v.boolean(), - podeGravar: v.boolean(), - }, - returns: v.union(v.id("menuPermissoesPersonalizadas"), v.null()), - handler: async (ctx, args) => { - // Buscar usuário pela matrícula - const usuario = await ctx.db - .query("usuarios") - .withIndex("by_matricula", (q) => q.eq("matricula", args.matricula)) - .first(); - - if (!usuario) { - throw new Error("Usuário não encontrado com esta matrícula"); - } - - // Verificar se já existe permissão personalizada - const existente = await ctx.db - .query("menuPermissoesPersonalizadas") - .withIndex("by_usuario_and_menu", (q) => - q.eq("usuarioId", usuario._id).eq("menuPath", args.menuPath) - ) - .first(); - - if (existente) { - // Atualizar permissão existente - await ctx.db.patch(existente._id, { - podeAcessar: args.podeAcessar, - podeConsultar: args.podeConsultar, - podeGravar: args.podeGravar, - }); - return existente._id; - } else { - // Criar nova permissão - return await ctx.db.insert("menuPermissoesPersonalizadas", { - usuarioId: usuario._id, - matricula: args.matricula, - menuPath: args.menuPath, - podeAcessar: args.podeAcessar, - podeConsultar: args.podeConsultar, - podeGravar: args.podeGravar, - }); - } - }, -}); - -/** - * Remover permissão personalizada - */ -export const removerPermissaoPersonalizada = mutation({ - args: { - permissaoId: v.id("menuPermissoesPersonalizadas"), - }, - returns: v.null(), - handler: async (ctx, args) => { - await ctx.db.delete(args.permissaoId); - return null; - }, -}); - -/** - * Listar permissões personalizadas de um usuário por matrícula - */ -export const listarPermissoesPersonalizadas = query({ - args: { - matricula: v.string(), - }, - returns: v.array( - v.object({ - _id: v.id("menuPermissoesPersonalizadas"), - menuPath: v.string(), - menuNome: v.string(), - podeAcessar: v.boolean(), - podeConsultar: v.boolean(), - podeGravar: v.boolean(), - }) - ), - handler: async (ctx, args) => { - // Buscar usuário - const usuario = await ctx.db - .query("usuarios") - .withIndex("by_matricula", (q) => q.eq("matricula", args.matricula)) - .first(); - - if (!usuario) { - return []; - } - - // Buscar permissões personalizadas - const permissoes = await ctx.db - .query("menuPermissoesPersonalizadas") - .withIndex("by_usuario", (q) => q.eq("usuarioId", usuario._id)) - .collect(); - - // Mapear com nomes dos menus - return permissoes.map((p) => { - const menu = MENUS_SISTEMA.find((m) => m.path === p.menuPath); - return { - _id: p._id, - menuPath: p.menuPath, - menuNome: menu?.nome || p.menuPath, - podeAcessar: p.podeAcessar, - podeConsultar: p.podeConsultar, - podeGravar: p.podeGravar, - }; - }); - }, -}); - -/** - * Buscar usuário por matrícula para o painel de personalização - */ -export const buscarUsuarioPorMatricula = query({ - args: { - matricula: v.string(), - }, - returns: v.union( - v.object({ - _id: v.id("usuarios"), - matricula: v.string(), - nome: v.string(), - email: v.string(), - role: v.object({ - nome: v.string(), - nivel: v.number(), - descricao: v.string(), - }), - ativo: v.boolean(), - }), - v.null() - ), - handler: async (ctx, args) => { - const usuario = await ctx.db - .query("usuarios") - .withIndex("by_matricula", (q) => q.eq("matricula", args.matricula)) - .first(); - - if (!usuario) { - return null; - } - - const role = await ctx.db.get(usuario.roleId); - if (!role) { - return null; - } - - return { - _id: usuario._id, - matricula: usuario.matricula, - nome: usuario.nome, - email: usuario.email, - role: { - nome: role.nome, - nivel: role.nivel, - descricao: role.descricao, - }, - ativo: usuario.ativo, - }; - }, -}); - diff --git a/packages/backend/convex/perfisCustomizados.ts b/packages/backend/convex/perfisCustomizados.ts index 87c89ca..34529bc 100644 --- a/packages/backend/convex/perfisCustomizados.ts +++ b/packages/backend/convex/perfisCustomizados.ts @@ -1,12 +1,15 @@ import { v } from "convex/values"; import { mutation, query } from "./_generated/server"; import { registrarAtividade } from "./logsAtividades"; +import { api } from "./_generated/api"; +import { Id } from "./_generated/dataModel"; /** * Listar todos os perfis customizados */ export const listarPerfisCustomizados = query({ args: {}, + returns: v.array(v.any()), handler: async (ctx) => { const perfis = await ctx.db.query("perfisCustomizados").collect(); @@ -15,7 +18,7 @@ export const listarPerfisCustomizados = query({ perfis.map(async (perfil) => { const role = await ctx.db.get(perfil.roleId); const criador = await ctx.db.get(perfil.criadoPor); - + // Contar usuários usando este perfil const usuarios = await ctx.db .query("usuarios") @@ -42,6 +45,16 @@ export const obterPerfilComPermissoes = query({ args: { perfilId: v.id("perfisCustomizados"), }, + returns: v.union( + v.object({ + perfil: v.any(), + role: v.any(), + permissoes: v.array(v.any()), + menuPermissoes: v.array(v.any()), + usuarios: v.array(v.any()), + }), + v.null() + ), handler: async (ctx, args) => { const perfil = await ctx.db.get(args.perfilId); if (!perfil) { @@ -99,20 +112,31 @@ export const criarPerfilCustomizado = mutation({ criadoPorId: v.id("usuarios"), }, returns: v.union( - v.object({ sucesso: v.literal(true), perfilId: v.id("perfisCustomizados") }), + v.object({ + sucesso: v.literal(true), + perfilId: v.id("perfisCustomizados"), + }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { // Validar nível (deve ser >= 3) if (args.nivel < 3) { - return { sucesso: false as const, erro: "Perfis customizados devem ter nível >= 3" }; + return { + sucesso: false as const, + erro: "Perfis customizados devem ter nível >= 3", + }; } // Verificar se nome já existe const roles = await ctx.db.query("roles").collect(); - const nomeExiste = roles.some((r) => r.nome.toLowerCase() === args.nome.toLowerCase()); + const nomeExiste = roles.some( + (r) => r.nome.toLowerCase() === args.nome.toLowerCase() + ); if (nomeExiste) { - return { sucesso: false as const, erro: "Já existe um perfil com este nome" }; + return { + sucesso: false as const, + erro: "Já existe um perfil com este nome", + }; } // Criar role correspondente @@ -130,7 +154,7 @@ export const criarPerfilCustomizado = mutation({ // Copiar permissões gerais const permissoesClonar = await ctx.db .query("rolePermissoes") - .withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId)) + .withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId!)) .collect(); for (const perm of permissoesClonar) { @@ -143,7 +167,7 @@ export const criarPerfilCustomizado = mutation({ // Copiar permissões de menu const menuPermsClonar = await ctx.db .query("menuPermissoes") - .withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId)) + .withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId!)) .collect(); for (const menuPerm of menuPermsClonar) { @@ -321,7 +345,10 @@ export const clonarPerfil = mutation({ criadoPorId: v.id("usuarios"), }, returns: v.union( - v.object({ sucesso: v.literal(true), perfilId: v.id("perfisCustomizados") }), + v.object({ + sucesso: v.literal(true), + perfilId: v.id("perfisCustomizados"), + }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { @@ -330,17 +357,80 @@ export const clonarPerfil = mutation({ return { sucesso: false as const, erro: "Perfil origem não encontrado" }; } - // Criar novo perfil clonando o original - const resultado = await criarPerfilCustomizado(ctx, { + // Verificar se nome já existe + const roles = await ctx.db.query("roles").collect(); + const nomeExiste = roles.some( + (r) => r.nome.toLowerCase() === args.novoNome.toLowerCase() + ); + if (nomeExiste) { + return { + sucesso: false as const, + erro: "Já existe um perfil com este nome", + }; + } + + // Criar role correspondente + const roleId = await ctx.db.insert("roles", { + nome: args.novoNome.toLowerCase().replace(/\s+/g, "_"), + descricao: args.novaDescricao, + nivel: perfilOrigem.nivel, + customizado: true, + criadoPor: args.criadoPorId, + editavel: true, + }); + + // Copiar permissões gerais do perfil de origem + const permissoesClonar = await ctx.db + .query("rolePermissoes") + .withIndex("by_role", (q) => q.eq("roleId", perfilOrigem.roleId)) + .collect(); + for (const perm of permissoesClonar) { + await ctx.db.insert("rolePermissoes", { + roleId, + permissaoId: perm.permissaoId, + }); + } + + // Copiar permissões de menu + const menuPermsClonar = await ctx.db + .query("menuPermissoes") + .withIndex("by_role", (q) => q.eq("roleId", perfilOrigem.roleId)) + .collect(); + for (const menuPerm of menuPermsClonar) { + await ctx.db.insert("menuPermissoes", { + roleId, + menuPath: menuPerm.menuPath, + podeAcessar: menuPerm.podeAcessar, + podeConsultar: menuPerm.podeConsultar, + podeGravar: menuPerm.podeGravar, + }); + } + + // Criar perfil customizado + const perfilId = await ctx.db.insert("perfisCustomizados", { nome: args.novoNome, descricao: args.novaDescricao, nivel: perfilOrigem.nivel, - clonarDeRoleId: perfilOrigem.roleId, - criadoPorId: args.criadoPorId, + roleId, + criadoPor: args.criadoPorId, + criadoEm: Date.now(), + atualizadoEm: Date.now(), }); - return resultado; + // Log de atividade + await registrarAtividade( + ctx as any, + args.criadoPorId, + "criar", + "perfis", + JSON.stringify({ + perfilId, + nome: args.novoNome, + nivel: perfilOrigem.nivel, + }), + perfilId + ); + + return { sucesso: true as const, perfilId }; }, }); - - diff --git a/packages/backend/convex/permissoesAcoes.ts b/packages/backend/convex/permissoesAcoes.ts new file mode 100644 index 0000000..25820e4 --- /dev/null +++ b/packages/backend/convex/permissoesAcoes.ts @@ -0,0 +1,210 @@ +import { query, mutation, internalQuery } from "./_generated/server"; +import { v } from "convex/values"; + +// Catálogo base de recursos e ações +// Ajuste/expanda conforme os módulos disponíveis no sistema +export const CATALOGO_RECURSOS = [ + { + recurso: "funcionarios", + acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"], + }, + { + recurso: "simbolos", + acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"], + }, +] as const; + +export const listarRecursosEAcoes = query({ + args: {}, + returns: v.array( + v.object({ + recurso: v.string(), + acoes: v.array(v.string()), + }) + ), + handler: async () => { + return CATALOGO_RECURSOS.map((r) => ({ + recurso: r.recurso, + acoes: [...r.acoes], + })); + }, +}); + +export const listarPermissoesAcoesPorRole = query({ + args: { roleId: v.id("roles") }, + returns: v.array( + v.object({ + recurso: v.string(), + acoes: v.array(v.string()), + }) + ), + handler: async (ctx, args) => { + // Buscar vínculos permissao<-role + const rolePerms = await ctx.db + .query("rolePermissoes") + .withIndex("by_role", (q) => q.eq("roleId", args.roleId)) + .collect(); + + // Carregar documentos de permissões + const actionsByResource: Record> = {}; + for (const rp of rolePerms) { + const perm = await ctx.db.get(rp.permissaoId); + if (!perm) continue; + const set = (actionsByResource[perm.recurso] ||= new Set()); + set.add(perm.acao); + } + + // Normalizar para todos os recursos do catálogo + const result: Array<{ recurso: string; acoes: Array }> = []; + for (const item of CATALOGO_RECURSOS) { + const granted = Array.from( + actionsByResource[item.recurso] ?? new Set() + ); + result.push({ recurso: item.recurso, acoes: granted }); + } + return result; + }, +}); + +export const atualizarPermissaoAcao = mutation({ + args: { + roleId: v.id("roles"), + recurso: v.string(), + acao: v.string(), + conceder: v.boolean(), + }, + returns: v.null(), + handler: async (ctx, args) => { + // Garantir documento de permissão (recurso+acao) + let permissao = await ctx.db + .query("permissoes") + .withIndex("by_recurso_e_acao", (q) => + q.eq("recurso", args.recurso).eq("acao", args.acao) + ) + .first(); + + if (!permissao) { + const nome = `${args.recurso}.${args.acao}`; + const descricao = `Permite ${args.acao} em ${args.recurso}`; + const id = await ctx.db.insert("permissoes", { + nome, + descricao, + recurso: args.recurso, + acao: args.acao, + }); + permissao = await ctx.db.get(id); + } + + if (!permissao) return null; + + // Verificar vínculo atual + const existente = await ctx.db + .query("rolePermissoes") + .withIndex("by_role", (q) => q.eq("roleId", args.roleId)) + .collect(); + + const vinculo = existente.find((rp) => rp.permissaoId === permissao!._id); + + if (args.conceder) { + if (!vinculo) { + await ctx.db.insert("rolePermissoes", { + roleId: args.roleId, + permissaoId: permissao._id, + }); + } + } else { + if (vinculo) { + await ctx.db.delete(vinculo._id); + } + } + return null; + }, +}); + +export const verificarAcao = query({ + args: { + usuarioId: v.id("usuarios"), + recurso: v.string(), + acao: v.string(), + }, + returns: v.null(), + handler: async (ctx, args) => { + const usuario = await ctx.db.get(args.usuarioId); + if (!usuario) throw new Error("acesso_negado"); + + const role = await ctx.db.get(usuario.roleId); + if (!role) throw new Error("acesso_negado"); + + // Níveis administrativos têm acesso total + if (role.nivel <= 1) return null; + + // Encontrar permissão + const permissao = await ctx.db + .query("permissoes") + .withIndex("by_recurso_e_acao", (q) => + q.eq("recurso", args.recurso).eq("acao", args.acao) + ) + .first(); + if (!permissao) throw new Error("acesso_negado"); + + const hasLink = await ctx.db + .query("rolePermissoes") + .withIndex("by_role", (q) => q.eq("roleId", usuario.roleId)) + .collect(); + const permitido = hasLink.some((rp) => rp.permissaoId === permissao!._id); + if (!permitido) throw new Error("acesso_negado"); + return null; + }, +}); + +export const assertPermissaoAcaoAtual = internalQuery({ + args: { + recurso: v.string(), + acao: v.string(), + }, + returns: v.null(), + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + let usuarioAtual: any = null; + + if (identity && identity.email) { + usuarioAtual = await ctx.db + .query("usuarios") + .withIndex("by_email", (q) => q.eq("email", identity.email!)) + .first(); + } + + if (!usuarioAtual) { + const sessaoAtiva = await ctx.db + .query("sessoes") + .filter((q) => q.eq(q.field("ativo"), true)) + .order("desc") + .first(); + if (sessaoAtiva) { + usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); + } + } + + if (!usuarioAtual) throw new Error("acesso_negado"); + + const role: any = await ctx.db.get(usuarioAtual.roleId as any); + if (!role) throw new Error("acesso_negado"); + if ((role as any).nivel <= 1) return null; + + const permissao = await ctx.db + .query("permissoes") + .withIndex("by_recurso_e_acao", (q) => + q.eq("recurso", args.recurso).eq("acao", args.acao) + ) + .first(); + if (!permissao) throw new Error("acesso_negado"); + + const links = await ctx.db + .query("rolePermissoes") + .withIndex("by_role", (q) => q.eq("roleId", (role as any)._id as any)) + .collect(); + const ok = links.some((rp) => rp.permissaoId === permissao!._id); + if (!ok) throw new Error("acesso_negado"); + return null; + }, +}); diff --git a/packages/backend/convex/roles.ts b/packages/backend/convex/roles.ts index ba840db..d4f2d48 100644 --- a/packages/backend/convex/roles.ts +++ b/packages/backend/convex/roles.ts @@ -14,7 +14,7 @@ export const listar = query({ descricao: v.string(), nivel: v.number(), setor: v.optional(v.string()), - customizado: v.boolean(), + customizado: v.optional(v.boolean()), editavel: v.optional(v.boolean()), criadoPor: v.optional(v.id("usuarios")), }) @@ -45,4 +45,3 @@ export const buscarPorId = query({ return await ctx.db.get(args.roleId); }, }); - diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index b73c2da..60c5be7 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -1,7 +1,5 @@ import { defineSchema, defineTable } from "convex/server"; import { Infer, v } from "convex/values"; -import { tables } from "./betterAuth/schema"; -import { cidrv4 } from "better-auth"; export const simboloTipo = v.union( v.literal("cargo_comissionado"), @@ -245,6 +243,7 @@ export default defineSchema({ acao: v.string(), // "criar", "ler", "editar", "excluir" }) .index("by_recurso", ["recurso"]) + .index("by_recurso_e_acao", ["recurso", "acao"]) .index("by_nome", ["nome"]), rolePermissoes: defineTable({ diff --git a/packages/backend/convex/seed.ts b/packages/backend/convex/seed.ts index 4b43a3b..29ec545 100644 --- a/packages/backend/convex/seed.ts +++ b/packages/backend/convex/seed.ts @@ -337,7 +337,7 @@ export const seedDatabase = internalMutation({ // 2. Criar usuários iniciais console.log("👤 Criando usuários iniciais..."); - + // TI Master const senhaTIMaster = await hashPassword("TI@123"); await ctx.db.insert("usuarios", { @@ -370,10 +370,59 @@ export const seedDatabase = internalMutation({ }); console.log(" ✅ Admin criado (matrícula: 2000, senha: Admin@123)"); + // 2.1 Criar catálogo de permissões por ação e conceder a Admin/TI + console.log("🔐 Criando permissões por ação..."); + const CATALOGO_RECURSOS = [ + { recurso: "dashboard", acoes: ["ver"] }, + { + recurso: "funcionarios", + acoes: ["ver", "listar", "criar", "editar", "excluir"], + }, + { + recurso: "simbolos", + acoes: ["ver", "listar", "criar", "editar", "excluir"], + }, + { + recurso: "usuarios", + acoes: ["ver", "listar", "criar", "editar", "excluir"], + }, + { + recurso: "perfis", + acoes: ["ver", "listar", "criar", "editar", "excluir"], + }, + ] as const; + + const permissaoKeyToId = new Map(); + for (const item of CATALOGO_RECURSOS) { + for (const acao of item.acoes) { + const nome = `${item.recurso}.${acao}`; + const id = await ctx.db.insert("permissoes", { + nome, + descricao: `Permite ${acao} em ${item.recurso}`, + recurso: item.recurso, + acao, + }); + permissaoKeyToId.set(nome, id); + } + } + console.log(` ✅ ${permissaoKeyToId.size} permissões criadas`); + + // Conceder todas permissões a Admin e TI + const rolesParaConceder = [roleAdmin, roleTIUsuario, roleTIMaster]; + for (const roleId of rolesParaConceder) { + for (const [, permId] of permissaoKeyToId) { + await ctx.db.insert("rolePermissoes", { + roleId: roleId as any, + permissaoId: permId as any, + }); + } + } + console.log(" ✅ Todas as permissões concedidas a Admin e TI"); + // 3. Inserir símbolos console.log("📝 Inserindo símbolos..."); const simbolosMap = new Map(); - + for (const simbolo of simbolosData) { const id = await ctx.db.insert("simbolos", { descricao: simbolo.descricao, @@ -393,7 +442,9 @@ export const seedDatabase = internalMutation({ for (const funcionario of funcionariosData) { const simboloId = simbolosMap.get(funcionario.simboloNome); if (!simboloId) { - console.error(` ❌ Símbolo não encontrado: ${funcionario.simboloNome}`); + console.error( + ` ❌ Símbolo não encontrado: ${funcionario.simboloNome}` + ); continue; } @@ -436,7 +487,9 @@ export const seedDatabase = internalMutation({ criadoEm: Date.now(), atualizadoEm: Date.now(), }); - console.log(` ✅ Usuário criado: ${funcionario.nome} (senha: Mudar@123)`); + console.log( + ` ✅ Usuário criado: ${funcionario.nome} (senha: Mudar@123)` + ); } // 6. Inserir solicitações de acesso @@ -462,28 +515,32 @@ export const seedDatabase = internalMutation({ codigo: "USUARIO_BLOQUEADO", nome: "Usuário Bloqueado", titulo: "Sua conta foi bloqueada", - corpo: "Sua conta no SGSE foi bloqueada.\\n\\nMotivo: {{motivo}}\\n\\nPara mais informações, entre em contato com a TI.", + corpo: + "Sua conta no SGSE foi bloqueada.\\n\\nMotivo: {{motivo}}\\n\\nPara mais informações, entre em contato com a TI.", variaveis: ["motivo"], }, { codigo: "USUARIO_DESBLOQUEADO", nome: "Usuário Desbloqueado", titulo: "Sua conta foi desbloqueada", - corpo: "Sua conta no SGSE foi desbloqueada e você já pode acessar o sistema normalmente.", + corpo: + "Sua conta no SGSE foi desbloqueada e você já pode acessar o sistema normalmente.", variaveis: [], }, { codigo: "SENHA_RESETADA", nome: "Senha Resetada", titulo: "Sua senha foi resetada", - corpo: "Sua senha foi resetada pela equipe de TI.\\n\\nNova senha temporária: {{senha}}\\n\\nPor favor, altere sua senha no próximo login.", + corpo: + "Sua senha foi resetada pela equipe de TI.\\n\\nNova senha temporária: {{senha}}\\n\\nPor favor, altere sua senha no próximo login.", variaveis: ["senha"], }, { codigo: "PERMISSAO_ALTERADA", nome: "Permissão Alterada", titulo: "Suas permissões foram atualizadas", - corpo: "Suas permissões de acesso ao sistema foram atualizadas.\\n\\nPara verificar suas novas permissões, acesse o menu de perfil.", + corpo: + "Suas permissões de acesso ao sistema foram atualizadas.\\n\\nPara verificar suas novas permissões, acesse o menu de perfil.", variaveis: [], }, { @@ -497,7 +554,8 @@ export const seedDatabase = internalMutation({ codigo: "BEM_VINDO", nome: "Boas-vindas", titulo: "Bem-vindo ao SGSE", - corpo: "Olá {{nome}},\\n\\nSeja bem-vindo ao Sistema de Gestão da Secretaria de Esportes!\\n\\nSuas credenciais de acesso:\\nMatrícula: {{matricula}}\\nSenha temporária: {{senha}}\\n\\nPor favor, altere sua senha no primeiro acesso.\\n\\nEquipe de TI", + corpo: + "Olá {{nome}},\\n\\nSeja bem-vindo ao Sistema de Gestão da Secretaria de Esportes!\\n\\nSuas credenciais de acesso:\\nMatrícula: {{matricula}}\\nSenha temporária: {{senha}}\\n\\nPor favor, altere sua senha no primeiro acesso.\\n\\nEquipe de TI", variaveis: ["nome", "matricula", "senha"], }, ]; @@ -584,11 +642,15 @@ export const clearDatabase = internalMutation({ console.log(` ✅ ${menuPermissoes.length} menu-permissões removidas`); // Limpar menu-permissões personalizadas - const menuPermissoesPersonalizadas = await ctx.db.query("menuPermissoesPersonalizadas").collect(); + const menuPermissoesPersonalizadas = await ctx.db + .query("menuPermissoesPersonalizadas") + .collect(); for (const mpp of menuPermissoesPersonalizadas) { await ctx.db.delete(mpp._id); } - console.log(` ✅ ${menuPermissoesPersonalizadas.length} menu-permissões personalizadas removidas`); + console.log( + ` ✅ ${menuPermissoesPersonalizadas.length} menu-permissões personalizadas removidas` + ); // Limpar role-permissões const rolePermissoes = await ctx.db.query("rolePermissoes").collect(); @@ -615,4 +677,3 @@ export const clearDatabase = internalMutation({ return null; }, }); - diff --git a/packages/backend/convex/usuarios.ts b/packages/backend/convex/usuarios.ts index 87c01fc..803918b 100644 --- a/packages/backend/convex/usuarios.ts +++ b/packages/backend/convex/usuarios.ts @@ -3,6 +3,7 @@ import { mutation, query } from "./_generated/server"; import { hashPassword, generateToken } from "./auth/utils"; import { registrarAtividade } from "./logsAtividades"; import { Id } from "./_generated/dataModel"; +import { api } from "./_generated/api"; /** * Criar novo usuário (apenas TI) @@ -106,9 +107,7 @@ export const listar = query({ // Filtrar por matrícula if (args.matricula) { - usuarios = usuarios.filter((u) => - u.matricula.includes(args.matricula!) - ); + usuarios = usuarios.filter((u) => u.matricula.includes(args.matricula!)); } // Filtrar por ativo @@ -349,9 +348,9 @@ export const atualizarPerfil = mutation({ handler: async (ctx, args) => { // TENTAR BETTER AUTH PRIMEIRO const identity = await ctx.auth.getUserIdentity(); - + let usuarioAtual = null; - + if (identity && identity.email) { // Buscar por email (Better Auth) usuarioAtual = await ctx.db @@ -359,7 +358,7 @@ export const atualizarPerfil = mutation({ .withIndex("by_email", (q) => q.eq("email", identity.email!)) .first(); } - + // SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado) if (!usuarioAtual) { const sessaoAtiva = await ctx.db @@ -367,7 +366,7 @@ export const atualizarPerfil = mutation({ .filter((q) => q.eq(q.field("ativo"), true)) .order("desc") .first(); - + if (sessaoAtiva) { usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); } @@ -382,17 +381,20 @@ export const atualizarPerfil = mutation({ // Atualizar apenas os campos fornecidos const updates: any = { atualizadoEm: Date.now() }; - + if (args.avatar !== undefined) updates.avatar = args.avatar; if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil; if (args.setor !== undefined) updates.setor = args.setor; - if (args.statusMensagem !== undefined) updates.statusMensagem = args.statusMensagem; + if (args.statusMensagem !== undefined) + updates.statusMensagem = args.statusMensagem; if (args.statusPresenca !== undefined) { updates.statusPresenca = args.statusPresenca; updates.ultimaAtividade = Date.now(); } - if (args.notificacoesAtivadas !== undefined) updates.notificacoesAtivadas = args.notificacoesAtivadas; - if (args.somNotificacao !== undefined) updates.somNotificacao = args.somNotificacao; + if (args.notificacoesAtivadas !== undefined) + updates.notificacoesAtivadas = args.notificacoesAtivadas; + if (args.somNotificacao !== undefined) + updates.somNotificacao = args.somNotificacao; await ctx.db.patch(usuarioAtual._id, updates); @@ -405,15 +407,40 @@ export const atualizarPerfil = mutation({ */ export const obterPerfil = query({ args: {}, + returns: v.union( + v.object({ + _id: v.id("usuarios"), + nome: v.string(), + email: v.string(), + matricula: v.string(), + avatar: v.optional(v.string()), + fotoPerfil: v.optional(v.id("_storage")), + fotoPerfilUrl: v.union(v.string(), v.null()), + setor: v.optional(v.string()), + statusMensagem: v.optional(v.string()), + statusPresenca: v.optional( + v.union( + v.literal("online"), + v.literal("offline"), + v.literal("ausente"), + v.literal("externo"), + v.literal("em_reuniao") + ) + ), + notificacoesAtivadas: v.boolean(), + somNotificacao: v.boolean(), + }), + v.null() + ), handler: async (ctx) => { console.log("=== DEBUG obterPerfil ==="); - + // TENTAR BETTER AUTH PRIMEIRO const identity = await ctx.auth.getUserIdentity(); console.log("Identity:", identity ? "encontrado" : "null"); - + let usuarioAtual = null; - + if (identity && identity.email) { console.log("Tentando buscar por email:", identity.email); // Buscar por email (Better Auth) @@ -421,10 +448,13 @@ export const obterPerfil = query({ .query("usuarios") .withIndex("by_email", (q) => q.eq("email", identity.email!)) .first(); - - console.log("Usuário encontrado por email:", usuarioAtual ? "SIM" : "NÃO"); + + console.log( + "Usuário encontrado por email:", + usuarioAtual ? "SIM" : "NÃO" + ); } - + // SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado) if (!usuarioAtual) { console.log("Buscando por sessão ativa..."); @@ -433,24 +463,30 @@ export const obterPerfil = query({ .filter((q) => q.eq(q.field("ativo"), true)) .order("desc") .first(); - + console.log("Sessão ativa encontrada:", sessaoAtiva ? "SIM" : "NÃO"); - + if (sessaoAtiva) { usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); - console.log("Usuário da sessão encontrado:", usuarioAtual ? "SIM" : "NÃO"); + console.log( + "Usuário da sessão encontrado:", + usuarioAtual ? "SIM" : "NÃO" + ); } } - + if (!usuarioAtual) { console.log("❌ Nenhum usuário encontrado"); // Listar todos os usuários para debug const todosUsuarios = await ctx.db.query("usuarios").collect(); console.log("Total de usuários no banco:", todosUsuarios.length); - console.log("Emails cadastrados:", todosUsuarios.map(u => u.email)); + console.log( + "Emails cadastrados:", + todosUsuarios.map((u) => u.email) + ); return null; } - + console.log("✅ Usuário encontrado:", usuarioAtual.nome); // Buscar fotoPerfil URL se existir @@ -542,12 +578,13 @@ export const listarParaChat = query({ */ export const uploadFotoPerfil = mutation({ args: {}, + returns: v.string(), handler: async (ctx) => { // TENTAR BETTER AUTH PRIMEIRO const identity = await ctx.auth.getUserIdentity(); - + let usuarioAtual = null; - + if (identity && identity.email) { // Buscar por email (Better Auth) usuarioAtual = await ctx.db @@ -555,7 +592,7 @@ export const uploadFotoPerfil = mutation({ .withIndex("by_email", (q) => q.eq("email", identity.email!)) .first(); } - + // SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado) if (!usuarioAtual) { const sessaoAtiva = await ctx.db @@ -563,7 +600,7 @@ export const uploadFotoPerfil = mutation({ .filter((q) => q.eq(q.field("ativo"), true)) .order("desc") .first(); - + if (sessaoAtiva) { usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); } @@ -743,7 +780,8 @@ export const resetarSenhaUsuario = mutation({ // Helper para gerar senha temporária function gerarSenhaTemporaria(): string { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%"; + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%"; let senha = ""; for (let i = 0; i < 12; i++) { senha += chars.charAt(Math.floor(Math.random() * chars.length)); @@ -811,6 +849,116 @@ export const editarUsuario = mutation({ }, }); +/** + * Criar/Promover usuário Admin Master (TI_MASTER - nível 0) + */ +export const criarAdminMaster = mutation({ + args: { + matricula: v.string(), + nome: v.string(), + email: v.string(), + senha: v.optional(v.string()), + }, + returns: v.union( + v.object({ + sucesso: v.literal(true), + usuarioId: v.id("usuarios"), + senhaTemporaria: v.string(), + }), + v.object({ sucesso: v.literal(false), erro: v.string() }) + ), + handler: async (ctx, args) => { + // Garantir que a role TI_MASTER exista (nível 0) + let roleTIMaster = await ctx.db + .query("roles") + .withIndex("by_nome", (q) => q.eq("nome", "ti_master")) + .first(); + + if (!roleTIMaster) { + const roleId = await ctx.db.insert("roles", { + nome: "ti_master", + descricao: "TI Master", + nivel: 0, + setor: "ti", + customizado: false, + editavel: false, + }); + roleTIMaster = await ctx.db.get(roleId); + } + + if (!roleTIMaster) { + return { + sucesso: false as const, + erro: "Falha ao garantir role TI Master", + }; + } + + // Se já existir usuário por matrícula, promove/atualiza + const existentePorMatricula = await ctx.db + .query("usuarios") + .withIndex("by_matricula", (q) => q.eq("matricula", args.matricula)) + .first(); + + const senhaTemporaria = args.senha || gerarSenhaTemporaria(); + const senhaHash = await hashPassword(senhaTemporaria); + + if (existentePorMatricula) { + await ctx.db.patch(existentePorMatricula._id, { + nome: args.nome, + email: args.email, + senhaHash, + roleId: roleTIMaster._id, + ativo: true, + primeiroAcesso: true, + atualizadoEm: Date.now(), + }); + return { + sucesso: true as const, + usuarioId: existentePorMatricula._id, + senhaTemporaria, + }; + } + + // Verificar se email já existe + const existentePorEmail = await ctx.db + .query("usuarios") + .withIndex("by_email", (q) => q.eq("email", args.email)) + .first(); + if (existentePorEmail) { + // Promove usuário existente por email + await ctx.db.patch(existentePorEmail._id, { + matricula: args.matricula, + nome: args.nome, + senhaHash, + roleId: roleTIMaster._id, + ativo: true, + primeiroAcesso: true, + atualizadoEm: Date.now(), + }); + return { + sucesso: true as const, + usuarioId: existentePorEmail._id, + senhaTemporaria, + }; + } + + // Criar novo usuário TI Master + const usuarioId = await ctx.db.insert("usuarios", { + matricula: args.matricula, + senhaHash, + nome: args.nome, + email: args.email, + roleId: roleTIMaster._id, + ativo: true, + primeiroAcesso: true, + criadoEm: Date.now(), + atualizadoEm: Date.now(), + }); + + return { sucesso: true as const, usuarioId, senhaTemporaria }; + }, +}); + /** * Desativar usuário logicamente (soft delete - apenas TI_MASTER) */ @@ -875,7 +1023,11 @@ export const criarUsuarioCompleto = mutation({ enviarEmailBoasVindas: v.optional(v.boolean()), }, returns: v.union( - v.object({ sucesso: v.literal(true), usuarioId: v.id("usuarios"), senhaTemporaria: v.string() }), + v.object({ + sucesso: v.literal(true), + usuarioId: v.id("usuarios"), + senhaTemporaria: v.string(), + }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { @@ -934,3 +1086,85 @@ export const criarUsuarioCompleto = mutation({ }, }); +/** + * Criar (ou garantir) um usuário ADMIN padrão + */ +export const criarAdminPadrao = mutation({ + args: { + matricula: v.optional(v.string()), + nome: v.optional(v.string()), + email: v.optional(v.string()), + senha: v.optional(v.string()), + }, + returns: v.object({ + sucesso: v.boolean(), + usuarioId: v.optional(v.id("usuarios")), + }), + handler: async (ctx, args) => { + const matricula = args.matricula ?? "0000"; + const nome = args.nome ?? "Administrador Geral"; + const email = args.email ?? "admin@sgse.pe.gov.br"; + const senha = args.senha ?? "Admin@123"; + + // Garantir role ADMIN (nível 2) + let roleAdmin = await ctx.db + .query("roles") + .withIndex("by_nome", (q) => q.eq("nome", "admin")) + .first(); + if (!roleAdmin) { + const roleId = await ctx.db.insert("roles", { + nome: "admin", + descricao: "Administrador Geral", + nivel: 2, + setor: "administrativo", + customizado: false, + editavel: true, + }); + roleAdmin = await ctx.db.get(roleId); + } + + if (!roleAdmin) return { sucesso: false }; + + // Verificar se já existe por matrícula ou email + const existentePorMatricula = await ctx.db + .query("usuarios") + .withIndex("by_matricula", (q) => q.eq("matricula", matricula)) + .first(); + + const existentePorEmail = await ctx.db + .query("usuarios") + .withIndex("by_email", (q) => q.eq("email", email)) + .first(); + + const senhaHash = await hashPassword(senha); + + if (existentePorMatricula || existentePorEmail) { + const alvo = existentePorMatricula ?? existentePorEmail!; + await ctx.db.patch(alvo._id, { + matricula, + nome, + email, + senhaHash, + roleId: roleAdmin._id, + ativo: true, + primeiroAcesso: false, + atualizadoEm: Date.now(), + }); + return { sucesso: true, usuarioId: alvo._id }; + } + + const usuarioId = await ctx.db.insert("usuarios", { + matricula, + senhaHash, + nome, + email, + roleId: roleAdmin._id, + ativo: true, + primeiroAcesso: false, + criadoEm: Date.now(), + atualizadoEm: Date.now(), + }); + + return { sucesso: true, usuarioId }; + }, +}); diff --git a/packages/backend/package.json b/packages/backend/package.json index 4f458e6..8448d0b 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -13,9 +13,7 @@ "typescript": "^5.9.2" }, "dependencies": { - "@convex-dev/better-auth": "^0.9.6", "@dicebear/avataaars": "^9.2.4", - "better-auth": "1.3.27", "convex": "^1.28.0" } -} +} \ No newline at end of file