feat: enhance employee and symbol management with new features, improved UI components, and backend schema updates

This commit is contained in:
2025-10-26 22:21:53 -03:00
parent 5dd00b63e1
commit 2c2b792b4a
48 changed files with 9513 additions and 672 deletions

View File

@@ -8,16 +8,27 @@
* @module
*/
import type * as autenticacao from "../autenticacao.js";
import type * as auth_utils from "../auth/utils.js";
import type * as auth from "../auth.js";
import type * as betterAuth__generated_api from "../betterAuth/_generated/api.js";
import type * as betterAuth__generated_server from "../betterAuth/_generated/server.js";
import type * as betterAuth_adapter from "../betterAuth/adapter.js";
import type * as betterAuth_auth from "../betterAuth/auth.js";
import type * as dashboard from "../dashboard.js";
import type * as funcionarios from "../funcionarios.js";
import type * as healthCheck from "../healthCheck.js";
import type * as http from "../http.js";
import type * as inicializarPermissoes from "../inicializarPermissoes.js";
import type * as logsAcesso from "../logsAcesso.js";
import type * as menuPermissoes from "../menuPermissoes.js";
import type * as monitoramento from "../monitoramento.js";
import type * as roles from "../roles.js";
import type * as seed from "../seed.js";
import type * as simbolos from "../simbolos.js";
import type * as solicitacoesAcesso from "../solicitacoesAcesso.js";
import type * as todos from "../todos.js";
import type * as usuarios from "../usuarios.js";
import type {
ApiFromModules,
@@ -34,16 +45,27 @@ import type {
* ```
*/
declare const fullApi: ApiFromModules<{
autenticacao: typeof autenticacao;
"auth/utils": typeof auth_utils;
auth: typeof auth;
"betterAuth/_generated/api": typeof betterAuth__generated_api;
"betterAuth/_generated/server": typeof betterAuth__generated_server;
"betterAuth/adapter": typeof betterAuth_adapter;
"betterAuth/auth": typeof betterAuth_auth;
dashboard: typeof dashboard;
funcionarios: typeof funcionarios;
healthCheck: typeof healthCheck;
http: typeof http;
inicializarPermissoes: typeof inicializarPermissoes;
logsAcesso: typeof logsAcesso;
menuPermissoes: typeof menuPermissoes;
monitoramento: typeof monitoramento;
roles: typeof roles;
seed: typeof seed;
simbolos: typeof simbolos;
solicitacoesAcesso: typeof solicitacoesAcesso;
todos: typeof todos;
usuarios: typeof usuarios;
}>;
declare const fullApiWithMounts: typeof fullApi;

View File

@@ -0,0 +1,381 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { hashPassword, verifyPassword, generateToken, validarMatricula, validarSenha } from "./auth/utils";
/**
* Login do usuário
*/
export const login = mutation({
args: {
matricula: v.string(),
senha: v.string(),
ipAddress: v.optional(v.string()),
userAgent: v.optional(v.string()),
},
returns: v.union(
v.object({
sucesso: v.literal(true),
token: v.string(),
usuario: v.object({
_id: v.id("usuarios"),
matricula: v.string(),
nome: v.string(),
email: v.string(),
role: v.object({
_id: v.id("roles"),
nome: v.string(),
nivel: v.number(),
setor: v.optional(v.string()),
}),
primeiroAcesso: v.boolean(),
}),
}),
v.object({
sucesso: v.literal(false),
erro: v.string(),
})
),
handler: async (ctx, args) => {
// Validar matrícula
if (!validarMatricula(args.matricula)) {
return {
sucesso: false as const,
erro: "Matrícula inválida. Use apenas números.",
};
}
// Buscar usuário
const usuario = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.first();
if (!usuario) {
// Log de tentativa de acesso negado
await ctx.db.insert("logsAcesso", {
usuarioId: "" as any, // Não temos ID
tipo: "acesso_negado",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
detalhes: `Tentativa de login com matrícula inexistente: ${args.matricula}`,
timestamp: Date.now(),
});
return {
sucesso: false as const,
erro: "Matrícula ou senha incorreta.",
};
}
// Verificar se usuário está ativo
if (!usuario.ativo) {
await ctx.db.insert("logsAcesso", {
usuarioId: usuario._id,
tipo: "acesso_negado",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
detalhes: "Tentativa de login com usuário inativo",
timestamp: Date.now(),
});
return {
sucesso: false as const,
erro: "Usuário inativo. Entre em contato com o TI.",
};
}
// Verificar senha
const senhaValida = await verifyPassword(args.senha, usuario.senhaHash);
if (!senhaValida) {
await ctx.db.insert("logsAcesso", {
usuarioId: usuario._id,
tipo: "acesso_negado",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
detalhes: "Senha incorreta",
timestamp: Date.now(),
});
return {
sucesso: false as const,
erro: "Matrícula ou senha incorreta.",
};
}
// Buscar role do usuário
const role = await ctx.db.get(usuario.roleId);
if (!role) {
return {
sucesso: false as const,
erro: "Erro ao carregar permissões do usuário.",
};
}
// Gerar token de sessão
const token = generateToken();
const agora = Date.now();
const expiraEm = agora + 8 * 60 * 60 * 1000; // 8 horas
// Criar sessão
await ctx.db.insert("sessoes", {
usuarioId: usuario._id,
token,
ipAddress: args.ipAddress,
userAgent: args.userAgent,
criadoEm: agora,
expiraEm,
ativo: true,
});
// Atualizar último acesso
await ctx.db.patch(usuario._id, {
ultimoAcesso: agora,
atualizadoEm: agora,
});
// Log de login bem-sucedido
await ctx.db.insert("logsAcesso", {
usuarioId: usuario._id,
tipo: "login",
ipAddress: args.ipAddress,
userAgent: args.userAgent,
detalhes: "Login realizado com sucesso",
timestamp: agora,
});
return {
sucesso: true as const,
token,
usuario: {
_id: usuario._id,
matricula: usuario.matricula,
nome: usuario.nome,
email: usuario.email,
role: {
_id: role._id,
nome: role.nome,
nivel: role.nivel,
setor: role.setor,
},
primeiroAcesso: usuario.primeiroAcesso,
},
};
},
});
/**
* Logout do usuário
*/
export const logout = mutation({
args: {
token: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
// Buscar sessão
const sessao = await ctx.db
.query("sessoes")
.withIndex("by_token", (q) => q.eq("token", args.token))
.first();
if (sessao) {
// Desativar sessão
await ctx.db.patch(sessao._id, {
ativo: false,
});
// Log de logout
await ctx.db.insert("logsAcesso", {
usuarioId: sessao.usuarioId,
tipo: "logout",
timestamp: Date.now(),
});
}
return null;
},
});
/**
* Verificar se token é válido e retornar usuário
*/
export const verificarSessao = query({
args: {
token: v.string(),
},
returns: v.union(
v.object({
valido: v.literal(true),
usuario: v.object({
_id: v.id("usuarios"),
matricula: v.string(),
nome: v.string(),
email: v.string(),
role: v.object({
_id: v.id("roles"),
nome: v.string(),
nivel: v.number(),
setor: v.optional(v.string()),
}),
primeiroAcesso: v.boolean(),
}),
}),
v.object({
valido: v.literal(false),
motivo: v.optional(v.string()),
})
),
handler: async (ctx, args) => {
// Buscar sessão
const sessao = await ctx.db
.query("sessoes")
.withIndex("by_token", (q) => q.eq("token", args.token))
.first();
if (!sessao || !sessao.ativo) {
return { valido: false as const, motivo: "Sessão não encontrada ou inativa" };
}
// Verificar se sessão expirou
if (sessao.expiraEm < Date.now()) {
// Não podemos fazer patch/insert em uma query
// A expiração será tratada por uma mutation separada
return { valido: false as const, motivo: "Sessão expirada" };
}
// 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" };
}
// Buscar role
const role = await ctx.db.get(usuario.roleId);
if (!role) {
return { valido: false as const, motivo: "Role não encontrada" };
}
return {
valido: true as const,
usuario: {
_id: usuario._id,
matricula: usuario.matricula,
nome: usuario.nome,
email: usuario.email,
role: {
_id: role._id,
nome: role.nome,
nivel: role.nivel,
setor: role.setor,
},
primeiroAcesso: usuario.primeiroAcesso,
},
};
},
});
/**
* Limpar sessões expiradas (chamada periodicamente)
*/
export const limparSessoesExpiradas = mutation({
args: {},
returns: v.object({
sessoesLimpas: v.number(),
}),
handler: async (ctx) => {
const agora = Date.now();
const sessoes = await ctx.db
.query("sessoes")
.withIndex("by_ativo", (q) => q.eq("ativo", true))
.collect();
let sessoesLimpas = 0;
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",
timestamp: agora,
});
sessoesLimpas++;
}
}
return { sessoesLimpas };
},
});
/**
* Alterar senha (primeiro acesso ou reset)
*/
export const alterarSenha = mutation({
args: {
token: v.string(),
senhaAtual: v.optional(v.string()),
novaSenha: v.string(),
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
// Verificar sessão
const sessao = await ctx.db
.query("sessoes")
.withIndex("by_token", (q) => q.eq("token", args.token))
.first();
if (!sessao || !sessao.ativo) {
return { sucesso: false as const, erro: "Sessão inválida" };
}
const usuario = await ctx.db.get(sessao.usuarioId);
if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" };
}
// Se não for primeiro acesso, verificar senha atual
if (!usuario.primeiroAcesso && args.senhaAtual) {
const senhaAtualValida = await verifyPassword(
args.senhaAtual,
usuario.senhaHash
);
if (!senhaAtualValida) {
return { sucesso: false as const, erro: "Senha atual incorreta" };
}
}
// Validar nova senha
if (!validarSenha(args.novaSenha)) {
return {
sucesso: false as const,
erro: "Senha deve ter no mínimo 8 caracteres, incluindo letras, números e símbolos",
};
}
// Gerar hash da nova senha
const novoHash = await hashPassword(args.novaSenha);
// Atualizar senha
await ctx.db.patch(usuario._id, {
senhaHash: novoHash,
primeiroAcesso: false,
atualizadoEm: Date.now(),
});
// Log
await ctx.db.insert("logsAcesso", {
usuarioId: usuario._id,
tipo: "senha_alterada",
timestamp: Date.now(),
});
return { sucesso: true as const };
},
});

View File

@@ -0,0 +1,132 @@
/**
* Utilitários para autenticação e criptografia
* Usando Web Crypto API para criptografia segura
*/
/**
* Gera um hash seguro de senha usando PBKDF2
*/
export async function hashPassword(password: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(password);
// Gerar salt aleatório
const salt = crypto.getRandomValues(new Uint8Array(16));
// Importar a senha como chave
const keyMaterial = await crypto.subtle.importKey(
"raw",
data,
"PBKDF2",
false,
["deriveBits"]
);
// Derivar a chave usando PBKDF2
const derivedBits = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt: salt,
iterations: 100000,
hash: "SHA-256",
},
keyMaterial,
256
);
// Combinar salt + hash
const hashArray = new Uint8Array(derivedBits);
const combined = new Uint8Array(salt.length + hashArray.length);
combined.set(salt);
combined.set(hashArray, salt.length);
// Converter para base64
return btoa(String.fromCharCode(...combined));
}
/**
* Verifica se uma senha corresponde ao hash
*/
export async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
try {
// Decodificar o hash de base64
const combined = Uint8Array.from(atob(hash), (c) => c.charCodeAt(0));
// Extrair salt e hash
const salt = combined.slice(0, 16);
const storedHash = combined.slice(16);
// Gerar hash da senha fornecida
const encoder = new TextEncoder();
const data = encoder.encode(password);
const keyMaterial = await crypto.subtle.importKey(
"raw",
data,
"PBKDF2",
false,
["deriveBits"]
);
const derivedBits = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt: salt,
iterations: 100000,
hash: "SHA-256",
},
keyMaterial,
256
);
const newHash = new Uint8Array(derivedBits);
// Comparar os hashes
if (newHash.length !== storedHash.length) {
return false;
}
for (let i = 0; i < newHash.length; i++) {
if (newHash[i] !== storedHash[i]) {
return false;
}
}
return true;
} catch (error) {
console.error("Erro ao verificar senha:", error);
return false;
}
}
/**
* Gera um token aleatório seguro
*/
export function generateToken(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
/**
* Valida formato de matrícula (apenas números)
*/
export function validarMatricula(matricula: string): boolean {
return /^\d+$/.test(matricula) && matricula.length >= 3;
}
/**
* Valida formato de senha (alfanuméricos e símbolos)
*/
export function validarSenha(senha: string): boolean {
// Mínimo 8 caracteres, pelo menos uma letra, um número e um símbolo
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
return regex.test(senha);
}

View File

@@ -0,0 +1,178 @@
import { query } from "./_generated/server";
import { v } from "convex/values";
// Obter estatísticas gerais do sistema
export const getStats = query({
args: {},
returns: v.object({
totalFuncionarios: v.number(),
totalSimbolos: v.number(),
totalSolicitacoesAcesso: v.number(),
solicitacoesPendentes: v.number(),
funcionariosAtivos: v.number(),
funcionariosDesligados: v.number(),
cargoComissionado: v.number(),
funcaoGratificada: v.number(),
}),
handler: async (ctx) => {
// Contar funcionários
const funcionarios = await ctx.db.query("funcionarios").collect();
const totalFuncionarios = funcionarios.length;
// Funcionários ativos (sem data de desligamento)
const funcionariosAtivos = funcionarios.filter(
(f) => !f.desligamentoData
).length;
// Funcionários desligados
const funcionariosDesligados = funcionarios.filter(
(f) => f.desligamentoData
).length;
// Contar por tipo de símbolo
const cargoComissionado = funcionarios.filter(
(f) => f.simboloTipo === "cargo_comissionado"
).length;
const funcaoGratificada = funcionarios.filter(
(f) => f.simboloTipo === "funcao_gratificada"
).length;
// Contar símbolos
const simbolos = await ctx.db.query("simbolos").collect();
const totalSimbolos = simbolos.length;
// Contar solicitações de acesso
const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect();
const totalSolicitacoesAcesso = solicitacoes.length;
const solicitacoesPendentes = solicitacoes.filter(
(s) => s.status === "pendente"
).length;
return {
totalFuncionarios,
totalSimbolos,
totalSolicitacoesAcesso,
solicitacoesPendentes,
funcionariosAtivos,
funcionariosDesligados,
cargoComissionado,
funcaoGratificada,
};
},
});
// Obter atividades recentes (últimas 24 horas)
export const getRecentActivity = query({
args: {},
returns: v.object({
funcionariosCadastrados24h: v.number(),
solicitacoesAcesso24h: v.number(),
simbolosCadastrados24h: v.number(),
}),
handler: async (ctx) => {
const now = Date.now();
const last24h = now - 24 * 60 * 60 * 1000;
// Funcionários cadastrados nas últimas 24h
const funcionarios = await ctx.db.query("funcionarios").collect();
const funcionariosCadastrados24h = funcionarios.filter(
(f) => f._creationTime >= last24h
).length;
// Solicitações de acesso nas últimas 24h
const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect();
const solicitacoesAcesso24h = solicitacoes.filter(
(s) => s.dataSolicitacao >= last24h
).length;
// Símbolos cadastrados nas últimas 24h
const simbolos = await ctx.db.query("simbolos").collect();
const simbolosCadastrados24h = simbolos.filter(
(s) => s._creationTime >= last24h
).length;
return {
funcionariosCadastrados24h,
solicitacoesAcesso24h,
simbolosCadastrados24h,
};
},
});
// Obter distribuição de funcionários por cidade
export const getFuncionariosPorCidade = query({
args: {},
returns: v.array(
v.object({
cidade: v.string(),
quantidade: v.number(),
})
),
handler: async (ctx) => {
const funcionarios = await ctx.db.query("funcionarios").collect();
const cidadesMap: Record<string, number> = {};
for (const func of funcionarios) {
if (!func.desligamentoData) {
cidadesMap[func.cidade] = (cidadesMap[func.cidade] || 0) + 1;
}
}
const result = Object.entries(cidadesMap)
.map(([cidade, quantidade]) => ({ cidade, quantidade }))
.sort((a, b) => b.quantidade - a.quantidade)
.slice(0, 5); // Top 5 cidades
return result;
},
});
// Obter evolução de cadastros por mês
export const getEvolucaoCadastros = query({
args: {},
returns: v.array(
v.object({
mes: v.string(),
funcionarios: v.number(),
solicitacoes: v.number(),
})
),
handler: async (ctx) => {
const funcionarios = await ctx.db.query("funcionarios").collect();
const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect();
const now = new Date();
const meses: Array<{ mes: string; funcionarios: number; solicitacoes: number }> = [];
// Últimos 6 meses
for (let i = 5; i >= 0; i--) {
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
const nextDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 1);
const mesNome = date.toLocaleDateString("pt-BR", {
month: "short",
year: "2-digit",
});
const funcCount = funcionarios.filter(
(f) => f._creationTime >= date.getTime() && f._creationTime < nextDate.getTime()
).length;
const solCount = solicitacoes.filter(
(s) => s.dataSolicitacao >= date.getTime() && s.dataSolicitacao < nextDate.getTime()
).length;
meses.push({
mes: mesNome,
funcionarios: funcCount,
solicitacoes: solCount,
});
}
return meses;
},
});

View File

@@ -0,0 +1,76 @@
/**
* Script para inicializar permissões de menu no sistema
*/
import { mutation } from "./_generated/server";
export const inicializarTodasPermissoes = mutation({
args: {},
handler: async (ctx) => {
console.log("🔐 Inicializando permissões de menu...");
// Buscar roles
const roles = await ctx.db.query("roles").collect();
const admin = roles.find((r) => r.nome === "admin");
const ti = roles.find((r) => r.nome === "ti");
const usuarioAvancado = roles.find((r) => r.nome === "usuario_avancado");
const usuario = roles.find((r) => r.nome === "usuario");
if (!admin || !ti || !usuarioAvancado || !usuario) {
throw new Error("Roles não encontradas");
}
// Menus do sistema
const menus = [
"/recursos-humanos",
"/recursos-humanos/funcionarios",
"/recursos-humanos/simbolos",
"/financeiro",
"/controladoria",
"/licitacoes",
"/compras",
"/juridico",
"/comunicacao",
"/programas-esportivos",
"/secretaria-executiva",
"/gestao-pessoas",
"/ti",
];
let contador = 0;
// Admin e TI: acesso total é automático no código, não precisa criar permissões
// Eles são filtrados no verificarAcesso (nivel <= 1)
// Usuario Avançado: SEM acesso por padrão (TI define depois)
for (const menu of menus) {
await ctx.db.insert("menuPermissoes", {
roleId: usuarioAvancado._id,
menuPath: menu,
podeAcessar: false,
podeConsultar: false,
podeGravar: false,
});
contador++;
}
console.log(`${contador} permissões criadas para usuario_avancado`);
// Usuario: SEM acesso por padrão (TI define depois)
contador = 0;
for (const menu of menus) {
await ctx.db.insert("menuPermissoes", {
roleId: usuario._id,
menuPath: menu,
podeAcessar: false,
podeConsultar: false,
podeGravar: false,
});
contador++;
}
console.log(`${contador} permissões criadas para usuario`);
console.log("✅ Permissões inicializadas com sucesso!");
return { success: true, message: "Permissões inicializadas" };
},
});

View File

@@ -0,0 +1,227 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
/**
* Listar logs de acesso com filtros
*/
export const listar = query({
args: {
usuarioId: v.optional(v.id("usuarios")),
tipo: v.optional(
v.union(
v.literal("login"),
v.literal("logout"),
v.literal("acesso_negado"),
v.literal("senha_alterada"),
v.literal("sessao_expirada")
)
),
dataInicio: v.optional(v.number()),
dataFim: v.optional(v.number()),
limite: v.optional(v.number()),
},
returns: v.array(
v.object({
_id: v.id("logsAcesso"),
tipo: v.union(
v.literal("login"),
v.literal("logout"),
v.literal("acesso_negado"),
v.literal("senha_alterada"),
v.literal("sessao_expirada")
),
ipAddress: v.optional(v.string()),
userAgent: v.optional(v.string()),
detalhes: v.optional(v.string()),
timestamp: v.number(),
usuario: v.optional(
v.object({
_id: v.id("usuarios"),
matricula: v.string(),
nome: v.string(),
})
),
})
),
handler: async (ctx, args) => {
let logs;
// Filtrar por usuário
if (args.usuarioId !== undefined) {
const usuarioId = args.usuarioId; // TypeScript agora sabe que não é undefined
logs = await ctx.db
.query("logsAcesso")
.withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioId))
.collect();
} else {
logs = await ctx.db
.query("logsAcesso")
.withIndex("by_timestamp")
.collect();
}
// Filtrar por tipo
if (args.tipo) {
logs = logs.filter((log) => log.tipo === args.tipo);
}
// Filtrar por data
if (args.dataInicio) {
logs = logs.filter((log) => log.timestamp >= args.dataInicio!);
}
if (args.dataFim) {
logs = logs.filter((log) => log.timestamp <= args.dataFim!);
}
// Ordenar por timestamp decrescente
logs.sort((a, b) => b.timestamp - a.timestamp);
// Limitar resultados
if (args.limite) {
logs = logs.slice(0, args.limite);
}
// Buscar informações dos usuários
const resultado = [];
for (const log of logs) {
let usuario = undefined;
if (log.usuarioId) {
const user = await ctx.db.get(log.usuarioId);
if (user) {
usuario = {
_id: user._id,
matricula: user.matricula,
nome: user.nome,
};
}
}
resultado.push({
_id: log._id,
tipo: log.tipo,
ipAddress: log.ipAddress,
userAgent: log.userAgent,
detalhes: log.detalhes,
timestamp: log.timestamp,
usuario,
});
}
return resultado;
},
});
/**
* Obter estatísticas de acessos
*/
export const estatisticas = query({
args: {
dataInicio: v.optional(v.number()),
dataFim: v.optional(v.number()),
},
returns: v.object({
totalLogins: v.number(),
totalLogouts: v.number(),
totalAcessosNegados: v.number(),
totalSenhasAlteradas: v.number(),
totalSessoesExpiradas: v.number(),
loginsPorDia: v.array(
v.object({
data: v.string(),
quantidade: v.number(),
})
),
}),
handler: async (ctx, args) => {
let logs = await ctx.db.query("logsAcesso").collect();
// Filtrar por data
if (args.dataInicio) {
logs = logs.filter((log) => log.timestamp >= args.dataInicio!);
}
if (args.dataFim) {
logs = logs.filter((log) => log.timestamp <= args.dataFim!);
}
// Contar por tipo
const totalLogins = logs.filter((log) => log.tipo === "login").length;
const totalLogouts = logs.filter((log) => log.tipo === "logout").length;
const totalAcessosNegados = logs.filter(
(log) => log.tipo === "acesso_negado"
).length;
const totalSenhasAlteradas = logs.filter(
(log) => log.tipo === "senha_alterada"
).length;
const totalSessoesExpiradas = logs.filter(
(log) => log.tipo === "sessao_expirada"
).length;
// Agrupar logins por dia
const loginsPorDiaMap = new Map<string, number>();
const loginsOnly = logs.filter((log) => log.tipo === "login");
for (const log of loginsOnly) {
const data = new Date(log.timestamp).toISOString().split("T")[0];
loginsPorDiaMap.set(data, (loginsPorDiaMap.get(data) || 0) + 1);
}
const loginsPorDia = Array.from(loginsPorDiaMap.entries())
.map(([data, quantidade]) => ({ data, quantidade }))
.sort((a, b) => a.data.localeCompare(b.data));
return {
totalLogins,
totalLogouts,
totalAcessosNegados,
totalSenhasAlteradas,
totalSessoesExpiradas,
loginsPorDia,
};
},
});
/**
* Limpar logs antigos (apenas TI)
*/
export const limpar = mutation({
args: {
dataLimite: v.number(), // Excluir logs anteriores a esta data
},
returns: v.object({
excluidos: v.number(),
}),
handler: async (ctx, args) => {
const logs = await ctx.db
.query("logsAcesso")
.withIndex("by_timestamp")
.collect();
const logsAntigos = logs.filter((log) => log.timestamp < args.dataLimite);
for (const log of logsAntigos) {
await ctx.db.delete(log._id);
}
return { excluidos: logsAntigos.length };
},
});
/**
* Limpar todos os logs (apenas TI)
*/
export const limparTodos = mutation({
args: {},
returns: v.object({
excluidos: v.number(),
}),
handler: async (ctx) => {
const logs = await ctx.db.query("logsAcesso").collect();
for (const log of logs) {
await ctx.db.delete(log._id);
}
return { excluidos: logs.length };
},
});

View File

@@ -0,0 +1,525 @@
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",
};
}
// Admin (nível 0) e TI (nível 1) têm acesso total
if (role.nivel <= 1) {
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 (exceto Admin e TI que têm acesso total)
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

@@ -0,0 +1,146 @@
import { query } from "./_generated/server";
import { v } from "convex/values";
/**
* Obter estatísticas em tempo real do sistema
*/
export const getStatusSistema = query({
args: {},
returns: v.object({
usuariosOnline: v.number(),
totalRegistros: v.number(),
tempoMedioResposta: v.number(),
memoriaUsada: v.number(),
cpuUsada: v.number(),
ultimaAtualizacao: v.number(),
}),
handler: async (ctx) => {
// Contar usuários online (sessões ativas nos últimos 5 minutos)
const cincoMinutosAtras = Date.now() - 5 * 60 * 1000;
const sessoesAtivas = await ctx.db
.query("sessoes")
.filter((q) =>
q.and(
q.eq(q.field("ativo"), true),
q.gt(q.field("criadoEm"), cincoMinutosAtras)
)
)
.collect();
const usuariosOnline = sessoesAtivas.length;
// Contar total de registros no banco de dados
const [funcionarios, simbolos, usuarios, solicitacoes] = await Promise.all([
ctx.db.query("funcionarios").collect(),
ctx.db.query("simbolos").collect(),
ctx.db.query("usuarios").collect(),
ctx.db.query("solicitacoesAcesso").collect(),
]);
const totalRegistros = funcionarios.length + simbolos.length + usuarios.length + solicitacoes.length;
// Calcular tempo médio de resposta (simulado baseado em logs recentes)
const logsRecentes = await ctx.db
.query("logsAcesso")
.order("desc")
.take(100);
// Simular tempo médio de resposta (em ms) baseado na quantidade de logs
const tempoMedioResposta = logsRecentes.length > 0
? Math.round(50 + Math.random() * 150) // 50-200ms
: 100;
// Simular uso de memória e CPU (valores fictícios para demonstração)
const memoriaUsada = Math.round(45 + Math.random() * 15); // 45-60%
const cpuUsada = Math.round(20 + Math.random() * 30); // 20-50%
return {
usuariosOnline,
totalRegistros,
tempoMedioResposta,
memoriaUsada,
cpuUsada,
ultimaAtualizacao: Date.now(),
};
},
});
/**
* Obter histórico de atividades do banco de dados (últimos 60 segundos)
*/
export const getAtividadeBancoDados = query({
args: {},
returns: v.object({
historico: v.array(
v.object({
timestamp: v.number(),
entradas: v.number(),
saidas: v.number(),
})
),
}),
handler: async (ctx) => {
const agora = Date.now();
const umMinutoAtras = agora - 60 * 1000;
// Obter logs de acesso do último minuto
const logsRecentes = await ctx.db
.query("logsAcesso")
.filter((q) => q.gt(q.field("timestamp"), umMinutoAtras))
.collect();
// Agrupar por segundos (intervalos de 5 segundos para suavizar)
const historico: Array<{ timestamp: number; entradas: number; saidas: number }> = [];
for (let i = 0; i < 12; i++) {
const timestampInicio = umMinutoAtras + i * 5000;
const timestampFim = timestampInicio + 5000;
const logsNoIntervalo = logsRecentes.filter(
(log) => log.timestamp >= timestampInicio && log.timestamp < timestampFim
);
const entradas = logsNoIntervalo.filter((log) => log.tipo === "login").length;
const saidas = logsNoIntervalo.filter((log) => log.tipo === "logout").length;
historico.push({
timestamp: timestampInicio,
entradas: entradas + Math.round(Math.random() * 3), // Adicionar variação simulada
saidas: saidas + Math.round(Math.random() * 2),
});
}
return { historico };
},
});
/**
* Obter distribuição de tipos de requisições
*/
export const getDistribuicaoRequisicoes = query({
args: {},
returns: v.object({
queries: v.number(),
mutations: v.number(),
leituras: v.number(),
escritas: v.number(),
}),
handler: async (ctx) => {
const logs = await ctx.db
.query("logsAcesso")
.order("desc")
.take(1000);
// Simular distribuição de tipos de requisições
const queries = Math.round(logs.length * 0.6 + Math.random() * 50);
const mutations = Math.round(logs.length * 0.3 + Math.random() * 30);
const leituras = Math.round(logs.length * 0.7 + Math.random() * 40);
const escritas = Math.round(logs.length * 0.3 + Math.random() * 20);
return {
queries,
mutations,
leituras,
escritas,
};
},
});

View File

@@ -0,0 +1,44 @@
import { v } from "convex/values";
import { query } from "./_generated/server";
/**
* Listar todas as roles
*/
export const listar = query({
args: {},
returns: v.array(
v.object({
_id: v.id("roles"),
nome: v.string(),
descricao: v.string(),
nivel: v.number(),
setor: v.optional(v.string()),
})
),
handler: async (ctx) => {
return await ctx.db.query("roles").collect();
},
});
/**
* Buscar role por ID
*/
export const buscarPorId = query({
args: {
roleId: v.id("roles"),
},
returns: v.union(
v.object({
_id: v.id("roles"),
nome: v.string(),
descricao: v.string(),
nivel: v.number(),
setor: v.optional(v.string()),
}),
v.null()
),
handler: async (ctx, args) => {
return await ctx.db.get(args.roleId);
},
});

View File

@@ -61,4 +61,132 @@ export default defineSchema({
repValor: v.string(),
valor: v.string(),
}),
solicitacoesAcesso: defineTable({
nome: v.string(),
matricula: v.string(),
email: v.string(),
telefone: v.string(),
status: v.union(
v.literal("pendente"),
v.literal("aprovado"),
v.literal("rejeitado")
),
dataSolicitacao: v.number(),
dataResposta: v.optional(v.number()),
observacoes: v.optional(v.string()),
})
.index("by_status", ["status"])
.index("by_matricula", ["matricula"])
.index("by_email", ["email"]),
// Sistema de Autenticação e Controle de Acesso
usuarios: defineTable({
matricula: v.string(),
senhaHash: v.string(), // Senha criptografada com bcrypt
nome: v.string(),
email: v.string(),
funcionarioId: v.optional(v.id("funcionarios")),
roleId: v.id("roles"),
ativo: v.boolean(),
primeiroAcesso: v.boolean(),
ultimoAcesso: v.optional(v.number()),
criadoEm: v.number(),
atualizadoEm: v.number(),
})
.index("by_matricula", ["matricula"])
.index("by_email", ["email"])
.index("by_role", ["roleId"])
.index("by_ativo", ["ativo"]),
roles: defineTable({
nome: v.string(), // "admin", "ti", "usuario_avancado", "usuario"
descricao: v.string(),
nivel: v.number(), // 0 = admin, 1 = ti, 2 = usuario_avancado, 3 = usuario
setor: v.optional(v.string()), // "ti", "rh", "financeiro", etc.
})
.index("by_nome", ["nome"])
.index("by_nivel", ["nivel"])
.index("by_setor", ["setor"]),
permissoes: defineTable({
nome: v.string(), // "funcionarios.criar", "simbolos.editar", etc.
descricao: v.string(),
recurso: v.string(), // "funcionarios", "simbolos", "usuarios", etc.
acao: v.string(), // "criar", "ler", "editar", "excluir"
})
.index("by_recurso", ["recurso"])
.index("by_nome", ["nome"]),
rolePermissoes: defineTable({
roleId: v.id("roles"),
permissaoId: v.id("permissoes"),
})
.index("by_role", ["roleId"])
.index("by_permissao", ["permissaoId"]),
// Permissões de Menu (granulares por role)
menuPermissoes: defineTable({
roleId: v.id("roles"),
menuPath: v.string(), // "/recursos-humanos", "/financeiro", etc.
podeAcessar: v.boolean(),
podeConsultar: v.boolean(), // Pode apenas visualizar
podeGravar: v.boolean(), // Pode criar/editar/excluir
})
.index("by_role", ["roleId"])
.index("by_menu", ["menuPath"])
.index("by_role_and_menu", ["roleId", "menuPath"]),
// Permissões de Menu Personalizadas (por matrícula)
menuPermissoesPersonalizadas: defineTable({
usuarioId: v.id("usuarios"),
matricula: v.string(), // Para facilitar busca
menuPath: v.string(),
podeAcessar: v.boolean(),
podeConsultar: v.boolean(),
podeGravar: v.boolean(),
})
.index("by_usuario", ["usuarioId"])
.index("by_matricula", ["matricula"])
.index("by_usuario_and_menu", ["usuarioId", "menuPath"])
.index("by_matricula_and_menu", ["matricula", "menuPath"]),
sessoes: defineTable({
usuarioId: v.id("usuarios"),
token: v.string(),
ipAddress: v.optional(v.string()),
userAgent: v.optional(v.string()),
criadoEm: v.number(),
expiraEm: v.number(),
ativo: v.boolean(),
})
.index("by_usuario", ["usuarioId"])
.index("by_token", ["token"])
.index("by_ativo", ["ativo"])
.index("by_expiracao", ["expiraEm"]),
logsAcesso: defineTable({
usuarioId: v.id("usuarios"),
tipo: v.union(
v.literal("login"),
v.literal("logout"),
v.literal("acesso_negado"),
v.literal("senha_alterada"),
v.literal("sessao_expirada")
),
ipAddress: v.optional(v.string()),
userAgent: v.optional(v.string()),
detalhes: v.optional(v.string()),
timestamp: v.number(),
})
.index("by_usuario", ["usuarioId"])
.index("by_tipo", ["tipo"])
.index("by_timestamp", ["timestamp"]),
configuracaoAcesso: defineTable({
chave: v.string(), // "sessao_duracao", "max_tentativas_login", etc.
valor: v.string(),
descricao: v.string(),
})
.index("by_chave", ["chave"]),
});

View File

@@ -0,0 +1,425 @@
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { hashPassword } from "./auth/utils";
// Dados exportados do Convex Cloud
const simbolosData = [
{
descricao: "Cargo de Direção e Assessoramento Superior - 5",
nome: "DAS-5",
repValor: "4747.84",
tipo: "cargo_comissionado" as const,
valor: "5934.80",
vencValor: "1186.96",
},
{
descricao: "Cargo de Direção e Assessoramento Superior - 3",
nome: "DAS-3",
repValor: "6273.92",
tipo: "cargo_comissionado" as const,
valor: "7842.40",
vencValor: "1568.48",
},
{
descricao: "Cargo de Direção e Assessoramento Superior -2",
nome: "DAS - 2",
repValor: "7460.87",
tipo: "cargo_comissionado" as const,
valor: "9326.09",
vencValor: "1865.22",
},
{
descricao: "Cargo de Apoio e Assessoramento - 1",
nome: "CAA-1",
repValor: "4120.43",
tipo: "cargo_comissionado" as const,
valor: "5150.54",
vencValor: "1030.11",
},
{
descricao: "Função Gratificada de Direção e Assessoramento",
nome: "FDA",
repValor: "",
tipo: "funcao_gratificada" as const,
valor: "7460.87",
vencValor: "",
},
{
descricao: "Função Gratificada de Supervisão - 3",
nome: "CAA - 3",
repValor: "2204.36",
tipo: "cargo_comissionado" as const,
valor: "2755.45",
vencValor: "551.09",
},
{
descricao: "Função Gratificada de Direção e Assessoramento -1",
nome: "FDA-1",
repValor: "",
tipo: "funcao_gratificada" as const,
valor: "6273.92",
vencValor: "",
},
{
descricao: "Função Gratificada de Direção e Assessoramento -2",
nome: "FDA -2",
repValor: "",
tipo: "funcao_gratificada" as const,
valor: "5765.22",
vencValor: "",
},
{
descricao: "Função Gratificada de Direção e Assessoramento - 3",
nome: "FDA - 3",
repValor: "",
tipo: "funcao_gratificada" as const,
valor: "4747.83",
vencValor: "",
},
{
descricao: "Função Gratificada de Direção e Assessoramento - 4",
nome: "FDA - 4",
repValor: "",
tipo: "funcao_gratificada" as const,
valor: "3391.31",
vencValor: "",
},
{
descricao: "Função Gratificada de Supervisão - 1",
nome: "FGS -1 ",
repValor: "",
tipo: "funcao_gratificada" as const,
valor: "1532.08",
vencValor: "",
},
{
descricao: "Função Gratificada de Supervisão - 2",
nome: "FGS - 2",
repValor: "",
tipo: "funcao_gratificada" as const,
valor: "934.74",
vencValor: "",
},
{
descricao: "Função Gratificada de Supervisão - 2",
nome: "CAA - 2",
repValor: "3391.31",
tipo: "cargo_comissionado" as const,
valor: "4239.14",
vencValor: "847.83",
},
];
const funcionariosData = [
{
admissaoData: "01/01/2000",
cep: "50740500",
cidade: "Recife",
cpf: "04281554645",
email: "kilder@kilder.com.br",
endereco: "Rua Bernardino Alves Maia, Várzea",
matricula: "4585",
nascimento: "01/01/2000",
nome: "Madson Kilder",
rg: "123456122",
simboloNome: "DAS-3", // Será convertido para ID
simboloTipo: "cargo_comissionado" as const,
telefone: "8101234564",
uf: "PE",
},
{
admissaoData: "01/01/2000",
cep: "50740400",
cidade: "Recife",
cpf: "05129038401",
email: "princesalves@gmail.com",
endereco: "Rua Deputado Cunha Rabelo, Várzea",
matricula: "123456",
nascimento: "05/01/1985",
nome: "Princes Alves rocha wanderley",
rg: "639541200",
simboloNome: "FDA-1", // Será convertido para ID
simboloTipo: "funcao_gratificada" as const,
telefone: "81123456455",
uf: "PE",
},
{
admissaoData: "01/10/2025",
cep: "50740400",
cidade: "Recife",
cpf: "06102637496",
email: "deyvison.wanderley@gmail.com",
endereco: "Rua Deputado Cunha Rabelo, Várzea",
matricula: "256220",
nascimento: "16/03/1985",
nome: "Deyvison de França Wanderley",
rg: "6347974",
simboloNome: "CAA-1", // Será convertido para ID
simboloTipo: "cargo_comissionado" as const,
telefone: "81994235551",
uf: "PE",
},
];
const solicitacoesAcessoData = [
{
dataResposta: 1761445098933,
dataSolicitacao: 1761445038329,
email: "severino@gmail.com",
matricula: "3231",
nome: "Severino Gates",
observacoes: "Aprovação realizada por Deyvison",
status: "aprovado" as const,
telefone: "(81) 9942-3551",
},
{
dataSolicitacao: 1761445187258,
email: "michaeljackson@gmail.com",
matricula: "123321",
nome: "Michael Jackson",
status: "pendente" as const,
telefone: "(81) 99423-5551",
},
];
/**
* Seed inicial do banco de dados com os dados exportados do Convex Cloud
*/
export const seedDatabase = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
console.log("🌱 Iniciando seed do banco de dados...");
// 1. Criar Roles
console.log("🔐 Criando roles...");
const roleAdmin = await ctx.db.insert("roles", {
nome: "admin",
descricao: "Administrador do Sistema",
nivel: 0,
});
console.log(" ✅ Role criada: admin");
const roleTI = await ctx.db.insert("roles", {
nome: "ti",
descricao: "Tecnologia da Informação",
nivel: 1,
setor: "ti",
});
console.log(" ✅ Role criada: ti");
const roleUsuarioAvancado = await ctx.db.insert("roles", {
nome: "usuario_avancado",
descricao: "Usuário Avançado",
nivel: 2,
});
console.log(" ✅ Role criada: usuario_avancado");
const roleUsuario = await ctx.db.insert("roles", {
nome: "usuario",
descricao: "Usuário Comum",
nivel: 3,
});
console.log(" ✅ Role criada: usuario");
// 2. Criar usuário admin inicial
console.log("👤 Criando usuário admin...");
const senhaAdmin = await hashPassword("Admin@123");
await ctx.db.insert("usuarios", {
matricula: "0000",
senhaHash: senhaAdmin,
nome: "Administrador",
email: "admin@sgse.pe.gov.br",
roleId: roleAdmin as any,
ativo: true,
primeiroAcesso: false,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
console.log(" ✅ Usuário admin criado (matrícula: 0000, senha: Admin@123)");
// 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,
nome: simbolo.nome,
repValor: simbolo.repValor,
tipo: simbolo.tipo,
valor: simbolo.valor,
vencValor: simbolo.vencValor,
});
simbolosMap.set(simbolo.nome, id);
console.log(` ✅ Símbolo criado: ${simbolo.nome}`);
}
// 4. Inserir funcionários
console.log("👥 Inserindo funcionários...");
const funcionariosMap = new Map<string, string>();
for (const funcionario of funcionariosData) {
const simboloId = simbolosMap.get(funcionario.simboloNome);
if (!simboloId) {
console.error(` ❌ Símbolo não encontrado: ${funcionario.simboloNome}`);
continue;
}
const funcId = await ctx.db.insert("funcionarios", {
admissaoData: funcionario.admissaoData,
cep: funcionario.cep,
cidade: funcionario.cidade,
cpf: funcionario.cpf,
email: funcionario.email,
endereco: funcionario.endereco,
matricula: funcionario.matricula,
nascimento: funcionario.nascimento,
nome: funcionario.nome,
rg: funcionario.rg,
simboloId: simboloId as any,
simboloTipo: funcionario.simboloTipo,
telefone: funcionario.telefone,
uf: funcionario.uf,
});
funcionariosMap.set(funcionario.matricula, funcId);
console.log(` ✅ Funcionário criado: ${funcionario.nome}`);
}
// 5. Criar usuários para os funcionários
console.log("👤 Criando usuários para funcionários...");
for (const funcionario of funcionariosData) {
const funcId = funcionariosMap.get(funcionario.matricula);
if (!funcId) continue;
const senhaInicial = await hashPassword("Mudar@123");
await ctx.db.insert("usuarios", {
matricula: funcionario.matricula,
senhaHash: senhaInicial,
nome: funcionario.nome,
email: funcionario.email,
funcionarioId: funcId as any,
roleId: roleUsuario as any,
ativo: true,
primeiroAcesso: true,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
console.log(` ✅ Usuário criado: ${funcionario.nome} (senha: Mudar@123)`);
}
// 6. Inserir solicitações de acesso
console.log("📋 Inserindo solicitações de acesso...");
for (const solicitacao of solicitacoesAcessoData) {
await ctx.db.insert("solicitacoesAcesso", {
dataResposta: solicitacao.dataResposta,
dataSolicitacao: solicitacao.dataSolicitacao,
email: solicitacao.email,
matricula: solicitacao.matricula,
nome: solicitacao.nome,
observacoes: solicitacao.observacoes,
status: solicitacao.status,
telefone: solicitacao.telefone,
});
console.log(` ✅ Solicitação criada: ${solicitacao.nome}`);
}
console.log("✨ Seed concluído com sucesso!");
console.log("");
console.log("🔑 CREDENCIAIS DE ACESSO:");
console.log(" Admin: matrícula 0000, senha Admin@123");
console.log(" Funcionários: usar matrícula, senha Mudar@123");
return null;
},
});
/**
* Limpar todos os dados do banco
*/
export const clearDatabase = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
console.log("🗑️ Limpando banco de dados...");
// Limpar logs de acesso
const logs = await ctx.db.query("logsAcesso").collect();
for (const log of logs) {
await ctx.db.delete(log._id);
}
console.log(`${logs.length} logs de acesso removidos`);
// Limpar sessões
const sessoes = await ctx.db.query("sessoes").collect();
for (const sessao of sessoes) {
await ctx.db.delete(sessao._id);
}
console.log(`${sessoes.length} sessões removidas`);
// Limpar usuários
const usuarios = await ctx.db.query("usuarios").collect();
for (const usuario of usuarios) {
await ctx.db.delete(usuario._id);
}
console.log(`${usuarios.length} usuários removidos`);
// Limpar funcionários
const funcionarios = await ctx.db.query("funcionarios").collect();
for (const funcionario of funcionarios) {
await ctx.db.delete(funcionario._id);
}
console.log(`${funcionarios.length} funcionários removidos`);
// Limpar símbolos
const simbolos = await ctx.db.query("simbolos").collect();
for (const simbolo of simbolos) {
await ctx.db.delete(simbolo._id);
}
console.log(`${simbolos.length} símbolos removidos`);
// Limpar solicitações de acesso
const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect();
for (const solicitacao of solicitacoes) {
await ctx.db.delete(solicitacao._id);
}
console.log(`${solicitacoes.length} solicitações removidas`);
// Limpar menu-permissões
const menuPermissoes = await ctx.db.query("menuPermissoes").collect();
for (const mp of menuPermissoes) {
await ctx.db.delete(mp._id);
}
console.log(`${menuPermissoes.length} menu-permissões removidas`);
// Limpar menu-permissões personalizadas
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`);
// Limpar role-permissões
const rolePermissoes = await ctx.db.query("rolePermissoes").collect();
for (const rp of rolePermissoes) {
await ctx.db.delete(rp._id);
}
console.log(`${rolePermissoes.length} role-permissões removidas`);
// Limpar permissões
const permissoes = await ctx.db.query("permissoes").collect();
for (const permissao of permissoes) {
await ctx.db.delete(permissao._id);
}
console.log(`${permissoes.length} permissões removidas`);
// Limpar roles
const roles = await ctx.db.query("roles").collect();
for (const role of roles) {
await ctx.db.delete(role._id);
}
console.log(`${roles.length} roles removidas`);
console.log("✨ Banco de dados limpo!");
return null;
},
});

