refactor: remove unused authentication files and dependencies; update package.json to streamline dependencies and improve project structure

This commit is contained in:
2025-10-29 18:57:05 -03:00
parent f219340cd8
commit 1058375a90
29 changed files with 1426 additions and 1542 deletions

View File

@@ -10,7 +10,6 @@
"typescript": "^5.9.2"
},
"dependencies": {
"convex": "^1.28.0",
"better-auth": "1.3.27"
"convex": "^1.28.0"
}
}
}

View File

@@ -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;

View File

@@ -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 };
},
});

View File

@@ -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<DataModel>(components.betterAuth, {
local: {
schema: schema as any,
},
});
export const createAuth = (
ctx: GenericCtx<DataModel>,
{ 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);
},
});

View File

@@ -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);

View File

@@ -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);

View File

@@ -1,5 +0,0 @@
import { defineComponent } from "convex/server";
const component = defineComponent("betterAuth");
export default component;

View File

@@ -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;

View File

@@ -1,7 +1,4 @@
import { defineApp } from "convex/server";
import betterAuth from "./betterAuth/convex.config";
const app = defineApp();
app.use(betterAuth);
export default app;

View File

@@ -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<string, string>
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 };
},
});

View File

@@ -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,
};
},
});

View File

@@ -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;
},
});

View File

@@ -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;
},
});

View File

@@ -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<number, number> = {};
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<string, number> = {};
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<string, number> = {};
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({
};
},
});

View File

@@ -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,
};
},
});

View File

@@ -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 };
},
});

View File

@@ -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<string, Set<string>> = {};
for (const rp of rolePerms) {
const perm = await ctx.db.get(rp.permissaoId);
if (!perm) continue;
const set = (actionsByResource[perm.recurso] ||= new Set<string>());
set.add(perm.acao);
}
// Normalizar para todos os recursos do catálogo
const result: Array<{ recurso: string; acoes: Array<string> }> = [];
for (const item of CATALOGO_RECURSOS) {
const granted = Array.from(
actionsByResource[item.recurso] ?? new Set<string>()
);
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;
},
});

View File

@@ -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);
},
});

View File

@@ -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({

View File

@@ -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<string, string>();
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<string, string>();
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;
},
});

View File

@@ -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 };
},
});

View File

@@ -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"
}
}
}