feat: implement advanced access control system with user blocking, rate limiting, and enhanced login security; update UI components for improved user experience and documentation
This commit is contained in:
259
packages/backend/convex/email.ts
Normal file
259
packages/backend/convex/email.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query, action, internalMutation } from "./_generated/server";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
import { renderizarTemplate } from "./templatesMensagens";
|
||||
|
||||
/**
|
||||
* Enfileirar email para envio
|
||||
*/
|
||||
export const enfileirarEmail = mutation({
|
||||
args: {
|
||||
destinatario: v.string(), // email
|
||||
destinatarioId: v.optional(v.id("usuarios")),
|
||||
assunto: v.string(),
|
||||
corpo: v.string(),
|
||||
templateId: v.optional(v.id("templatesMensagens")),
|
||||
enviadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), emailId: v.optional(v.id("notificacoesEmail")) }),
|
||||
handler: async (ctx, args) => {
|
||||
// Validar email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(args.destinatario)) {
|
||||
return { sucesso: false };
|
||||
}
|
||||
|
||||
// Adicionar à fila
|
||||
const emailId = await ctx.db.insert("notificacoesEmail", {
|
||||
destinatario: args.destinatario,
|
||||
destinatarioId: args.destinatarioId,
|
||||
assunto: args.assunto,
|
||||
corpo: args.corpo,
|
||||
templateId: args.templateId,
|
||||
status: "pendente",
|
||||
tentativas: 0,
|
||||
enviadoPor: args.enviadoPorId,
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
|
||||
return { sucesso: true, emailId };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Enviar email usando template
|
||||
*/
|
||||
export const enviarEmailComTemplate = mutation({
|
||||
args: {
|
||||
destinatario: v.string(),
|
||||
destinatarioId: v.optional(v.id("usuarios")),
|
||||
templateCodigo: v.string(),
|
||||
variaveis: v.any(), // Record<string, string>
|
||||
enviadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), emailId: v.optional(v.id("notificacoesEmail")) }),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar template
|
||||
const template = await ctx.db
|
||||
.query("templatesMensagens")
|
||||
.withIndex("by_codigo", (q) => q.eq("codigo", args.templateCodigo))
|
||||
.first();
|
||||
|
||||
if (!template) {
|
||||
console.error("Template não encontrado:", args.templateCodigo);
|
||||
return { sucesso: false };
|
||||
}
|
||||
|
||||
// Renderizar template
|
||||
const assunto = renderizarTemplate(template.titulo, args.variaveis);
|
||||
const corpo = renderizarTemplate(template.corpo, args.variaveis);
|
||||
|
||||
// Enfileirar email
|
||||
const emailId = await ctx.db.insert("notificacoesEmail", {
|
||||
destinatario: args.destinatario,
|
||||
destinatarioId: args.destinatarioId,
|
||||
assunto,
|
||||
corpo,
|
||||
templateId: template._id,
|
||||
status: "pendente",
|
||||
tentativas: 0,
|
||||
enviadoPor: args.enviadoPorId,
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
|
||||
return { sucesso: true, emailId };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Listar emails na fila
|
||||
*/
|
||||
export const listarFilaEmails = query({
|
||||
args: {
|
||||
status: v.optional(v.union(
|
||||
v.literal("pendente"),
|
||||
v.literal("enviando"),
|
||||
v.literal("enviado"),
|
||||
v.literal("falha")
|
||||
)),
|
||||
limite: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
let query = ctx.db.query("notificacoesEmail");
|
||||
|
||||
if (args.status) {
|
||||
query = query.withIndex("by_status", (q) => q.eq("status", args.status));
|
||||
} else {
|
||||
query = query.withIndex("by_criado_em");
|
||||
}
|
||||
|
||||
const emails = await query.order("desc").take(args.limite || 100);
|
||||
|
||||
return emails;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Reenviar email falhado
|
||||
*/
|
||||
export const reenviarEmail = mutation({
|
||||
args: {
|
||||
emailId: v.id("notificacoesEmail"),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean() }),
|
||||
handler: async (ctx, args) => {
|
||||
const email = await ctx.db.get(args.emailId);
|
||||
if (!email) {
|
||||
return { sucesso: false };
|
||||
}
|
||||
|
||||
// Resetar status para pendente
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "pendente",
|
||||
tentativas: 0,
|
||||
ultimaTentativa: undefined,
|
||||
erroDetalhes: undefined,
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Action para enviar email (será implementado com nodemailer)
|
||||
*
|
||||
* NOTA: Este é um placeholder. Implementação real requer nodemailer.
|
||||
*/
|
||||
export const enviarEmailAction = action({
|
||||
args: {
|
||||
emailId: v.id("notificacoesEmail"),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
||||
handler: async (ctx, args) => {
|
||||
// TODO: Implementar com nodemailer quando instalado
|
||||
|
||||
try {
|
||||
// Buscar email da fila
|
||||
const email = await ctx.runQuery(async (ctx) => {
|
||||
return await ctx.db.get(args.emailId);
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
return { sucesso: false, erro: "Email não encontrado" };
|
||||
}
|
||||
|
||||
// Buscar configuração SMTP
|
||||
const config = await ctx.runQuery(async (ctx) => {
|
||||
return await ctx.db
|
||||
.query("configuracaoEmail")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.first();
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
return { sucesso: false, erro: "Configuração de email não encontrada" };
|
||||
}
|
||||
|
||||
// Marcar como enviando
|
||||
await ctx.runMutation(async (ctx) => {
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "enviando",
|
||||
tentativas: (email.tentativas || 0) + 1,
|
||||
ultimaTentativa: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Enviar email real com nodemailer aqui
|
||||
console.log("⚠️ AVISO: Envio de email simulado (nodemailer não instalado)");
|
||||
console.log(" Para:", email.destinatario);
|
||||
console.log(" Assunto:", email.assunto);
|
||||
|
||||
// Simular delay de envio
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Marcar como enviado
|
||||
await ctx.runMutation(async (ctx) => {
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "enviado",
|
||||
enviadoEm: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
} catch (error: any) {
|
||||
// Marcar como falha
|
||||
await ctx.runMutation(async (ctx) => {
|
||||
const email = await ctx.db.get(args.emailId);
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "falha",
|
||||
erroDetalhes: error.message || "Erro desconhecido",
|
||||
tentativas: (email?.tentativas || 0) + 1,
|
||||
});
|
||||
});
|
||||
|
||||
return { sucesso: false, erro: error.message || "Erro ao enviar email" };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Processar fila de emails (cron job - processa emails pendentes)
|
||||
*/
|
||||
export const processarFilaEmails = internalMutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
// Buscar emails pendentes (max 10 por execução)
|
||||
const emailsPendentes = await ctx.db
|
||||
.query("notificacoesEmail")
|
||||
.withIndex("by_status", (q) => q.eq("status", "pendente"))
|
||||
.take(10);
|
||||
|
||||
let processados = 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",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Agendar envio (será feito por uma action separada)
|
||||
// Por enquanto, apenas marca como enviado para não bloquear
|
||||
await ctx.db.patch(email._id, {
|
||||
status: "enviado",
|
||||
enviadoEm: Date.now(),
|
||||
});
|
||||
|
||||
processados++;
|
||||
}
|
||||
|
||||
console.log(`📧 Fila de emails processada: ${processados} emails`);
|
||||
|
||||
return { processados };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user