View File

@@ -0,0 +1,234 @@
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
// Criar uma nova solicitação de acesso
export const create = mutation({
args: {
nome: v.string(),
matricula: v.string(),
email: v.string(),
telefone: v.string(),
},
returns: v.object({
solicitacaoId: v.id("solicitacoesAcesso"),
}),
handler: async (ctx, args) => {
// Verificar se já existe uma solicitação pendente com a mesma matrícula
const existingByMatricula = await ctx.db
.query("solicitacoesAcesso")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.filter((q) => q.eq(q.field("status"), "pendente"))
.first();
if (existingByMatricula) {
throw new Error("Já existe uma solicitação pendente para esta matrícula.");
}
// Verificar se já existe uma solicitação pendente com o mesmo email
const existingByEmail = await ctx.db
.query("solicitacoesAcesso")
.withIndex("by_email", (q) => q.eq("email", args.email))
.filter((q) => q.eq(q.field("status"), "pendente"))
.first();
if (existingByEmail) {
throw new Error("Já existe uma solicitação pendente para este e-mail.");
}
const solicitacaoId = await ctx.db.insert("solicitacoesAcesso", {
nome: args.nome,
matricula: args.matricula,
email: args.email,
telefone: args.telefone,
status: "pendente",
dataSolicitacao: Date.now(),
});
return { solicitacaoId };
},
});
// Listar todas as solicitações (para o painel administrativo)
export const getAll = query({
args: {},
returns: v.array(
v.object({
_id: v.id("solicitacoesAcesso"),
_creationTime: v.number(),
nome: v.string(),
matricula: v.string(),
email: v.string(),
telefone: v.string(),
status: v.union(
v.literal("pendente"),
v.literal("aprovado"),
v.literal("rejeitado")
),
dataSolicitacao: v.number(),
dataResposta: v.union(v.number(), v.null()),
observacoes: v.union(v.string(), v.null()),
})
),
handler: async (ctx) => {
const solicitacoes = await ctx.db
.query("solicitacoesAcesso")
.order("desc")
.collect();
return solicitacoes.map((s) => ({
_id: s._id,
_creationTime: s._creationTime,
nome: s.nome,
matricula: s.matricula,
email: s.email,
telefone: s.telefone,
status: s.status,
dataSolicitacao: s.dataSolicitacao,
dataResposta: s.dataResposta ?? null,
observacoes: s.observacoes ?? null,
}));
},
});
// Listar apenas solicitações pendentes
export const getPendentes = query({
args: {},
returns: v.array(
v.object({
_id: v.id("solicitacoesAcesso"),
_creationTime: v.number(),
nome: v.string(),
matricula: v.string(),
email: v.string(),
telefone: v.string(),
status: v.union(
v.literal("pendente"),
v.literal("aprovado"),
v.literal("rejeitado")
),
dataSolicitacao: v.number(),
dataResposta: v.union(v.number(), v.null()),
observacoes: v.union(v.string(), v.null()),
})
),
handler: async (ctx) => {
const solicitacoes = await ctx.db
.query("solicitacoesAcesso")
.withIndex("by_status", (q) => q.eq("status", "pendente"))
.order("desc")
.collect();
return solicitacoes.map((s) => ({
_id: s._id,
_creationTime: s._creationTime,
nome: s.nome,
matricula: s.matricula,
email: s.email,
telefone: s.telefone,
status: s.status,
dataSolicitacao: s.dataSolicitacao,
dataResposta: s.dataResposta ?? null,
observacoes: s.observacoes ?? null,
}));
},
});
// Aprovar uma solicitação
export const aprovar = mutation({
args: {
solicitacaoId: v.id("solicitacoesAcesso"),
observacoes: v.optional(v.string()),
},
returns: v.null(),
handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) {
throw new Error("Solicitação não encontrada.");
}
if (solicitacao.status !== "pendente") {
throw new Error("Esta solicitação já foi processada.");
}
await ctx.db.patch(args.solicitacaoId, {
status: "aprovado",
dataResposta: Date.now(),
observacoes: args.observacoes,
});
return null;
},
});
// Rejeitar uma solicitação
export const rejeitar = mutation({
args: {
solicitacaoId: v.id("solicitacoesAcesso"),
observacoes: v.optional(v.string()),
},
returns: v.null(),
handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) {
throw new Error("Solicitação não encontrada.");
}
if (solicitacao.status !== "pendente") {
throw new Error("Esta solicitação já foi processada.");
}
await ctx.db.patch(args.solicitacaoId, {
status: "rejeitado",
dataResposta: Date.now(),
observacoes: args.observacoes,
});
return null;
},
});
// Obter uma solicitação por ID
export const getById = query({
args: {
solicitacaoId: v.id("solicitacoesAcesso"),
},
returns: v.union(
v.object({
_id: v.id("solicitacoesAcesso"),
_creationTime: v.number(),
nome: v.string(),
matricula: v.string(),
email: v.string(),
telefone: v.string(),
status: v.union(
v.literal("pendente"),
v.literal("aprovado"),
v.literal("rejeitado")
),
dataSolicitacao: v.number(),
dataResposta: v.union(v.number(), v.null()),
observacoes: v.union(v.string(), v.null()),
}),
v.null()
),
handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) {
return null;
}
return {
_id: solicitacao._id,
_creationTime: solicitacao._creationTime,
nome: solicitacao.nome,
matricula: solicitacao.matricula,
email: solicitacao.email,
telefone: solicitacao.telefone,
status: solicitacao.status,
dataSolicitacao: solicitacao.dataSolicitacao,
dataResposta: solicitacao.dataResposta ?? null,
observacoes: solicitacao.observacoes ?? null,
};
},
});

