refactor(deploy): externalize all secrets to .env, migrate Brevo SMTP → REST API
- docker-compose.yml: replace 43 hardcoded env values with ${VAR} references.
Operators must provide /opt/architools/.env (chmod 600, gitignored) with the
matching keys. Removes the historical leak surface where every edit risked
echoing secrets.
- email-service.ts: drop nodemailer SMTP transport; use Brevo REST API
(POST https://api.brevo.com/v3/smtp/email) with BREVO_API_KEY header.
Brevo SMTP relay credentials have been deleted upstream.
- package.json: remove nodemailer + @types/nodemailer.
NOTE: legacy hardcoded credentials present in git history must still be
rotated separately (DB password, Authentik client secret, ENCRYPTION_SECRET,
ANCPI password, etc.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,55 +1,47 @@
|
||||
import nodemailer from "nodemailer";
|
||||
import type { Transporter } from "nodemailer";
|
||||
import type { EmailPayload } from "./types";
|
||||
|
||||
// ── Singleton transport (lazy init, same pattern as prisma) ──
|
||||
|
||||
const globalForEmail = globalThis as unknown as {
|
||||
emailTransport: Transporter | undefined;
|
||||
};
|
||||
|
||||
function getTransport(): Transporter {
|
||||
if (globalForEmail.emailTransport) return globalForEmail.emailTransport;
|
||||
|
||||
const host = process.env.BREVO_SMTP_HOST ?? "smtp-relay.brevo.com";
|
||||
const port = parseInt(process.env.BREVO_SMTP_PORT ?? "587", 10);
|
||||
const user = process.env.BREVO_SMTP_USER ?? "";
|
||||
const pass = process.env.BREVO_SMTP_PASS ?? "";
|
||||
|
||||
if (!user || !pass) {
|
||||
throw new Error(
|
||||
"BREVO_SMTP_USER and BREVO_SMTP_PASS must be set for email notifications",
|
||||
);
|
||||
}
|
||||
|
||||
const transport = nodemailer.createTransport({
|
||||
host,
|
||||
port,
|
||||
secure: false, // STARTTLS on port 587
|
||||
auth: { user, pass },
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForEmail.emailTransport = transport;
|
||||
}
|
||||
|
||||
return transport;
|
||||
}
|
||||
const BREVO_ENDPOINT = "https://api.brevo.com/v3/smtp/email";
|
||||
|
||||
/**
|
||||
* Send a single email via Brevo SMTP relay.
|
||||
* Send a single transactional email via Brevo REST API.
|
||||
*/
|
||||
export async function sendEmail(payload: EmailPayload): Promise<void> {
|
||||
const apiKey = process.env.BREVO_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("BREVO_API_KEY must be set for email notifications");
|
||||
}
|
||||
|
||||
const fromEmail =
|
||||
process.env.NOTIFICATION_FROM_EMAIL ?? "noreply@beletage.ro";
|
||||
const fromName = process.env.NOTIFICATION_FROM_NAME ?? "ArchiTools";
|
||||
|
||||
const transport = getTransport();
|
||||
const recipients = payload.to
|
||||
.split(",")
|
||||
.map((addr) => addr.trim())
|
||||
.filter(Boolean)
|
||||
.map((email) => ({ email }));
|
||||
|
||||
await transport.sendMail({
|
||||
from: `"${fromName}" <${fromEmail}>`,
|
||||
to: payload.to,
|
||||
subject: payload.subject,
|
||||
html: payload.html,
|
||||
if (recipients.length === 0) {
|
||||
throw new Error("sendEmail: no recipients");
|
||||
}
|
||||
|
||||
const res = await fetch(BREVO_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"content-type": "application/json",
|
||||
"api-key": apiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sender: { name: fromName, email: fromEmail },
|
||||
to: recipients,
|
||||
subject: payload.subject,
|
||||
htmlContent: payload.html,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const detail = await res.text().catch(() => "");
|
||||
throw new Error(`Brevo API ${res.status}: ${detail.slice(0, 300)}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user