import { v } from 'convex/values'; import { mutation, query, action, internalMutation } from './_generated/server'; import { encryptSMTPPassword } from './auth/utils'; import { registrarAtividade } from './logsAtividades'; import { api, internal } from './_generated/api'; /** * Obter configuração de email ativa (senha mascarada) */ export const obterConfigEmail = query({ args: {}, handler: async (ctx) => { const config = await ctx.db .query('configuracaoEmail') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .first(); if (!config) { return null; } // Retornar config com senha mascarada return { _id: config._id, servidor: config.servidor, porta: config.porta, usuario: config.usuario, senhaHash: '********', // Mascarar senha emailRemetente: config.emailRemetente, nomeRemetente: config.nomeRemetente, usarSSL: config.usarSSL, usarTLS: config.usarTLS, ativo: config.ativo, testadoEm: config.testadoEm, atualizadoEm: config.atualizadoEm }; } }); /** * Salvar configuração de email (apenas TI_MASTER) */ export const salvarConfigEmail = mutation({ args: { servidor: v.string(), porta: v.number(), usuario: v.string(), senha: v.string(), emailRemetente: v.string(), nomeRemetente: v.string(), usarSSL: v.boolean(), usarTLS: v.boolean(), configuradoPorId: v.id('usuarios') }, returns: v.union( v.object({ sucesso: v.literal(true), configId: v.id('configuracaoEmail') }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { // Validar email const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(args.emailRemetente)) { return { sucesso: false as const, erro: 'Email remetente inválido' }; } // Validar porta if (args.porta < 1 || args.porta > 65535) { return { sucesso: false as const, erro: 'Porta deve ser um número entre 1 e 65535' }; } // Buscar config ativa anterior para manter senha se não fornecida const configAtiva = await ctx.db .query('configuracaoEmail') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .first(); // Determinar senhaHash: usar nova senha se fornecida, senão manter a atual let senhaHash: string; if (args.senha && args.senha.trim().length > 0) { // Nova senha fornecida, criptografar usando criptografia reversível (AES) senhaHash = await encryptSMTPPassword(args.senha); } else if (configAtiva) { // Senha não fornecida, manter a atual (já criptografada) senhaHash = configAtiva.senhaHash; } else { // Sem senha e sem config existente - erro return { sucesso: false as const, erro: 'Senha é obrigatória para nova configuração' }; } // Desativar config anterior const configsAntigas = await ctx.db .query('configuracaoEmail') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .collect(); for (const config of configsAntigas) { await ctx.db.patch(config._id, { ativo: false }); } // Criar nova config const configId = await ctx.db.insert('configuracaoEmail', { servidor: args.servidor, porta: args.porta, usuario: args.usuario, senhaHash, emailRemetente: args.emailRemetente, nomeRemetente: args.nomeRemetente, usarSSL: args.usarSSL, usarTLS: args.usarTLS, ativo: true, configuradoPor: args.configuradoPorId, atualizadoEm: Date.now() }); // Log de atividade await registrarAtividade( ctx, args.configuradoPorId, 'configurar', 'email', JSON.stringify({ servidor: args.servidor, porta: args.porta }), configId ); return { sucesso: true as const, configId }; } }); /** * Mutation interna para atualizar testadoEm */ export const atualizarTestadoEm = internalMutation({ args: { configId: v.id('configuracaoEmail') }, returns: v.null(), handler: async (ctx, args) => { await ctx.db.patch(args.configId, { testadoEm: Date.now() }); return null; } }); /** * Testar conexão SMTP (action que chama action real) */ export const testarConexaoSMTP = action({ args: { servidor: v.string(), porta: v.number(), usuario: v.string(), senha: v.string(), usarSSL: v.boolean(), usarTLS: v.boolean() }, returns: v.union( v.object({ sucesso: v.literal(true) }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args): Promise<{ sucesso: true } | { sucesso: false; erro: string }> => { // Validações básicas if (!args.servidor || args.servidor.trim().length === 0) { return { sucesso: false as const, erro: 'Servidor SMTP não pode estar vazio' }; } if (!args.porta || args.porta < 1 || args.porta > 65535) { return { sucesso: false as const, erro: 'Porta inválida. Deve ser entre 1 e 65535' }; } if (!args.usuario || args.usuario.trim().length === 0) { return { sucesso: false as const, erro: 'Usuário não pode estar vazio' }; } if (!args.senha || args.senha.trim().length === 0) { return { sucesso: false as const, erro: 'Senha não pode estar vazia' }; } // Validação de SSL/TLS mutuamente exclusivos if (args.usarSSL && args.usarTLS) { return { sucesso: false as const, erro: 'SSL e TLS não podem estar habilitados simultaneamente' }; } // Chamar action de teste real (que usa nodemailer) try { const resultado: { sucesso: true } | { sucesso: false; erro: string } = await ctx.runAction( api.actions.smtp.testarConexao, { servidor: args.servidor, porta: args.porta, usuario: args.usuario, senha: args.senha, usarSSL: args.usarSSL, usarTLS: args.usarTLS } ); // Se o teste foi bem-sucedido e há uma config ativa, atualizar testadoEm if (resultado.sucesso) { const configAtiva = await ctx.runQuery(api.configuracaoEmail.obterConfigEmail, {}); if (configAtiva) { await ctx.runMutation(internal.configuracaoEmail.atualizarTestadoEm, { configId: configAtiva._id }); } } return resultado; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); return { sucesso: false as const, erro: errorMessage || 'Erro ao conectar com o servidor SMTP' }; } } }); /** * Marcar que a configuração foi testada com sucesso */ export const marcarConfigTestada = mutation({ args: { configId: v.id('configuracaoEmail') }, handler: async (ctx, args) => { await ctx.db.patch(args.configId, { testadoEm: Date.now() }); } });