import { Router, type IRouter } from "express";
import rateLimit from "express-rate-limit";
import { BrevoClient } from "@getbrevo/brevo";
import { SendContactMessageBody } from "@workspace/api-zod";
const router: IRouter = Router();
const contactRateLimit = rateLimit({
windowMs: 60 * 60 * 1000,
limit: 5,
standardHeaders: "draft-8",
legacyHeaders: false,
message: {
success: false,
message:
"Zu viele Anfragen. Bitte versuchen Sie es in einer Stunde erneut.",
},
});
function getBrevoClient() {
const apiKey = process.env.BREVO_API_KEY;
if (!apiKey) throw new Error("BREVO_API_KEY is not set");
return new BrevoClient({ apiKey });
}
router.post("/contact", contactRateLimit, async (req, res) => {
const parsed = SendContactMessageBody.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({
success: false,
message: "Ungültige Eingabe. Bitte überprüfen Sie Ihre Angaben.",
});
return;
}
const { name, email, subject, message } = parsed.data;
try {
const brevo = getBrevoClient();
await brevo.transactionalEmails.sendTransacEmail({
sender: { name: "Kontaktformular Portfolio", email: "jh@unixweb.de" },
to: [{ email: "jh@unixweb.de", name: "Joachim Hummel" }],
replyTo: { email, name },
subject: subject ? `[Portfolio] ${subject}` : `[Portfolio] Neue Anfrage von ${name}`,
textContent: [
`Name: ${name}`,
`E-Mail: ${email}`,
subject ? `Betreff: ${subject}` : "",
"",
message,
]
.filter((l) => l !== undefined)
.join("\n"),
htmlContent: buildNotificationEmail({ name, email, subject, message }),
});
req.log.info({ to: "jh@unixweb.de", from: email }, "Contact message sent via Brevo");
await brevo.transactionalEmails.sendTransacEmail({
sender: { name: "Joachim Hummel", email: "jh@unixweb.de" },
to: [{ email, name }],
subject: "Ihre Anfrage ist angekommen – vielen Dank!",
textContent: [
`Hallo ${name},`,
"",
"vielen Dank für Ihre Nachricht! Ich habe Ihre Anfrage erhalten und melde mich in der Regel innerhalb von 1–2 Werktagen bei Ihnen.",
"",
"Mit freundlichen Grüßen",
"Joachim Hummel",
"",
"—",
"jh@unixweb.de",
].join("\n"),
htmlContent: buildConfirmationEmail(name),
});
req.log.info({ to: email }, "Confirmation email sent to sender via Brevo");
res.json({ success: true, message: "Ihre Nachricht wurde erfolgreich gesendet." });
} catch (err) {
req.log.error({ err }, "Failed to send contact email via Brevo");
res.status(500).json({
success: false,
message:
"Die Nachricht konnte nicht gesendet werden. Bitte versuchen Sie es später erneut.",
});
}
});
function escapeHtml(text: string): string {
return text
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function buildNotificationEmail({
name,
email,
subject,
message,
}: {
name: string;
email: string;
subject?: string;
message: string;
}): string {
const safeName = escapeHtml(name);
const safeEmail = escapeHtml(email);
const safeSubject = subject ? escapeHtml(subject) : null;
const safeMessage = escapeHtml(message);
const brandBlue = "#3f4ff4";
const textDark = "#0f172a";
const textMid = "#334155";
const textMuted = "#64748b";
const textLight = "#94a3b8";
const borderColor = "#e2e8f0";
const bgPage = "#f1f5f9";
const bgCard = "#ffffff";
const bgField = "#f8fafc";
return `
Neue Kontaktanfrage
|
Joachim Hummel
|
|
Neue Kontaktanfrage
Jemand hat das Kontaktformular ausgefüllt.
|
Name
${safeName}
|
|
E-Mail
${safeEmail}
|
${
safeSubject
? `
|
Betreff
${safeSubject}
|
`
: ""
}
|
Nachricht
${safeMessage}
|
|
|
|
|
|
|
Diese E-Mail wurde automatisch durch das Kontaktformular generiert.
|
|
`;
}
function buildConfirmationEmail(name: string): string {
const safeName = escapeHtml(name);
const brandBlue = "#3f4ff4";
const textDark = "#0f172a";
const textMid = "#334155";
const textMuted = "#64748b";
const textLight = "#94a3b8";
const borderColor = "#e2e8f0";
const bgPage = "#f1f5f9";
const bgCard = "#ffffff";
return `
Anfrage erhalten
|
Joachim Hummel
|
|
Vielen Dank, ${safeName}!
Ihre Anfrage ist eingegangen.
Schön, dass Sie sich gemeldet haben. Ich habe Ihre Nachricht erhalten und werde mich in der Regel innerhalb von 1–2 Werktagen bei Ihnen melden.
Bis dahin können Sie gerne mein Portfolio besuchen oder mir direkt eine E-Mail schicken.
|
|
|
|
|
|
Diese E-Mail wurde automatisch versendet – bitte antworten Sie nicht direkt darauf.
|
|
`;
}
export default router;