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:
@@ -39,12 +39,56 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tornar SSL e TLS mutuamente exclusivos
|
||||||
|
$effect(() => {
|
||||||
|
if (usarSSL && usarTLS) {
|
||||||
|
// Se ambos estão marcados, priorizar TLS por padrão
|
||||||
|
usarSSL = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleSSL() {
|
||||||
|
usarSSL = !usarSSL;
|
||||||
|
if (usarSSL) {
|
||||||
|
usarTLS = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTLS() {
|
||||||
|
usarTLS = !usarTLS;
|
||||||
|
if (usarTLS) {
|
||||||
|
usarSSL = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function salvarConfiguracao() {
|
async function salvarConfiguracao() {
|
||||||
if (!servidor || !porta || !usuario || !senha || !emailRemetente) {
|
// Validação de campos obrigatórios
|
||||||
|
if (!servidor?.trim() || !porta || !usuario?.trim() || !emailRemetente?.trim() || !nomeRemetente?.trim()) {
|
||||||
mostrarMensagem("error", "Preencha todos os campos obrigatórios");
|
mostrarMensagem("error", "Preencha todos os campos obrigatórios");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validação de porta (1-65535)
|
||||||
|
const portaNum = Number(porta);
|
||||||
|
if (isNaN(portaNum) || portaNum < 1 || portaNum > 65535) {
|
||||||
|
mostrarMensagem("error", "Porta deve ser um número entre 1 e 65535");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validação de formato de email
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(emailRemetente.trim())) {
|
||||||
|
mostrarMensagem("error", "Email remetente inválido");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validação de senha: obrigatória apenas se não houver configuração existente
|
||||||
|
const temConfigExistente = configAtual?.data?.ativo;
|
||||||
|
if (!temConfigExistente && !senha) {
|
||||||
|
mostrarMensagem("error", "Senha é obrigatória para nova configuração");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!authStore.usuario) {
|
if (!authStore.usuario) {
|
||||||
mostrarMensagem("error", "Usuário não autenticado");
|
mostrarMensagem("error", "Usuário não autenticado");
|
||||||
return;
|
return;
|
||||||
@@ -54,9 +98,9 @@
|
|||||||
try {
|
try {
|
||||||
const resultado = await client.mutation(api.configuracaoEmail.salvarConfigEmail, {
|
const resultado = await client.mutation(api.configuracaoEmail.salvarConfigEmail, {
|
||||||
servidor: servidor.trim(),
|
servidor: servidor.trim(),
|
||||||
porta: Number(porta),
|
porta: portaNum,
|
||||||
usuario: usuario.trim(),
|
usuario: usuario.trim(),
|
||||||
senha: senha,
|
senha: senha || "", // Senha vazia será tratada no backend
|
||||||
emailRemetente: emailRemetente.trim(),
|
emailRemetente: emailRemetente.trim(),
|
||||||
nomeRemetente: nomeRemetente.trim(),
|
nomeRemetente: nomeRemetente.trim(),
|
||||||
usarSSL,
|
usarSSL,
|
||||||
@@ -79,16 +123,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function testarConexao() {
|
async function testarConexao() {
|
||||||
if (!servidor || !porta || !usuario || !senha) {
|
if (!servidor?.trim() || !porta || !usuario?.trim() || !senha) {
|
||||||
mostrarMensagem("error", "Preencha os dados de conexão antes de testar");
|
mostrarMensagem("error", "Preencha os dados de conexão antes de testar");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validação de porta
|
||||||
|
const portaNum = Number(porta);
|
||||||
|
if (isNaN(portaNum) || portaNum < 1 || portaNum > 65535) {
|
||||||
|
mostrarMensagem("error", "Porta deve ser um número entre 1 e 65535");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
testando = true;
|
testando = true;
|
||||||
try {
|
try {
|
||||||
const resultado = await client.action(api.configuracaoEmail.testarConexaoSMTP, {
|
const resultado = await client.mutation(api.configuracaoEmail.testarConexaoSMTP, {
|
||||||
servidor: servidor.trim(),
|
servidor: servidor.trim(),
|
||||||
porta: Number(porta),
|
porta: portaNum,
|
||||||
usuario: usuario.trim(),
|
usuario: usuario.trim(),
|
||||||
senha: senha,
|
senha: senha,
|
||||||
usarSSL,
|
usarSSL,
|
||||||
@@ -111,6 +162,9 @@
|
|||||||
const statusConfig = $derived(
|
const statusConfig = $derived(
|
||||||
configAtual?.data?.ativo ? "Configurado" : "Não configurado"
|
configAtual?.data?.ativo ? "Configurado" : "Não configurado"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isLoading = $derived(configAtual === undefined);
|
||||||
|
const hasError = $derived(configAtual === null && !isLoading);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-6 max-w-4xl">
|
<div class="container mx-auto px-4 py-6 max-w-4xl">
|
||||||
@@ -162,24 +216,35 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="alert alert-info mb-6">
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
<span>Carregando configurações...</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<div class="alert {configAtual?.data?.ativo ? 'alert-success' : 'alert-warning'} mb-6">
|
{#if !isLoading}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
<div class="alert {configAtual?.data?.ativo ? 'alert-success' : 'alert-warning'} mb-6">
|
||||||
{#if configAtual?.data?.ativo}
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
{#if configAtual?.data?.ativo}
|
||||||
{:else}
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
{:else}
|
||||||
{/if}
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
</svg>
|
{/if}
|
||||||
<span>
|
</svg>
|
||||||
<strong>Status:</strong> {statusConfig}
|
<span>
|
||||||
{#if configAtual?.data?.testadoEm}
|
<strong>Status:</strong> {statusConfig}
|
||||||
- Última conexão testada em {new Date(configAtual.data.testadoEm).toLocaleString('pt-BR')}
|
{#if configAtual?.data?.testadoEm}
|
||||||
{/if}
|
- Última conexão testada em {new Date(configAtual.data.testadoEm).toLocaleString('pt-BR')}
|
||||||
</span>
|
{/if}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Formulário -->
|
<!-- Formulário -->
|
||||||
|
{#if !isLoading}
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="card bg-base-100 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title mb-4">Dados do Servidor SMTP</h2>
|
<h2 class="card-title mb-4">Dados do Servidor SMTP</h2>
|
||||||
@@ -294,7 +359,8 @@
|
|||||||
<label class="label cursor-pointer gap-3">
|
<label class="label cursor-pointer gap-3">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:checked={usarSSL}
|
checked={usarSSL}
|
||||||
|
onchange={toggleSSL}
|
||||||
class="checkbox checkbox-primary"
|
class="checkbox checkbox-primary"
|
||||||
/>
|
/>
|
||||||
<span class="label-text">Usar SSL (porta 465)</span>
|
<span class="label-text">Usar SSL (porta 465)</span>
|
||||||
@@ -305,7 +371,8 @@
|
|||||||
<label class="label cursor-pointer gap-3">
|
<label class="label cursor-pointer gap-3">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:checked={usarTLS}
|
checked={usarTLS}
|
||||||
|
onchange={toggleTLS}
|
||||||
class="checkbox checkbox-primary"
|
class="checkbox checkbox-primary"
|
||||||
/>
|
/>
|
||||||
<span class="label-text">Usar TLS (porta 587)</span>
|
<span class="label-text">Usar TLS (porta 587)</span>
|
||||||
@@ -347,6 +414,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Exemplos Comuns -->
|
<!-- Exemplos Comuns -->
|
||||||
<div class="card bg-base-100 shadow-xl mt-6">
|
<div class="card bg-base-100 shadow-xl mt-6">
|
||||||
|
|||||||
@@ -325,7 +325,7 @@
|
|||||||
nome: destinatario.nome,
|
nome: destinatario.nome,
|
||||||
matricula: destinatario.matricula,
|
matricula: destinatario.matricula,
|
||||||
},
|
},
|
||||||
enviadoPorId: destinatario._id as any,
|
enviadoPorId: authStore.usuario._id as Id<"usuarios">,
|
||||||
agendadaPara: agendadaPara,
|
agendadaPara: agendadaPara,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -335,7 +335,7 @@
|
|||||||
destinatarioId: destinatario._id as any,
|
destinatarioId: destinatario._id as any,
|
||||||
assunto: "Notificação do Sistema",
|
assunto: "Notificação do Sistema",
|
||||||
corpo: mensagemPersonalizada,
|
corpo: mensagemPersonalizada,
|
||||||
enviadoPorId: destinatario._id as any,
|
enviadoPorId: authStore.usuario._id as Id<"usuarios">,
|
||||||
agendadaPara: agendadaPara,
|
agendadaPara: agendadaPara,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -433,7 +433,7 @@
|
|||||||
nome: destinatario.nome,
|
nome: destinatario.nome,
|
||||||
matricula: destinatario.matricula || "",
|
matricula: destinatario.matricula || "",
|
||||||
},
|
},
|
||||||
enviadoPorId: destinatario._id as any,
|
enviadoPorId: authStore.usuario._id as Id<"usuarios">,
|
||||||
agendadaPara: agendadaPara,
|
agendadaPara: agendadaPara,
|
||||||
});
|
});
|
||||||
sucessosEmail++;
|
sucessosEmail++;
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
destinatarioId: destinatario._id as any,
|
destinatarioId: destinatario._id as any,
|
||||||
assunto: "Notificação do Sistema",
|
assunto: "Notificação do Sistema",
|
||||||
corpo: mensagemPersonalizada,
|
corpo: mensagemPersonalizada,
|
||||||
enviadoPorId: destinatario._id as any,
|
enviadoPorId: authStore.usuario._id as Id<"usuarios">,
|
||||||
agendadaPara: agendadaPara,
|
agendadaPara: agendadaPara,
|
||||||
});
|
});
|
||||||
sucessosEmail++;
|
sucessosEmail++;
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export const enviar = action({
|
|||||||
return { sucesso: false, erro: "Email não encontrado" };
|
return { sucesso: false, erro: "Email não encontrado" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar configuração SMTP ativa
|
// Buscar configuração SMTP ativa com senha descriptografada
|
||||||
const config = await ctx.runQuery(internal.email.getActiveEmailConfig, {});
|
const config = await ctx.runQuery(internal.email.getActiveEmailConfigWithPassword, {});
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return {
|
return {
|
||||||
@@ -52,8 +52,7 @@ export const enviar = action({
|
|||||||
secure: config.usarSSL,
|
secure: config.usarSSL,
|
||||||
auth: {
|
auth: {
|
||||||
user: config.usuario,
|
user: config.usuario,
|
||||||
// Em produção deve ser armazenado com criptografia reversível
|
pass: config.senha, // Senha já descriptografada
|
||||||
pass: config.senhaHash,
|
|
||||||
},
|
},
|
||||||
tls: {
|
tls: {
|
||||||
// Permitir certificados autoassinados
|
// Permitir certificados autoassinados
|
||||||
|
|||||||
@@ -130,3 +130,106 @@ export function validarSenha(senha: string): boolean {
|
|||||||
return regex.test(senha);
|
return regex.test(senha);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Criptografia reversível para senhas SMTP usando AES-GCM
|
||||||
|
* NOTA: Esta função é usada apenas para senhas SMTP que precisam ser descriptografadas.
|
||||||
|
* Para senhas de usuários, use hashPassword() que é unidirecional.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Chave de criptografia derivada (em produção, deve vir de variável de ambiente)
|
||||||
|
// Para desenvolvimento, usando uma chave fixa. Em produção, deve ser configurada via env var.
|
||||||
|
const getEncryptionKey = async (): Promise<CryptoKey> => {
|
||||||
|
// Chave base - em produção, isso deve vir de process.env.ENCRYPTION_KEY
|
||||||
|
// Por enquanto, usando uma chave derivada de um valor fixo
|
||||||
|
const keyMaterial = new TextEncoder().encode("SGSE-EMAIL-ENCRYPTION-KEY-2024");
|
||||||
|
|
||||||
|
// Deriva uma chave de 256 bits usando PBKDF2
|
||||||
|
const key = await crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
keyMaterial,
|
||||||
|
{ name: "PBKDF2" },
|
||||||
|
false,
|
||||||
|
["deriveBits", "deriveKey"]
|
||||||
|
);
|
||||||
|
|
||||||
|
return await crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: "PBKDF2",
|
||||||
|
salt: new TextEncoder().encode("SGSE-SALT"),
|
||||||
|
iterations: 100000,
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
{ name: "AES-GCM", length: 256 },
|
||||||
|
false,
|
||||||
|
["encrypt", "decrypt"]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Criptografa uma senha SMTP usando AES-GCM
|
||||||
|
*/
|
||||||
|
export async function encryptSMTPPassword(password: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const key = await getEncryptionKey();
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(password);
|
||||||
|
|
||||||
|
// Gerar IV (Initialization Vector) aleatório
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
|
||||||
|
// Criptografar
|
||||||
|
const encrypted = await crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combinar IV + dados criptografados e converter para base64
|
||||||
|
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
||||||
|
combined.set(iv);
|
||||||
|
combined.set(new Uint8Array(encrypted), iv.length);
|
||||||
|
|
||||||
|
return btoa(String.fromCharCode(...combined));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao criptografar senha SMTP:", error);
|
||||||
|
throw new Error("Falha ao criptografar senha SMTP");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descriptografa uma senha SMTP usando AES-GCM
|
||||||
|
*/
|
||||||
|
export async function decryptSMTPPassword(encryptedPassword: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const key = await getEncryptionKey();
|
||||||
|
|
||||||
|
// Decodificar base64
|
||||||
|
const combined = Uint8Array.from(atob(encryptedPassword), (c) => c.charCodeAt(0));
|
||||||
|
|
||||||
|
// Extrair IV e dados criptografados
|
||||||
|
const iv = combined.slice(0, 12);
|
||||||
|
const encrypted = combined.slice(12);
|
||||||
|
|
||||||
|
// Descriptografar
|
||||||
|
const decrypted = await crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
encrypted
|
||||||
|
);
|
||||||
|
|
||||||
|
// Converter para string
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
return decoder.decode(decrypted);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao descriptografar senha SMTP:", error);
|
||||||
|
throw new Error("Falha ao descriptografar senha SMTP");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { mutation, query, action } from "./_generated/server";
|
import { mutation, query } from "./_generated/server";
|
||||||
import { hashPassword } from "./auth/utils";
|
import { encryptSMTPPassword } from "./auth/utils";
|
||||||
import { registrarAtividade } from "./logsAtividades";
|
import { registrarAtividade } from "./logsAtividades";
|
||||||
|
import { api } from "./_generated/api";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obter configuração de email ativa (senha mascarada)
|
* 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" };
|
return { sucesso: false as const, erro: "Email remetente inválido" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Criptografar senha
|
// Validar porta
|
||||||
const senhaHash = await hashPassword(args.senha);
|
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
|
// Desativar config anterior
|
||||||
const configsAntigas = await ctx.db
|
const configsAntigas = await ctx.db
|
||||||
@@ -105,10 +127,7 @@ export const salvarConfigEmail = mutation({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Testar conexão SMTP (action - precisa de Node.js)
|
* Testar conexão SMTP (mutation que chama action real)
|
||||||
*
|
|
||||||
* NOTA: Esta action será implementada quando instalarmos nodemailer.
|
|
||||||
* Por enquanto, retorna sucesso simulado para não bloquear o desenvolvimento.
|
|
||||||
*/
|
*/
|
||||||
export const testarConexaoSMTP = mutation({
|
export const testarConexaoSMTP = mutation({
|
||||||
args: {
|
args: {
|
||||||
@@ -119,10 +138,65 @@ export const testarConexaoSMTP = mutation({
|
|||||||
usarSSL: v.boolean(),
|
usarSSL: v.boolean(),
|
||||||
usarTLS: 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) => {
|
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"
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,30 @@ export const getActiveEmailConfig = internalQuery({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Query interna para obter configuração com senha descriptografada
|
||||||
|
export const getActiveEmailConfigWithPassword = internalQuery({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const { decryptSMTPPassword } = await import("./auth/utils");
|
||||||
|
const config = await ctx.db
|
||||||
|
.query("configuracaoEmail")
|
||||||
|
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptografar senha
|
||||||
|
const senhaDescriptografada = await decryptSMTPPassword(config.senhaHash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
senha: senhaDescriptografada,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const markEmailEnviando = internalMutation({
|
export const markEmailEnviando = internalMutation({
|
||||||
args: { emailId: v.id("notificacoesEmail") },
|
args: { emailId: v.id("notificacoesEmail") },
|
||||||
returns: v.null(),
|
returns: v.null(),
|
||||||
|
|||||||
@@ -471,7 +471,7 @@ export default defineSchema({
|
|||||||
servidor: v.string(), // smtp.gmail.com
|
servidor: v.string(), // smtp.gmail.com
|
||||||
porta: v.number(), // 587, 465, etc.
|
porta: v.number(), // 587, 465, etc.
|
||||||
usuario: v.string(),
|
usuario: v.string(),
|
||||||
senhaHash: v.string(), // senha criptografada
|
senhaHash: v.string(), // senha criptografada reversível (AES-GCM) - necessário para descriptografar e usar no SMTP
|
||||||
emailRemetente: v.string(),
|
emailRemetente: v.string(),
|
||||||
nomeRemetente: v.string(),
|
nomeRemetente: v.string(),
|
||||||
usarSSL: v.boolean(),
|
usarSSL: v.boolean(),
|
||||||
|
|||||||
Reference in New Issue
Block a user