refactor: remove unused authentication files and dependencies; update package.json to streamline dependencies and improve project structure
This commit is contained in:
4
packages/backend/convex/_generated/api.d.ts
vendored
4
packages/backend/convex/_generated/api.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -1,5 +0,0 @@
|
||||
import { defineComponent } from "convex/server";
|
||||
|
||||
const component = defineComponent("betterAuth");
|
||||
|
||||
export default component;
|
||||
@@ -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;
|
||||
@@ -1,7 +1,4 @@
|
||||
import { defineApp } from "convex/server";
|
||||
import betterAuth from "./betterAuth/convex.config";
|
||||
|
||||
const app = defineApp();
|
||||
app.use(betterAuth);
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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({
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
210
packages/backend/convex/permissoesAcoes.ts
Normal file
210
packages/backend/convex/permissoesAcoes.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user