import { v } from "convex/values"; import { mutation, query } from "./_generated/server"; import { hashPassword, generateToken } from "./auth/utils"; import { registrarAtividade } from "./logsAtividades"; import { Id, Doc } from "./_generated/dataModel"; import { api } from "./_generated/api"; /** * Associar funcionário a um usuário */ export const associarFuncionario = mutation({ args: { usuarioId: v.id("usuarios"), funcionarioId: v.id("funcionarios"), }, returns: v.object({ sucesso: v.boolean() }), handler: async (ctx, args) => { // Verificar se o funcionário existe const funcionario = await ctx.db.get(args.funcionarioId); if (!funcionario) { throw new Error("Funcionário não encontrado"); } // Verificar se o funcionário já está associado a outro usuário const usuarioExistente = await ctx.db .query("usuarios") .withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", args.funcionarioId) ) .first(); if (usuarioExistente && usuarioExistente._id !== args.usuarioId) { throw new Error( `Este funcionário já está associado ao usuário: ${usuarioExistente.nome} (${usuarioExistente.matricula})` ); } // Associar funcionário ao usuário await ctx.db.patch(args.usuarioId, { funcionarioId: args.funcionarioId, }); return { sucesso: true }; }, }); /** * Desassociar funcionário de um usuário */ export const desassociarFuncionario = mutation({ args: { usuarioId: v.id("usuarios"), }, returns: v.object({ sucesso: v.boolean() }), handler: async (ctx, args) => { await ctx.db.patch(args.usuarioId, { funcionarioId: undefined, }); return { sucesso: true }; }, }); /** * 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(), bloqueado: v.optional(v.boolean()), motivoBloqueio: v.optional(v.string()), 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, bloqueado: usuario.bloqueado, motivoBloqueio: usuario.motivoBloqueio, 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; }, }); /** * Atualizar perfil do usuário (foto, avatar, setor, status, preferências) */ export const atualizarPerfil = mutation({ args: { avatar: v.optional(v.string()), fotoPerfil: v.optional(v.id("_storage")), setor: v.optional(v.string()), statusMensagem: v.optional(v.string()), statusPresenca: v.optional( v.union( v.literal("online"), v.literal("offline"), v.literal("ausente"), v.literal("externo"), v.literal("em_reuniao") ) ), notificacoesAtivadas: v.optional(v.boolean()), somNotificacao: v.optional(v.boolean()), }, returns: v.null(), handler: async (ctx, args) => { // TENTAR BETTER AUTH PRIMEIRO const identity = await ctx.auth.getUserIdentity(); let usuarioAtual = null; if (identity && identity.email) { // Buscar por email (Better Auth) usuarioAtual = await ctx.db .query("usuarios") .withIndex("by_email", (q) => q.eq("email", identity.email!)) .first(); } // SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado) if (!usuarioAtual) { const sessaoAtiva = await ctx.db .query("sessoes") .filter((q) => q.eq(q.field("ativo"), true)) .order("desc") .first(); if (sessaoAtiva) { usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); } } if (!usuarioAtual) throw new Error("Usuário não encontrado"); // Validar statusMensagem (max 100 chars) if (args.statusMensagem && args.statusMensagem.length > 100) { throw new Error("Mensagem de status deve ter no máximo 100 caracteres"); } // Atualizar apenas os campos fornecidos const updates: Partial> & { atualizadoEm: number } = { atualizadoEm: Date.now() }; if (args.avatar !== undefined) updates.avatar = args.avatar; if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil; if (args.setor !== undefined) updates.setor = args.setor; if (args.statusMensagem !== undefined) updates.statusMensagem = args.statusMensagem; if (args.statusPresenca !== undefined) { updates.statusPresenca = args.statusPresenca; updates.ultimaAtividade = Date.now(); } if (args.notificacoesAtivadas !== undefined) updates.notificacoesAtivadas = args.notificacoesAtivadas; if (args.somNotificacao !== undefined) updates.somNotificacao = args.somNotificacao; await ctx.db.patch(usuarioAtual._id, updates); return null; }, }); /** * Obter perfil do usuário atual */ export const obterPerfil = query({ args: {}, returns: v.union( v.object({ _id: v.id("usuarios"), nome: v.string(), email: v.string(), matricula: v.string(), funcionarioId: v.optional(v.id("funcionarios")), avatar: v.optional(v.string()), fotoPerfil: v.optional(v.id("_storage")), fotoPerfilUrl: v.union(v.string(), v.null()), setor: v.optional(v.string()), statusMensagem: v.optional(v.string()), statusPresenca: v.optional( v.union( v.literal("online"), v.literal("offline"), v.literal("ausente"), v.literal("externo"), v.literal("em_reuniao") ) ), notificacoesAtivadas: v.boolean(), somNotificacao: v.boolean(), }), v.null() ), handler: async (ctx) => { console.log("=== DEBUG obterPerfil ==="); // TENTAR BETTER AUTH PRIMEIRO const identity = await ctx.auth.getUserIdentity(); console.log("Identity:", identity ? "encontrado" : "null"); let usuarioAtual = null; if (identity && identity.email) { console.log("Tentando buscar por email:", identity.email); // Buscar por email (Better Auth) usuarioAtual = await ctx.db .query("usuarios") .withIndex("by_email", (q) => q.eq("email", identity.email!)) .first(); console.log( "Usuário encontrado por email:", usuarioAtual ? "SIM" : "NÃO" ); } // SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado) if (!usuarioAtual) { console.log("Buscando por sessão ativa..."); const sessaoAtiva = await ctx.db .query("sessoes") .filter((q) => q.eq(q.field("ativo"), true)) .order("desc") .first(); console.log("Sessão ativa encontrada:", sessaoAtiva ? "SIM" : "NÃO"); if (sessaoAtiva) { usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); console.log( "Usuário da sessão encontrado:", usuarioAtual ? "SIM" : "NÃO" ); } } if (!usuarioAtual) { console.log("❌ Nenhum usuário encontrado"); // Listar todos os usuários para debug const todosUsuarios = await ctx.db.query("usuarios").collect(); console.log("Total de usuários no banco:", todosUsuarios.length); console.log( "Emails cadastrados:", todosUsuarios.map((u) => u.email) ); return null; } console.log("✅ Usuário encontrado:", usuarioAtual.nome); // Buscar fotoPerfil URL se existir let fotoPerfilUrl = null; if (usuarioAtual.fotoPerfil) { fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil); } return { _id: usuarioAtual._id, nome: usuarioAtual.nome, email: usuarioAtual.email, matricula: usuarioAtual.matricula, funcionarioId: usuarioAtual.funcionarioId, avatar: usuarioAtual.avatar, fotoPerfil: usuarioAtual.fotoPerfil, fotoPerfilUrl, setor: usuarioAtual.setor, statusMensagem: usuarioAtual.statusMensagem, statusPresenca: usuarioAtual.statusPresenca, notificacoesAtivadas: usuarioAtual.notificacoesAtivadas ?? true, somNotificacao: usuarioAtual.somNotificacao ?? true, }; }, }); /** * Listar todos usuários para o chat (com avatar, foto e status) */ export const listarParaChat = query({ args: {}, returns: v.array( v.object({ _id: v.id("usuarios"), nome: v.string(), email: v.string(), matricula: v.optional(v.string()), avatar: v.optional(v.string()), fotoPerfil: v.optional(v.id("_storage")), fotoPerfilUrl: v.union(v.string(), v.null()), statusPresenca: v.optional( v.union( v.literal("online"), v.literal("offline"), v.literal("ausente"), v.literal("externo"), v.literal("em_reuniao") ) ), statusMensagem: v.optional(v.string()), ultimaAtividade: v.optional(v.number()), }) ), handler: async (ctx) => { // Buscar todos os usuários ativos const usuarios = await ctx.db .query("usuarios") .filter((q) => q.eq(q.field("ativo"), true)) .collect(); // Buscar foto de perfil URL para cada usuário const usuariosComFoto = await Promise.all( usuarios.map(async (usuario) => { let fotoPerfilUrl = null; if (usuario.fotoPerfil) { fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil); } return { _id: usuario._id, nome: usuario.nome, email: usuario.email, matricula: usuario.matricula || undefined, avatar: usuario.avatar, fotoPerfil: usuario.fotoPerfil, fotoPerfilUrl, statusPresenca: usuario.statusPresenca || "offline", statusMensagem: usuario.statusMensagem, ultimaAtividade: usuario.ultimaAtividade, }; }) ); return usuariosComFoto; }, }); /** * Gera URL para upload de foto de perfil */ export const uploadFotoPerfil = mutation({ args: {}, returns: v.string(), handler: async (ctx) => { // TENTAR BETTER AUTH PRIMEIRO const identity = await ctx.auth.getUserIdentity(); let usuarioAtual = null; if (identity && identity.email) { // Buscar por email (Better Auth) usuarioAtual = await ctx.db .query("usuarios") .withIndex("by_email", (q) => q.eq("email", identity.email!)) .first(); } // SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado) if (!usuarioAtual) { const sessaoAtiva = await ctx.db .query("sessoes") .filter((q) => q.eq(q.field("ativo"), true)) .order("desc") .first(); if (sessaoAtiva) { usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId); } } if (!usuarioAtual) throw new Error("Usuário não autenticado"); return await ctx.storage.generateUploadUrl(); }, }); // ==================== GESTÃO AVANÇADA DE USUÁRIOS (TI_MASTER) ==================== /** * Bloquear usuário (apenas TI_MASTER) */ export const bloquearUsuario = mutation({ args: { usuarioId: v.id("usuarios"), motivo: v.string(), bloqueadoPorId: v.id("usuarios"), }, returns: v.union( v.object({ sucesso: v.literal(true) }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { const usuario = await ctx.db.get(args.usuarioId); if (!usuario) { return { sucesso: false as const, erro: "Usuário não encontrado" }; } // Atualizar usuário como bloqueado await ctx.db.patch(args.usuarioId, { bloqueado: true, motivoBloqueio: args.motivo, dataBloqueio: Date.now(), atualizadoEm: Date.now(), }); // Registrar no histórico de bloqueios await ctx.db.insert("bloqueiosUsuarios", { usuarioId: args.usuarioId, motivo: args.motivo, bloqueadoPor: args.bloqueadoPorId, dataInicio: Date.now(), ativo: true, }); // Desativar todas as sessões ativas do usuário const sessoes = await ctx.db .query("sessoes") .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) .filter((q) => q.eq(q.field("ativo"), true)) .collect(); for (const sessao of sessoes) { await ctx.db.patch(sessao._id, { ativo: false }); } // Log de atividade await registrarAtividade( ctx, args.bloqueadoPorId, "bloquear", "usuarios", JSON.stringify({ usuarioId: args.usuarioId, motivo: args.motivo }), args.usuarioId ); return { sucesso: true as const }; }, }); /** * Desbloquear usuário (apenas TI_MASTER) */ export const desbloquearUsuario = mutation({ args: { usuarioId: v.id("usuarios"), desbloqueadoPorId: v.id("usuarios"), }, returns: v.union( v.object({ sucesso: v.literal(true) }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { const usuario = await ctx.db.get(args.usuarioId); if (!usuario) { return { sucesso: false as const, erro: "Usuário não encontrado" }; } // Atualizar usuário como desbloqueado await ctx.db.patch(args.usuarioId, { bloqueado: false, motivoBloqueio: undefined, dataBloqueio: undefined, tentativasLogin: 0, ultimaTentativaLogin: undefined, atualizadoEm: Date.now(), }); // Fechar bloqueios ativos const bloqueiosAtivos = await ctx.db .query("bloqueiosUsuarios") .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) .filter((q) => q.eq(q.field("ativo"), true)) .collect(); for (const bloqueio of bloqueiosAtivos) { await ctx.db.patch(bloqueio._id, { ativo: false, dataFim: Date.now(), desbloqueadoPor: args.desbloqueadoPorId, }); } // Log de atividade await registrarAtividade( ctx, args.desbloqueadoPorId, "desbloquear", "usuarios", JSON.stringify({ usuarioId: args.usuarioId }), args.usuarioId ); return { sucesso: true as const }; }, }); /** * Resetar senha de usuário (apenas TI_MASTER) */ export const resetarSenhaUsuario = mutation({ args: { usuarioId: v.id("usuarios"), resetadoPorId: v.id("usuarios"), novaSenhaTemporaria: v.optional(v.string()), // Se não fornecer, gera automática }, returns: v.union( v.object({ sucesso: v.literal(true), senhaTemporaria: v.string() }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { const usuario = await ctx.db.get(args.usuarioId); if (!usuario) { return { sucesso: false as const, erro: "Usuário não encontrado" }; } // Gerar senha temporária se não foi fornecida const senhaTemporaria = args.novaSenhaTemporaria || gerarSenhaTemporaria(); const senhaHash = await hashPassword(senhaTemporaria); // Atualizar usuário await ctx.db.patch(args.usuarioId, { senhaHash, primeiroAcesso: true, // Força mudança de senha no próximo login tentativasLogin: 0, ultimaTentativaLogin: undefined, atualizadoEm: Date.now(), }); // Log de atividade await registrarAtividade( ctx, args.resetadoPorId, "resetar_senha", "usuarios", JSON.stringify({ usuarioId: args.usuarioId }), args.usuarioId ); return { sucesso: true as const, senhaTemporaria }; }, }); // Helper para gerar senha temporária function gerarSenhaTemporaria(): string { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%"; let senha = ""; for (let i = 0; i < 12; i++) { senha += chars.charAt(Math.floor(Math.random() * chars.length)); } return senha; } /** * Editar dados de usuário (apenas TI_MASTER) */ export const editarUsuario = mutation({ args: { usuarioId: v.id("usuarios"), nome: v.optional(v.string()), email: v.optional(v.string()), roleId: v.optional(v.id("roles")), setor: v.optional(v.string()), editadoPorId: v.id("usuarios"), }, returns: v.union( v.object({ sucesso: v.literal(true) }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { const usuario = await ctx.db.get(args.usuarioId); if (!usuario) { return { sucesso: false as const, erro: "Usuário não encontrado" }; } // Verificar se email já existe (se estiver mudando) if (args.email && args.email !== usuario.email) { 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" }; } } // Atualizar campos fornecidos const updates: Partial> & { atualizadoEm: number } = { atualizadoEm: Date.now(), }; if (args.nome !== undefined) updates.nome = args.nome; if (args.email !== undefined) updates.email = args.email; if (args.roleId !== undefined) updates.roleId = args.roleId; if (args.setor !== undefined) updates.setor = args.setor; await ctx.db.patch(args.usuarioId, updates); // Log de atividade await registrarAtividade( ctx, args.editadoPorId, "editar", "usuarios", JSON.stringify(updates), args.usuarioId ); return { sucesso: true as const }; }, }); /** * Criar/Promover usuário Admin Master (TI_MASTER - nível 0) */ export const criarAdminMaster = mutation({ args: { matricula: v.string(), nome: v.string(), email: v.string(), senha: v.optional(v.string()), }, returns: v.union( v.object({ sucesso: v.literal(true), usuarioId: v.id("usuarios"), senhaTemporaria: v.string(), }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { // Garantir que a role TI_MASTER exista (nível 0) let roleTIMaster = await ctx.db .query("roles") .withIndex("by_nome", (q) => q.eq("nome", "ti_master")) .first(); if (!roleTIMaster) { const roleId = await ctx.db.insert("roles", { nome: "ti_master", descricao: "TI Master", nivel: 0, setor: "ti", customizado: false, editavel: false, }); roleTIMaster = await ctx.db.get(roleId); } if (!roleTIMaster) { return { sucesso: false as const, erro: "Falha ao garantir role TI Master", }; } // Se já existir usuário por matrícula, promove/atualiza const existentePorMatricula = await ctx.db .query("usuarios") .withIndex("by_matricula", (q) => q.eq("matricula", args.matricula)) .first(); const senhaTemporaria = args.senha || gerarSenhaTemporaria(); const senhaHash = await hashPassword(senhaTemporaria); if (existentePorMatricula) { await ctx.db.patch(existentePorMatricula._id, { nome: args.nome, email: args.email, senhaHash, roleId: roleTIMaster._id, ativo: true, primeiroAcesso: true, atualizadoEm: Date.now(), }); return { sucesso: true as const, usuarioId: existentePorMatricula._id, senhaTemporaria, }; } // Verificar se email já existe const existentePorEmail = await ctx.db .query("usuarios") .withIndex("by_email", (q) => q.eq("email", args.email)) .first(); if (existentePorEmail) { // Promove usuário existente por email await ctx.db.patch(existentePorEmail._id, { matricula: args.matricula, nome: args.nome, senhaHash, roleId: roleTIMaster._id, ativo: true, primeiroAcesso: true, atualizadoEm: Date.now(), }); return { sucesso: true as const, usuarioId: existentePorEmail._id, senhaTemporaria, }; } // Criar novo usuário TI Master const usuarioId = await ctx.db.insert("usuarios", { matricula: args.matricula, senhaHash, nome: args.nome, email: args.email, roleId: roleTIMaster._id, ativo: true, primeiroAcesso: true, criadoEm: Date.now(), atualizadoEm: Date.now(), }); return { sucesso: true as const, usuarioId, senhaTemporaria }; }, }); /** * Desativar usuário logicamente (soft delete - apenas TI_MASTER) */ export const excluirUsuarioLogico = mutation({ args: { usuarioId: v.id("usuarios"), excluidoPorId: v.id("usuarios"), }, returns: v.union( v.object({ sucesso: v.literal(true) }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { const usuario = await ctx.db.get(args.usuarioId); if (!usuario) { return { sucesso: false as const, erro: "Usuário não encontrado" }; } // Marcar como inativo await ctx.db.patch(args.usuarioId, { 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.usuarioId)) .filter((q) => q.eq(q.field("ativo"), true)) .collect(); for (const sessao of sessoes) { await ctx.db.patch(sessao._id, { ativo: false }); } // Log de atividade await registrarAtividade( ctx, args.excluidoPorId, "excluir", "usuarios", JSON.stringify({ usuarioId: args.usuarioId }), args.usuarioId ); return { sucesso: true as const }; }, }); /** * Criar usuário completo com permissões (TI_MASTER) */ export const criarUsuarioCompleto = mutation({ args: { matricula: v.string(), nome: v.string(), email: v.string(), roleId: v.id("roles"), setor: v.optional(v.string()), senhaInicial: v.optional(v.string()), criadoPorId: v.id("usuarios"), enviarEmailBoasVindas: v.optional(v.boolean()), }, returns: v.union( v.object({ sucesso: v.literal(true), usuarioId: v.id("usuarios"), senhaTemporaria: v.string(), }), v.object({ sucesso: v.literal(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 senha inicial se não fornecida const senhaTemporaria = args.senhaInicial || gerarSenhaTemporaria(); const senhaHash = await hashPassword(senhaTemporaria); // Criar usuário const usuarioId = await ctx.db.insert("usuarios", { matricula: args.matricula, senhaHash, nome: args.nome, email: args.email, roleId: args.roleId, setor: args.setor, ativo: true, primeiroAcesso: true, criadoEm: Date.now(), atualizadoEm: Date.now(), }); // Log de atividade await registrarAtividade( ctx, args.criadoPorId, "criar", "usuarios", JSON.stringify({ usuarioId, matricula: args.matricula, nome: args.nome }), usuarioId ); // TODO: Se enviarEmailBoasVindas = true, enfileirar email // Isso será implementado quando criarmos o sistema de emails return { sucesso: true as const, usuarioId, senhaTemporaria }; }, }); /** * Criar (ou garantir) um usuário ADMIN padrão */ export const criarAdminPadrao = mutation({ args: { matricula: v.optional(v.string()), nome: v.optional(v.string()), email: v.optional(v.string()), senha: v.optional(v.string()), }, returns: v.object({ sucesso: v.boolean(), usuarioId: v.optional(v.id("usuarios")), }), handler: async (ctx, args) => { const matricula = args.matricula ?? "0000"; const nome = args.nome ?? "Administrador Geral"; const email = args.email ?? "admin@sgse.pe.gov.br"; const senha = args.senha ?? "Admin@123"; // Garantir role ADMIN (nível 2) let roleAdmin = await ctx.db .query("roles") .withIndex("by_nome", (q) => q.eq("nome", "admin")) .first(); if (!roleAdmin) { const roleId = await ctx.db.insert("roles", { nome: "admin", descricao: "Administrador Geral", nivel: 2, setor: "administrativo", customizado: false, editavel: true, }); roleAdmin = await ctx.db.get(roleId); } if (!roleAdmin) return { sucesso: false }; // Verificar se já existe por matrícula ou email const existentePorMatricula = await ctx.db .query("usuarios") .withIndex("by_matricula", (q) => q.eq("matricula", matricula)) .first(); const existentePorEmail = await ctx.db .query("usuarios") .withIndex("by_email", (q) => q.eq("email", email)) .first(); const senhaHash = await hashPassword(senha); if (existentePorMatricula || existentePorEmail) { const alvo = existentePorMatricula ?? existentePorEmail!; await ctx.db.patch(alvo._id, { matricula, nome, email, senhaHash, roleId: roleAdmin._id, ativo: true, primeiroAcesso: false, atualizadoEm: Date.now(), }); return { sucesso: true, usuarioId: alvo._id }; } const usuarioId = await ctx.db.insert("usuarios", { matricula, senhaHash, nome, email, roleId: roleAdmin._id, ativo: true, primeiroAcesso: false, criadoEm: Date.now(), atualizadoEm: Date.now(), }); return { sucesso: true, usuarioId }; }, });