Files
helpdesk/frontend/src/app/api/correos/route.ts
T

129 lines
3.8 KiB
TypeScript

// 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 });
}