import { v } from 'convex/values'; import { mutation, query } from './_generated/server'; import { registrarAtividade } from './logsAtividades'; import { Id, Doc } from './_generated/dataModel'; import type { QueryCtx } 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, setor, status, preferências) */ export const atualizarPerfil = mutation({ args: { fotoPerfil: v.optional(v.id('_storage')), avatar: v.optional(v.string()), // URL do avatar gerado (ex: DiceBear) 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()), temaPreferido: v.optional(v.string()) }, returns: v.null(), handler: async (ctx, args) => { const usuarioAtual = await getCurrentUserFunction(ctx); 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.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil; if (args.avatar !== undefined) updates.avatar = args.avatar; 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; if (args.temaPreferido !== undefined) updates.temaPreferido = args.temaPreferido; await ctx.db.patch(usuarioAtual._id, updates); return null; } }); /** * Atualizar tema preferido do usuário */ export const atualizarTema = mutation({ args: { temaPreferido: v.string() }, returns: v.object({ sucesso: v.boolean() }), handler: async (ctx, args) => { const usuarioAtual = await getCurrentUserFunction(ctx); if (!usuarioAtual) { throw new Error('Usuário não encontrado'); } await ctx.db.patch(usuarioAtual._id, { temaPreferido: args.temaPreferido, atualizadoEm: Date.now() }); return { sucesso: true }; } }); /** * 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')), fotoPerfil: v.optional(v.id('_storage')), fotoPerfilUrl: v.union(v.string(), v.null()), avatar: v.optional(v.string()), // URL do avatar gerado (ex: DiceBear) 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 usuarioAutenticado = await getCurrentUserFunction(ctx); if (!usuarioAutenticado) { return null; } const usuarioAtual = usuarioAutenticado; // 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, fotoPerfil: usuarioAtual.fotoPerfil, fotoPerfilUrl, avatar: usuarioAtual.avatar, 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 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()), fotoPerfil: v.optional(v.id('_storage')), fotoPerfilUrl: v.union(v.string(), v.null()), avatar: v.optional(v.string()), // URL do avatar gerado (ex: DiceBear) 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, 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) => { const usuarioAtual = await getCurrentUserFunction(ctx); 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' }; } // Verificar permissão (apenas TI_MASTER) const resetadoPor = await ctx.db.get(args.resetadoPorId); if (!resetadoPor) { return { sucesso: false as const, erro: 'Usuário que está resetando não encontrado' }; } // Buscar a role do usuário if (!resetadoPor.roleId) { return { sucesso: false as const, erro: 'Usuário não possui role definida' }; } const role = await ctx.db.get(resetadoPor.roleId); if (!role) { return { sucesso: false as const, erro: 'Role do usuário não encontrada' }; } // Permitir TI_MASTER, TI_USUARIO e ADMIN const rolesPermitidas = ['ti_master', 'ti_usuario', 'admin']; if (!rolesPermitidas.includes(role.nome)) { return { sucesso: false as const, erro: 'Apenas usuários de TI ou administradores podem resetar senhas' }; } // Gerar senha temporária se não foi fornecida const senhaTemporaria = args.novaSenhaTemporaria || gerarSenhaTemporaria(); try { // Fazer hash da senha const { hashPassword } = await import('./auth/utils'); 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() }); // Desativar todas as sessões ativas 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 }); } // Enviar email com a nova senha usando template try { await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, { destinatario: usuario.email, destinatarioId: args.usuarioId, templateCodigo: 'SENHA_RESETADA', variaveis: { senha: senhaTemporaria }, enviadoPor: args.resetadoPorId }); } catch (emailError) { console.error('Erro ao agendar envio de email:', emailError); // Não falhar a mutation se o email falhar, apenas logar o erro } // Log de atividade await registrarAtividade( ctx, args.resetadoPorId, 'resetar_senha', 'usuarios', JSON.stringify({ usuarioId: args.usuarioId }), args.usuarioId ); return { sucesso: true as const, senhaTemporaria }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { sucesso: false as const, erro: `Erro ao resetar senha: ${errorMessage}` }; } } }); // 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 }; } });