import { v } from "convex/values"; import { mutation, query } from "./_generated/server"; import { hashPassword } from "./auth/utils"; import { registrarAtividade } from "./logsAtividades"; import { Id, Doc } from "./_generated/dataModel"; import type { QueryCtx, MutationCtx } from "./_generated/server"; import { createAuthUser, getCurrentUserFunction } from "./auth"; /** * Helper para obter a matrícula do usuário (do funcionário se houver) */ async function obterMatriculaUsuario( ctx: QueryCtx, usuario: Doc<"usuarios"> ): Promise { if (usuario.funcionarioId) { const funcionario = await ctx.db.get(usuario.funcionarioId); return funcionario?.matricula; } return undefined; } /** * 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) { const matricula = await obterMatriculaUsuario(ctx, usuarioExistente); throw new Error( `Este funcionário já está associado ao usuário: ${ usuarioExistente.nome }${matricula ? ` (${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: { 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 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" }; } const senhaTemporaria = args.senhaInicial; const authUserId = await createAuthUser(ctx, { nome: args.nome, email: args.email, password: senhaTemporaria, }); // Criar usuário const usuarioId = await ctx.db.insert("usuarios", { authId: authUserId, 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()), }, handler: async (ctx, args) => { let usuarios = await ctx.db.query("usuarios").collect(); // Filtrar por matrícula (buscar no funcionário) if (args.matricula) { const usuariosComMatricula = await Promise.all( usuarios.map(async (u) => { const matricula = await obterMatriculaUsuario(ctx, u); return { usuario: u, matricula }; }) ); usuarios = usuariosComMatricula .filter(({ matricula }) => matricula?.includes(args.matricula!)) .map(({ usuario }) => usuario); } // Filtrar por ativo if (args.ativo !== undefined) { usuarios = usuarios.filter((u) => u.ativo === args.ativo); } // Buscar roles e funcionários const resultado = []; const usuariosSemRole: Array<{ nome: string; matricula: string; roleId: Id<"roles">; }> = []; for (const usuario of usuarios) { try { const role = await ctx.db.get(usuario.roleId); // Se a role não existe, criar uma role de erro mas ainda incluir o usuário if (!role) { const matricula = await obterMatriculaUsuario(ctx, usuario); usuariosSemRole.push({ nome: usuario.nome, matricula: matricula || "N/A", roleId: usuario.roleId, }); // Filtrar por setor - se filtro está ativo e role não existe, pular if (args.setor) { continue; } // Incluir usuário com role de erro let funcionario = undefined; if (usuario.funcionarioId) { try { const func = await ctx.db.get(usuario.funcionarioId); if (func) { funcionario = { _id: func._id, nome: func.nome, matricula: func.matricula, descricaoCargo: func.descricaoCargo, simboloTipo: func.simboloTipo, }; } } catch (error) { console.error( `Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`, error ); } } const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario); // Criar role de erro (sem _creationTime pois a role não existe) resultado.push({ _id: usuario._id, matricula: matriculaUsuario, 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: usuario.roleId, descricao: "Perfil não encontrado" as const, nome: "erro_role_ausente" as const, nivel: 999 as const, erro: true as const, }, funcionario, avisos: [ { tipo: "erro" as const, mensagem: `Perfil de acesso (ID: ${usuario.roleId}) não encontrado. Este usuário precisa ter seu perfil reatribuído.`, }, ], }); continue; } // Filtrar por setor if (args.setor && role.setor !== args.setor) { continue; } // Buscar funcionário associado let funcionario = undefined; if (usuario.funcionarioId) { try { const func = await ctx.db.get(usuario.funcionarioId); if (func) { funcionario = { _id: func._id, nome: func.nome, matricula: func.matricula, descricaoCargo: func.descricaoCargo, simboloTipo: func.simboloTipo, }; } } catch (error) { console.error( `Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`, error ); } } // Construir objeto role - incluir _creationTime se existir (campo automático do Convex) const roleObj = { _id: role._id, descricao: role.descricao, nome: role.nome, nivel: role.nivel, ...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }), ...(role.customizado !== undefined && { customizado: role.customizado, }), ...(role.editavel !== undefined && { editavel: role.editavel }), ...(role.setor !== undefined && { setor: role.setor }), }; const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario); resultado.push({ _id: usuario._id, matricula: matriculaUsuario, 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: roleObj, funcionario, }); } catch (error) { console.error(`Erro ao processar usuário ${usuario._id}:`, error); // Continua processando outros usuários mesmo se houver erro em um } } // Log de usuários sem role para depuração if (usuariosSemRole.length > 0) { console.warn( `⚠️ Encontrados ${usuariosSemRole.length} usuário(s) com perfil ausente:`, usuariosSemRole.map( (u) => `${u.nome}${ u.matricula !== "N/A" ? ` (${u.matricula})` : "" } - RoleID: ${u.roleId}` ) ); } 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(); } 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.optional(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) => { 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(); } if (!usuarioAtual) { 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); } const matricula = await obterMatriculaUsuario(ctx, usuarioAtual); return { _id: usuarioAtual._id, nome: usuarioAtual.nome, email: usuarioAtual.email, matricula: matricula || undefined, 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) => { // Obter usuário autenticado usando função helper compartilhada const usuarioAtual = await getCurrentUserFunction(ctx); if (!usuarioAtual) { return []; } // Buscar todos os usuários ativos const usuarios = await ctx.db .query("usuarios") .filter((q) => q.eq(q.field("ativo"), true)) .collect(); // Filtrar o usuário atual da lista apenas se conseguimos identificá-lo com certeza // Se não conseguimos identificar (usuarioAtual é null), retornar todos // O frontend fará um filtro adicional usando obterPerfil como camada de segurança const usuariosFiltrados = usuarioAtual ? usuarios.filter((u) => u._id !== usuarioAtual._id) : usuarios; // Buscar foto de perfil URL para cada usuário const usuariosComFoto = await Promise.all( usuariosFiltrados.map(async (usuario) => { let fotoPerfilUrl = null; if (usuario.fotoPerfil) { fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil); } const matricula = await obterMatriculaUsuario(ctx, usuario); return { _id: usuario._id, nome: usuario.nome, email: usuario.email, matricula: 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 usuarioAtual = await getCurrentUserFunction(ctx); if (!usuarioAtual) { return { sucesso: false as const, erro: "Usuário não autenticado" }; } 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 usuarioAtual = await getCurrentUserFunction(ctx); if (!usuarioAtual) { return { sucesso: false as const, erro: "Usuário não autenticado" }; } 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 usuarioAtual = await getCurrentUserFunction(ctx); if (!usuarioAtual) { return { sucesso: false as const, erro: "Usuário não autenticado" }; } 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: { 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", }; } const senhaTemporaria = args.senha || gerarSenhaTemporaria(); const authUserId = await createAuthUser(ctx, { nome: args.nome, email: args.email, password: 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, { nome: args.nome, roleId: roleTIMaster._id, ativo: true, primeiroAcesso: true, atualizadoEm: Date.now(), authId: authUserId, }); return { sucesso: true as const, usuarioId: existentePorEmail._id, senhaTemporaria, }; } // Criar novo usuário TI Master const usuarioId = await ctx.db.insert("usuarios", { authId: authUserId, 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 }; }, });