import { v } from "convex/values"; import { mutation, query } from "./_generated/server"; import { encryptSMTPPassword } from "./auth/utils"; import { registrarAtividade } from "./logsAtividades"; import { api } 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 }; }, }); /** * Testar conexão SMTP (mutation que chama action real) */ export const testarConexaoSMTP = mutation({ 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) => { // 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 = await ctx.scheduler.runAfter(0, 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.db .query("configuracaoEmail") .withIndex("by_ativo", (q) => q.eq("ativo", true)) .first(); if (configAtiva) { await ctx.db.patch(configAtiva._id, { testadoEm: Date.now(), }); } } return resultado; } catch (error: any) { return { sucesso: false as const, erro: error.message || "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(), }); }, });