feat: correo via banco mail, rol supervisor y lint en cero
This commit is contained in:
1 parent
2502de4e06
commit
b1cddbac6c
15 files changed
+677
-107
No files matched your search
@@ -0,0 +1,128 @@
|
||||
// Route handler server-side para envío de correos vía banco único mail.*.
|
||||
// El navegador NUNCA lleva la X-Api-Key: llama aquí (/api/correos) y este
|
||||
// route llama a SAM.API (POST /api/email/send) con X-Module + X-Api-Key.
|
||||
// Docs: sam/docs/email-modulos.md punto 7.
|
||||
//
|
||||
// Env (servidor, SIN prefijo NEXT_PUBLIC_):
|
||||
// MAIL_API_KEY=clave entregada por POST /api/email/modules (solo servidor)
|
||||
// MAIL_MODULE=helpdesk (opcional, default helpdesk)
|
||||
// SAM_API_URL=https://api.devops.norteamericano.cl (default prod;
|
||||
// en dev cae a NEXT_PUBLIC_API_URL)
|
||||
|
||||
const API_BASE =
|
||||
process.env.SAM_API_URL ??
|
||||
process.env.NEXT_PUBLIC_API_URL ??
|
||||
"https://api.devops.norteamericano.cl";
|
||||
const MODULE = process.env.MAIL_MODULE ?? "helpdesk";
|
||||
|
||||
type SendBody = {
|
||||
to: string | string[];
|
||||
templateKey?: string;
|
||||
subject?: string;
|
||||
htmlBody?: string;
|
||||
vars?: Record<string, string | null>;
|
||||
};
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
async function requireAdmin(req: Request): Promise<Response | null> {
|
||||
const auth = req.headers.get("authorization") ?? "";
|
||||
if (!auth.toLowerCase().startsWith("bearer ")) {
|
||||
return Response.json(
|
||||
{ success: false, message: "Requiere sesión de admin" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
try {
|
||||
const me = await fetch(`${API_BASE}/api/helpdesk/me`, {
|
||||
headers: { Authorization: auth },
|
||||
});
|
||||
if (!me.ok) {
|
||||
return Response.json(
|
||||
{ success: false, message: "Sesión inválida" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
const user = (await me.json()) as { role?: string };
|
||||
if (user.role !== "admin") {
|
||||
return Response.json(
|
||||
{ success: false, message: "Requiere rol admin" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
return Response.json(
|
||||
{ success: false, message: `No se pudo verificar admin: ${String(err)}` },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request): Promise<Response> {
|
||||
const denied = await requireAdmin(req);
|
||||
if (denied) return denied;
|
||||
|
||||
const apiKey = process.env.MAIL_API_KEY ?? "";
|
||||
if (!apiKey) {
|
||||
return Response.json(
|
||||
{ success: false, message: "MAIL_API_KEY no configurada en el servidor del módulo" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
let body: SendBody;
|
||||
try {
|
||||
body = (await req.json()) as SendBody;
|
||||
} catch {
|
||||
return Response.json({ success: false, message: "JSON inválido" }, { status: 400 });
|
||||
}
|
||||
|
||||
const raw = Array.isArray(body.to) ? body.to : [body.to];
|
||||
const clean = [
|
||||
...new Set(
|
||||
raw.map((e) => String(e ?? "").trim()).filter((e) => EMAIL_RE.test(e))
|
||||
),
|
||||
].slice(0, 50);
|
||||
if (clean.length === 0) {
|
||||
return Response.json({ success: false, message: "Sin destinatarios válidos" }, { status: 400 });
|
||||
}
|
||||
if (!body.templateKey && (!body.subject || !body.htmlBody)) {
|
||||
return Response.json(
|
||||
{ success: false, message: "templateKey o subject+htmlBody requeridos" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
let upstream: Response;
|
||||
try {
|
||||
upstream = await fetch(`${API_BASE}/api/email/send`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Module": MODULE,
|
||||
"X-Api-Key": apiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
to: clean.length === 1 ? clean[0] : clean,
|
||||
templateKey: body.templateKey,
|
||||
subject: body.subject,
|
||||
htmlBody: body.htmlBody,
|
||||
vars: body.vars ?? {},
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
return Response.json(
|
||||
{ success: false, message: `No se pudo contactar a SAM.API: ${String(err)}` },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
|
||||
let data: unknown = null;
|
||||
try {
|
||||
data = await upstream.json();
|
||||
} catch {
|
||||
data = { success: false, message: `SAM.API respondió ${upstream.status} sin JSON` };
|
||||
}
|
||||
return Response.json(data, { status: upstream.ok ? 200 : upstream.status });
|
||||
}
|
||||
Reference in new issue
Block a user