feat: enhance email configuration and validation features

- Implemented mutual exclusivity for SSL and TLS options in the email configuration.
- Added comprehensive validation for required fields, port range, email format, and password requirements.
- Updated the backend to support reversible encryption for SMTP passwords, ensuring secure handling of sensitive data.
- Introduced loading states and improved user feedback in the email configuration UI for better user experience.
This commit is contained in:
2025-11-03 23:51:57 -03:00
parent 3d8f907fa5
commit ce24190b1a
7 changed files with 310 additions and 42 deletions

View File

@@ -1,7 +1,8 @@
import { v } from "convex/values";
import { mutation, query, action } from "./_generated/server";
import { hashPassword } from "./auth/utils";
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)
@@ -62,8 +63,29 @@ export const salvarConfigEmail = mutation({
return { sucesso: false as const, erro: "Email remetente inválido" };
}
// Criptografar senha
const senhaHash = await hashPassword(args.senha);
// 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
@@ -105,10 +127,7 @@ export const salvarConfigEmail = mutation({
});
/**
* Testar conexão SMTP (action - precisa de Node.js)
*
* NOTA: Esta action será implementada quando instalarmos nodemailer.
* Por enquanto, retorna sucesso simulado para não bloquear o desenvolvimento.
* Testar conexão SMTP (mutation que chama action real)
*/
export const testarConexaoSMTP = mutation({
args: {
@@ -119,10 +138,65 @@ export const testarConexaoSMTP = mutation({
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) => {
// Delegar para a action de Node em arquivo separado
// 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" };
}
return { sucesso: true };
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"
};
}
},
});