feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.
This commit is contained in:
@@ -1,233 +1,216 @@
|
||||
"use node";
|
||||
'use node';
|
||||
|
||||
import { action } from "../_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { internal } from "../_generated/api";
|
||||
import { decryptSMTPPasswordNode } from "./utils/nodeCrypto";
|
||||
import nodemailer from "nodemailer";
|
||||
import { v } from 'convex/values';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { internal } from '../_generated/api';
|
||||
import { action } from '../_generated/server';
|
||||
import { decryptSMTPPasswordNode } from './utils/nodeCrypto';
|
||||
|
||||
export const enviar = action({
|
||||
args: {
|
||||
emailId: v.id("notificacoesEmail"),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
||||
handler: async (ctx, args) => {
|
||||
"use node";
|
||||
args: {
|
||||
emailId: v.id('notificacoesEmail')
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
||||
handler: async (ctx, args) => {
|
||||
'use node';
|
||||
|
||||
let email;
|
||||
try {
|
||||
// Buscar email da fila
|
||||
email = await ctx.runQuery(internal.email.getEmailById, {
|
||||
emailId: args.emailId,
|
||||
});
|
||||
let email;
|
||||
try {
|
||||
// Buscar email da fila
|
||||
email = await ctx.runQuery(internal.email.getEmailById, {
|
||||
emailId: args.emailId
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
return { sucesso: false, erro: "Email não encontrado" };
|
||||
}
|
||||
if (!email) {
|
||||
return { sucesso: false, erro: 'Email não encontrado' };
|
||||
}
|
||||
|
||||
// Buscar configuração SMTP ativa
|
||||
const configRaw = await ctx.runQuery(
|
||||
internal.email.getActiveEmailConfig,
|
||||
{}
|
||||
);
|
||||
// Buscar configuração SMTP ativa
|
||||
const configRaw = await ctx.runQuery(internal.email.getActiveEmailConfig, {});
|
||||
|
||||
if (!configRaw) {
|
||||
console.error(
|
||||
"❌ Configuração SMTP não encontrada ou inativa para email:",
|
||||
email.destinatario
|
||||
);
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: "Configuração de email não encontrada ou inativa. Verifique as configurações SMTP no painel de TI.",
|
||||
};
|
||||
}
|
||||
if (!configRaw) {
|
||||
console.error(
|
||||
'❌ Configuração SMTP não encontrada ou inativa para email:',
|
||||
email.destinatario
|
||||
);
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Configuração de email não encontrada ou inativa. Verifique as configurações SMTP no painel de TI.'
|
||||
};
|
||||
}
|
||||
|
||||
console.log("📧 Tentando enviar email:", {
|
||||
para: email.destinatario,
|
||||
assunto: email.assunto,
|
||||
servidor: configRaw.servidor,
|
||||
porta: configRaw.porta,
|
||||
});
|
||||
console.log('📧 Tentando enviar email:', {
|
||||
para: email.destinatario,
|
||||
assunto: email.assunto,
|
||||
servidor: configRaw.servidor,
|
||||
porta: configRaw.porta
|
||||
});
|
||||
|
||||
// Descriptografar senha usando função compatível com Node.js
|
||||
let senhaDescriptografada: string;
|
||||
try {
|
||||
senhaDescriptografada = await decryptSMTPPasswordNode(
|
||||
configRaw.senhaHash
|
||||
);
|
||||
} catch (decryptError) {
|
||||
const decryptErrorMessage =
|
||||
decryptError instanceof Error
|
||||
? decryptError.message
|
||||
: String(decryptError);
|
||||
console.error(
|
||||
"Erro ao descriptografar senha SMTP:",
|
||||
decryptErrorMessage
|
||||
);
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: `Erro ao descriptografar senha SMTP: ${decryptErrorMessage}`,
|
||||
};
|
||||
}
|
||||
// Descriptografar senha usando função compatível com Node.js
|
||||
let senhaDescriptografada: string;
|
||||
try {
|
||||
senhaDescriptografada = await decryptSMTPPasswordNode(configRaw.senhaHash);
|
||||
} catch (decryptError) {
|
||||
const decryptErrorMessage =
|
||||
decryptError instanceof Error ? decryptError.message : String(decryptError);
|
||||
console.error('Erro ao descriptografar senha SMTP:', decryptErrorMessage);
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: `Erro ao descriptografar senha SMTP: ${decryptErrorMessage}`
|
||||
};
|
||||
}
|
||||
|
||||
const config = {
|
||||
...configRaw,
|
||||
senha: senhaDescriptografada,
|
||||
};
|
||||
const config = {
|
||||
...configRaw,
|
||||
senha: senhaDescriptografada
|
||||
};
|
||||
|
||||
// Config já foi validado acima
|
||||
// Config já foi validado acima
|
||||
|
||||
// Avisar mas não bloquear se não foi testado
|
||||
if (!config.testadoEm) {
|
||||
console.warn(
|
||||
"⚠️ Configuração SMTP não foi testada. Tentando enviar mesmo assim..."
|
||||
);
|
||||
}
|
||||
// Avisar mas não bloquear se não foi testado
|
||||
if (!config.testadoEm) {
|
||||
console.warn('⚠️ Configuração SMTP não foi testada. Tentando enviar mesmo assim...');
|
||||
}
|
||||
|
||||
// Marcar como enviando
|
||||
await ctx.runMutation(internal.email.markEmailEnviando, {
|
||||
emailId: args.emailId,
|
||||
});
|
||||
// Marcar como enviando
|
||||
await ctx.runMutation(internal.email.markEmailEnviando, {
|
||||
emailId: args.emailId
|
||||
});
|
||||
|
||||
// Criar transporter do nodemailer com configuração melhorada
|
||||
const transporterOptions: {
|
||||
host: string;
|
||||
port: number;
|
||||
secure: boolean;
|
||||
requireTLS?: boolean;
|
||||
auth: {
|
||||
user: string;
|
||||
pass: string;
|
||||
};
|
||||
tls?: {
|
||||
rejectUnauthorized: boolean;
|
||||
ciphers?: string;
|
||||
};
|
||||
connectionTimeout: number;
|
||||
greetingTimeout: number;
|
||||
socketTimeout: number;
|
||||
pool?: boolean;
|
||||
maxConnections?: number;
|
||||
maxMessages?: number;
|
||||
} = {
|
||||
host: config.servidor,
|
||||
port: config.porta,
|
||||
secure: config.usarSSL,
|
||||
auth: {
|
||||
user: config.usuario,
|
||||
pass: config.senha, // Senha já descriptografada
|
||||
},
|
||||
connectionTimeout: 15000, // 15 segundos
|
||||
greetingTimeout: 15000,
|
||||
socketTimeout: 15000,
|
||||
pool: true, // Usar pool de conexões
|
||||
maxConnections: 5,
|
||||
maxMessages: 100,
|
||||
};
|
||||
// Criar transporter do nodemailer com configuração melhorada
|
||||
const transporterOptions: {
|
||||
host: string;
|
||||
port: number;
|
||||
secure: boolean;
|
||||
requireTLS?: boolean;
|
||||
auth: {
|
||||
user: string;
|
||||
pass: string;
|
||||
};
|
||||
tls?: {
|
||||
rejectUnauthorized: boolean;
|
||||
ciphers?: string;
|
||||
};
|
||||
connectionTimeout: number;
|
||||
greetingTimeout: number;
|
||||
socketTimeout: number;
|
||||
pool?: boolean;
|
||||
maxConnections?: number;
|
||||
maxMessages?: number;
|
||||
} = {
|
||||
host: config.servidor,
|
||||
port: config.porta,
|
||||
secure: config.usarSSL,
|
||||
auth: {
|
||||
user: config.usuario,
|
||||
pass: config.senha // Senha já descriptografada
|
||||
},
|
||||
connectionTimeout: 15000, // 15 segundos
|
||||
greetingTimeout: 15000,
|
||||
socketTimeout: 15000,
|
||||
pool: true, // Usar pool de conexões
|
||||
maxConnections: 5,
|
||||
maxMessages: 100
|
||||
};
|
||||
|
||||
// Adicionar TLS apenas se necessário
|
||||
if (config.usarTLS) {
|
||||
transporterOptions.requireTLS = true;
|
||||
transporterOptions.tls = {
|
||||
rejectUnauthorized: false, // Permitir certificados autoassinados
|
||||
};
|
||||
} else if (config.usarSSL) {
|
||||
transporterOptions.tls = {
|
||||
rejectUnauthorized: false,
|
||||
};
|
||||
}
|
||||
// Adicionar TLS apenas se necessário
|
||||
if (config.usarTLS) {
|
||||
transporterOptions.requireTLS = true;
|
||||
transporterOptions.tls = {
|
||||
rejectUnauthorized: false // Permitir certificados autoassinados
|
||||
};
|
||||
} else if (config.usarSSL) {
|
||||
transporterOptions.tls = {
|
||||
rejectUnauthorized: false
|
||||
};
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(transporterOptions);
|
||||
const transporter = nodemailer.createTransport(transporterOptions);
|
||||
|
||||
// Verificar conexão antes de enviar
|
||||
try {
|
||||
await transporter.verify();
|
||||
console.log("✅ Conexão SMTP verificada com sucesso");
|
||||
} catch (verifyError) {
|
||||
const verifyErrorMessage =
|
||||
verifyError instanceof Error
|
||||
? verifyError.message
|
||||
: String(verifyError);
|
||||
console.warn(
|
||||
"⚠️ Falha na verificação SMTP, mas tentando enviar mesmo assim:",
|
||||
verifyErrorMessage
|
||||
);
|
||||
// Não bloquear envio por falha na verificação, apenas avisar
|
||||
}
|
||||
// Verificar conexão antes de enviar
|
||||
try {
|
||||
await transporter.verify();
|
||||
console.log('✅ Conexão SMTP verificada com sucesso');
|
||||
} catch (verifyError) {
|
||||
const verifyErrorMessage =
|
||||
verifyError instanceof Error ? verifyError.message : String(verifyError);
|
||||
console.warn(
|
||||
'⚠️ Falha na verificação SMTP, mas tentando enviar mesmo assim:',
|
||||
verifyErrorMessage
|
||||
);
|
||||
// Não bloquear envio por falha na verificação, apenas avisar
|
||||
}
|
||||
|
||||
// Validar email destinatário antes de enviar
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email.destinatario)) {
|
||||
throw new Error(`Email destinatário inválido: ${email.destinatario}`);
|
||||
}
|
||||
// Validar email destinatário antes de enviar
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email.destinatario)) {
|
||||
throw new Error(`Email destinatário inválido: ${email.destinatario}`);
|
||||
}
|
||||
|
||||
// Criar versão texto do HTML (remover tags e decodificar entidades básicas)
|
||||
const textoPlano = email.corpo
|
||||
.replace(/<[^>]*>/g, "") // Remover tags HTML
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.trim();
|
||||
// Criar versão texto do HTML (remover tags e decodificar entidades básicas)
|
||||
const textoPlano = email.corpo
|
||||
.replace(/<[^>]*>/g, '') // Remover tags HTML
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.trim();
|
||||
|
||||
// Enviar email
|
||||
const info = await transporter.sendMail({
|
||||
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
|
||||
to: email.destinatario,
|
||||
subject: email.assunto,
|
||||
html: email.corpo,
|
||||
text: textoPlano || email.assunto, // Versão texto para clientes que não suportam HTML
|
||||
headers: {
|
||||
"X-Mailer": "SGSE-Sistema-de-Gerenciamento-de-Secretaria",
|
||||
"X-Priority": "3",
|
||||
},
|
||||
});
|
||||
// Enviar email
|
||||
const info = await transporter.sendMail({
|
||||
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
|
||||
to: email.destinatario,
|
||||
subject: email.assunto,
|
||||
html: email.corpo,
|
||||
text: textoPlano || email.assunto, // Versão texto para clientes que não suportam HTML
|
||||
headers: {
|
||||
'X-Mailer': 'SGSE-Sistema-de-Gerenciamento-de-Secretaria',
|
||||
'X-Priority': '3'
|
||||
}
|
||||
});
|
||||
|
||||
interface MessageInfo {
|
||||
messageId?: string;
|
||||
response?: string;
|
||||
}
|
||||
interface MessageInfo {
|
||||
messageId?: string;
|
||||
response?: string;
|
||||
}
|
||||
|
||||
const messageInfo = info as MessageInfo;
|
||||
const messageInfo = info as MessageInfo;
|
||||
|
||||
console.log("✅ Email enviado com sucesso!", {
|
||||
para: email.destinatario,
|
||||
assunto: email.assunto,
|
||||
messageId: messageInfo.messageId,
|
||||
response: messageInfo.response,
|
||||
});
|
||||
console.log('✅ Email enviado com sucesso!', {
|
||||
para: email.destinatario,
|
||||
assunto: email.assunto,
|
||||
messageId: messageInfo.messageId,
|
||||
response: messageInfo.response
|
||||
});
|
||||
|
||||
// Marcar como enviado
|
||||
await ctx.runMutation(internal.email.markEmailEnviado, {
|
||||
emailId: args.emailId,
|
||||
});
|
||||
// Marcar como enviado
|
||||
await ctx.runMutation(internal.email.markEmailEnviado, {
|
||||
emailId: args.emailId
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||
return { sucesso: true };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||
|
||||
console.error("❌ Erro ao enviar email:", {
|
||||
emailId: args.emailId,
|
||||
destinatario: email?.destinatario,
|
||||
erro: errorMessage,
|
||||
stack: errorStack,
|
||||
});
|
||||
console.error('❌ Erro ao enviar email:', {
|
||||
emailId: args.emailId,
|
||||
destinatario: email?.destinatario,
|
||||
erro: errorMessage,
|
||||
stack: errorStack
|
||||
});
|
||||
|
||||
// Marcar como falha com detalhes completos
|
||||
const erroCompleto = errorStack
|
||||
? `${errorMessage}\n\nStack: ${errorStack}`
|
||||
: errorMessage;
|
||||
// Marcar como falha com detalhes completos
|
||||
const erroCompleto = errorStack ? `${errorMessage}\n\nStack: ${errorStack}` : errorMessage;
|
||||
|
||||
await ctx.runMutation(internal.email.markEmailFalha, {
|
||||
emailId: args.emailId,
|
||||
erro: erroCompleto.substring(0, 2000), // Limitar tamanho do erro
|
||||
});
|
||||
await ctx.runMutation(internal.email.markEmailFalha, {
|
||||
emailId: args.emailId,
|
||||
erro: erroCompleto.substring(0, 2000) // Limitar tamanho do erro
|
||||
});
|
||||
|
||||
return { sucesso: false, erro: errorMessage };
|
||||
}
|
||||
},
|
||||
return { sucesso: false, erro: errorMessage };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user