feat: enhance employee and symbol management with new features, improved UI components, and backend schema updates
This commit is contained in:
22
packages/backend/convex/_generated/api.d.ts
vendored
22
packages/backend/convex/_generated/api.d.ts
vendored
@@ -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;
|
||||
|
||||
|
||||
381
packages/backend/convex/autenticacao.ts
Normal file
381
packages/backend/convex/autenticacao.ts
Normal 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 };
|
||||
},
|
||||
});
|
||||
|
||||
132
packages/backend/convex/auth/utils.ts
Normal file
132
packages/backend/convex/auth/utils.ts
Normal 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);
|
||||
}
|
||||
|
||||
178
packages/backend/convex/dashboard.ts
Normal file
178
packages/backend/convex/dashboard.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
|
||||
76
packages/backend/convex/inicializarPermissoes.ts
Normal file
76
packages/backend/convex/inicializarPermissoes.ts
Normal 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" };
|
||||
},
|
||||
});
|
||||
|
||||
227
packages/backend/convex/logsAcesso.ts
Normal file
227
packages/backend/convex/logsAcesso.ts
Normal 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 };
|
||||
},
|
||||
});
|
||||
|
||||
525
packages/backend/convex/menuPermissoes.ts
Normal file
525
packages/backend/convex/menuPermissoes.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
146
packages/backend/convex/monitoramento.ts
Normal file
146
packages/backend/convex/monitoramento.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
44
packages/backend/convex/roles.ts
Normal file
44
packages/backend/convex/roles.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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"]),
|
||||
});
|
||||
|
||||
425
packages/backend/convex/seed.ts
Normal file
425
packages/backend/convex/seed.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
|
||||
234
packages/backend/convex/solicitacoesAcesso.ts
Normal file
234
packages/backend/convex/solicitacoesAcesso.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
320
packages/backend/convex/usuarios.ts
Normal file
320
packages/backend/convex/usuarios.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user