Fix usuarios page #6

Merged
killer-cf merged 28 commits from fix-usuarios-page into master 2025-11-04 17:42:21 +00:00
65 changed files with 9082 additions and 7130 deletions
Showing only changes of commit 5d2df8077b - Show all commits

View File

@@ -50,28 +50,48 @@ export const enviar = action({
host: config.servidor, host: config.servidor,
port: config.porta, port: config.porta,
secure: config.usarSSL, secure: config.usarSSL,
requireTLS: config.usarTLS,
auth: { auth: {
user: config.usuario, user: config.usuario,
pass: config.senha, // Senha já descriptografada pass: config.senha, // Senha já descriptografada
}, },
tls: { tls: {
// Permitir certificados autoassinados // Permitir certificados autoassinados apenas se necessário
rejectUnauthorized: false, rejectUnauthorized: false,
ciphers: "SSLv3",
}, },
connectionTimeout: 10000, // 10 segundos
greetingTimeout: 10000,
socketTimeout: 10000,
}); });
// 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}`);
}
// Enviar email // Enviar email
const info = await transporter.sendMail({ const info = await transporter.sendMail({
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`, from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
to: email.destinatario, to: email.destinatario,
subject: email.assunto, subject: email.assunto,
html: email.corpo, html: email.corpo,
text: email.corpo.replace(/<[^>]*>/g, ""), // Versão texto para clientes que não suportam HTML
}); });
interface MessageInfo {
messageId?: string;
response?: string;
}
const messageInfo = info as MessageInfo;
console.log("✅ Email enviado com sucesso!", { console.log("✅ Email enviado com sucesso!", {
para: email.destinatario, para: email.destinatario,
assunto: email.assunto, assunto: email.assunto,
messageId: (info as { messageId?: string }).messageId, messageId: messageInfo.messageId,
response: messageInfo.response,
}); });
// Marcar como enviado // Marcar como enviado

View File

@@ -42,8 +42,11 @@ export const testarConexao = action({
pass: args.senha, pass: args.senha,
}, },
tls: { tls: {
rejectUnauthorized: !args.usarTLS ? false : false, rejectUnauthorized: false,
}, },
connectionTimeout: 10000, // 10 segundos
greetingTimeout: 10000,
socketTimeout: 10000,
}); });
// Verificar conexão // Verificar conexão

View File

@@ -220,11 +220,21 @@ export const reenviarEmail = mutation({
args: { args: {
emailId: v.id("notificacoesEmail"), emailId: v.id("notificacoesEmail"),
}, },
returns: v.object({ sucesso: v.boolean() }), returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args): Promise<{ sucesso: boolean }> => { handler: async (ctx, args): Promise<{ sucesso: boolean; erro?: string }> => {
const email = await ctx.db.get(args.emailId); const email = await ctx.db.get(args.emailId);
if (!email) { if (!email) {
return { sucesso: false }; return { sucesso: false, erro: "Email não encontrado" };
}
// Verificar se o email não foi enviado com sucesso ainda
if (email.status === "enviado") {
return { sucesso: false, erro: "Este email já foi enviado com sucesso" };
}
// Verificar se ainda não excedeu o limite de tentativas (max 3)
if ((email.tentativas || 0) >= 3 && email.status !== "falha") {
return { sucesso: false, erro: "Número máximo de tentativas excedido. Crie um novo email." };
} }
// Resetar status para pendente // Resetar status para pendente
@@ -235,6 +245,11 @@ export const reenviarEmail = mutation({
erroDetalhes: undefined, erroDetalhes: undefined,
}); });
// Agendar envio imediato
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
emailId: args.emailId,
});
return { sucesso: true }; return { sucesso: true };
}, },
}); });
@@ -317,6 +332,58 @@ export const buscarEmailsPorIds = query({
}, },
}); });
/**
* Obter estatísticas da fila de emails
*/
export const obterEstatisticasFilaEmails = query({
args: {},
returns: v.object({
total: v.number(),
pendentes: v.number(),
enviando: v.number(),
enviados: v.number(),
falhas: v.number(),
comErro: v.number(),
ultimaExecucaoCron: v.optional(v.number()),
}),
handler: async (ctx) => {
const todosEmails = await ctx.db
.query("notificacoesEmail")
.collect();
const estatisticas = {
total: todosEmails.length,
pendentes: 0,
enviando: 0,
enviados: 0,
falhas: 0,
comErro: 0,
};
for (const email of todosEmails) {
switch (email.status) {
case "pendente":
estatisticas.pendentes++;
break;
case "enviando":
estatisticas.enviando++;
break;
case "enviado":
estatisticas.enviados++;
break;
case "falha":
estatisticas.falhas++;
if (email.erroDetalhes) {
estatisticas.comErro++;
}
break;
}
}
return estatisticas;
},
});
/** /**
* Listar agendamentos de email do usuário atual * Listar agendamentos de email do usuário atual
*/ */
@@ -452,15 +519,23 @@ export const markEmailFalha = internalMutation({
*/ */
export const processarFilaEmails = internalMutation({ export const processarFilaEmails = internalMutation({
args: {}, args: {},
returns: v.object({ processados: v.number() }), returns: v.object({ processados: v.number(), falhas: v.number() }),
handler: async (ctx) => { handler: async (ctx) => {
// Buscar emails pendentes (max 10 por execução) // Buscar emails pendentes que não estão agendados para o futuro (max 10 por execução)
const agora = Date.now();
const emailsPendentes = await ctx.db const emailsPendentes = await ctx.db
.query("notificacoesEmail") .query("notificacoesEmail")
.withIndex("by_status", (q) => q.eq("status", "pendente")) .withIndex("by_status", (q) => q.eq("status", "pendente"))
.filter((q) =>
q.or(
q.eq(q.field("agendadaPara"), undefined),
q.lte(q.field("agendadaPara"), agora)
)
)
.take(10); .take(10);
let processados = 0; let processados = 0;
let falhas = 0;
for (const email of emailsPendentes) { for (const email of emailsPendentes) {
// Verificar se não excedeu tentativas (max 3) // Verificar se não excedeu tentativas (max 3)
@@ -469,21 +544,115 @@ export const processarFilaEmails = internalMutation({
status: "falha", status: "falha",
erroDetalhes: "Número máximo de tentativas excedido", erroDetalhes: "Número máximo de tentativas excedido",
}); });
falhas++;
continue; continue;
} }
// Agendar envio via action // Agendar envio via action
await ctx.scheduler.runAfter(0, api.actions.email.enviar, { try {
emailId: email._id, await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
}); emailId: email._id,
});
processados++; processados++;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Erro ao agendar email ${email._id}:`, errorMessage);
await ctx.db.patch(email._id, {
status: "falha",
erroDetalhes: `Erro ao agendar envio: ${errorMessage}`,
tentativas: (email.tentativas || 0) + 1,
});
falhas++;
}
} }
console.log( if (processados > 0 || falhas > 0) {
`📧 Fila de emails processada: ${processados} emails agendados para envio` console.log(
); `📧 Fila de emails processada: ${processados} emails agendados, ${falhas} falhas`
);
}
return { processados }; return { processados, falhas };
},
});
/**
* Processar fila de emails manualmente (para testes e envio imediato)
*/
export const processarFilaEmailsManual = mutation({
args: {
limite: v.optional(v.number()),
},
returns: v.object({
sucesso: v.boolean(),
processados: v.number(),
falhas: v.number(),
erro: v.optional(v.string())
}),
handler: async (ctx, args): Promise<{
sucesso: boolean;
processados: number;
falhas: number;
erro?: string
}> => {
const usuarioAtual = await getUsuarioAutenticado(ctx);
if (!usuarioAtual) {
return { sucesso: false, processados: 0, falhas: 0, erro: "Usuário não autenticado" };
}
// Verificar se usuário tem permissão (TI_MASTER ou admin)
const role = await ctx.db.get(usuarioAtual.roleId);
if (!role || (role.nivel !== 0 && role.nivel !== 1)) {
return { sucesso: false, processados: 0, falhas: 0, erro: "Permissão negada" };
}
const limite = args.limite || 10;
const agora = Date.now();
// Buscar emails pendentes que não estão agendados para o futuro
const emailsPendentes = await ctx.db
.query("notificacoesEmail")
.withIndex("by_status", (q) => q.eq("status", "pendente"))
.filter((q) =>
q.or(
q.eq(q.field("agendadaPara"), undefined),
q.lte(q.field("agendadaPara"), agora)
)
)
.take(limite);
let processados = 0;
let falhas = 0;
for (const email of emailsPendentes) {
// Verificar se não excedeu tentativas (max 3)
if ((email.tentativas || 0) >= 3) {
await ctx.db.patch(email._id, {
status: "falha",
erroDetalhes: "Número máximo de tentativas excedido",
});
falhas++;
continue;
}
// Agendar envio via action
try {
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
emailId: email._id,
});
processados++;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Erro ao agendar email ${email._id}:`, errorMessage);
await ctx.db.patch(email._id, {
status: "falha",
erroDetalhes: `Erro ao agendar envio: ${errorMessage}`,
tentativas: (email.tentativas || 0) + 1,
});
falhas++;
}
}
return { sucesso: true, processados, falhas };
}, },
}); });