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:
Claude VM
2026-04-22 07:49:08 +03:00
parent 265e1c934b
commit 6b3d56e1e8
4 changed files with 181 additions and 226 deletions
+34 -42
View File
@@ -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)}`);
}
}