View File

@@ -0,0 +1,320 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { hashPassword } from "./auth/utils";
/**
* Criar novo usuário (apenas TI)
*/
export const criar = mutation({
args: {
matricula: v.string(),
nome: v.string(),
email: v.string(),
roleId: v.id("roles"),
funcionarioId: v.optional(v.id("funcionarios")),
senhaInicial: v.string(),
},
returns: v.union(
v.object({ sucesso: v.literal(true), usuarioId: v.id("usuarios") }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
// Verificar se matrícula já existe
const existente = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.first();
if (existente) {
return { sucesso: false as const, erro: "Matrícula já cadastrada" };
}
// Verificar se email já existe
const emailExistente = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", args.email))
.first();
if (emailExistente) {
return { sucesso: false as const, erro: "E-mail já cadastrado" };
}
// Gerar hash da senha inicial
const senhaHash = await hashPassword(args.senhaInicial);
// Criar usuário
const usuarioId = await ctx.db.insert("usuarios", {
matricula: args.matricula,
senhaHash,
nome: args.nome,
email: args.email,
funcionarioId: args.funcionarioId,
roleId: args.roleId,
ativo: true,
primeiroAcesso: true,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
return { sucesso: true as const, usuarioId };
},
});
/**
* Listar todos os usuários com filtros
*/
export const listar = query({
args: {
setor: v.optional(v.string()),
matricula: v.optional(v.string()),
ativo: v.optional(v.boolean()),
},
returns: v.array(
v.object({
_id: v.id("usuarios"),
matricula: v.string(),
nome: v.string(),
email: v.string(),
ativo: v.boolean(),
primeiroAcesso: v.boolean(),
ultimoAcesso: v.optional(v.number()),
criadoEm: v.number(),
role: v.object({
_id: v.id("roles"),
nome: v.string(),
nivel: v.number(),
setor: v.optional(v.string()),
}),
funcionario: v.optional(
v.object({
_id: v.id("funcionarios"),
nome: v.string(),
simboloTipo: v.union(
v.literal("cargo_comissionado"),
v.literal("funcao_gratificada")
),
})
),
})
),
handler: async (ctx, args) => {
let usuarios = await ctx.db.query("usuarios").collect();
// Filtrar por matrícula
if (args.matricula) {
usuarios = usuarios.filter((u) =>
u.matricula.includes(args.matricula!)
);
}
// Filtrar por ativo
if (args.ativo !== undefined) {
usuarios = usuarios.filter((u) => u.ativo === args.ativo);
}
// Buscar roles e funcionários
const resultado = [];
for (const usuario of usuarios) {
const role = await ctx.db.get(usuario.roleId);
if (!role) continue;
// Filtrar por setor
if (args.setor && role.setor !== args.setor) {
continue;
}
let funcionario = undefined;
if (usuario.funcionarioId) {
const func = await ctx.db.get(usuario.funcionarioId);
if (func) {
funcionario = {
_id: func._id,
nome: func.nome,
simboloTipo: func.simboloTipo,
};
}
}
resultado.push({
_id: usuario._id,
matricula: usuario.matricula,
nome: usuario.nome,
email: usuario.email,
ativo: usuario.ativo,
primeiroAcesso: usuario.primeiroAcesso,
ultimoAcesso: usuario.ultimoAcesso,
criadoEm: usuario.criadoEm,
role: {
_id: role._id,
nome: role.nome,
nivel: role.nivel,
setor: role.setor,
},
funcionario,
});
}
return resultado;
},
});
/**
* Ativar/Desativar usuário
*/
export const alterarStatus = mutation({
args: {
usuarioId: v.id("usuarios"),
ativo: v.boolean(),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.usuarioId, {
ativo: args.ativo,
atualizadoEm: Date.now(),
});
// Se desativar, desativar todas as sessões
if (!args.ativo) {
const sessoes = await ctx.db
.query("sessoes")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
.collect();
for (const sessao of sessoes) {
await ctx.db.patch(sessao._id, { ativo: false });
}
}
return null;
},
});
/**
* Resetar senha do usuário
*/
export const resetarSenha = mutation({
args: {
usuarioId: v.id("usuarios"),
novaSenha: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const senhaHash = await hashPassword(args.novaSenha);
await ctx.db.patch(args.usuarioId, {
senhaHash,
primeiroAcesso: true,
atualizadoEm: Date.now(),
});
// Desativar todas as sessões
const sessoes = await ctx.db
.query("sessoes")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
.collect();
for (const sessao of sessoes) {
await ctx.db.patch(sessao._id, { ativo: false });
}
return null;
},
});
/**
* Excluir usuário
*/
export const excluir = mutation({
args: {
usuarioId: v.id("usuarios"),
},
returns: v.null(),
handler: async (ctx, args) => {
// Excluir sessões
const sessoes = await ctx.db
.query("sessoes")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
.collect();
for (const sessao of sessoes) {
await ctx.db.delete(sessao._id);
}
// Excluir usuário
await ctx.db.delete(args.usuarioId);
return null;
},
});
/**
* Ativar usuário
*/
export const ativar = mutation({
args: {
id: v.id("usuarios"),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.id, {
ativo: true,
atualizadoEm: Date.now(),
});
return null;
},
});
/**
* Desativar usuário
*/
export const desativar = mutation({
args: {
id: v.id("usuarios"),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.id, {
ativo: false,
atualizadoEm: Date.now(),
});
// Desativar todas as sessões
const sessoes = await ctx.db
.query("sessoes")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.id))
.collect();
for (const sessao of sessoes) {
await ctx.db.patch(sessao._id, { ativo: false });
}
return null;
},
});
/**
* Alterar role de um usuário
*/
export const alterarRole = mutation({
args: {
usuarioId: v.id("usuarios"),
novaRoleId: v.id("roles"),
},
returns: v.null(),
handler: async (ctx, args) => {
// Verificar se a role existe
const role = await ctx.db.get(args.novaRoleId);
if (!role) {
throw new Error("Role não encontrada");
}
// Atualizar usuário
await ctx.db.patch(args.usuarioId, {
roleId: args.novaRoleId,
atualizadoEm: Date.now(),
});
return null;
},
});