-
-
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}
-
- {/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