Files
kontakt-api/server.js
T
kerimatilganandClaude Opus 4.8 a617846aa9 Kontaktformular-Dienst (Node + nodemailer), self-hosted für Coolify
- POST /api/kontakt: validiert, Honeypot-Spamschutz, Rate-Limit (5/10min)
- Versand per SMTP über eigenes Postfach, keine Speicherung/DB
- CORS auf die eigene Website beschränkt (ALLOWED_ORIGIN)
- Konfiguration komplett über Environment-Variablen (.env.example)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:13:38 +02:00

109 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Kontaktformular-Dienst für atilgan-gartenbau.de
// Nimmt das Formular per POST entgegen und verschickt es als E-Mail über das
// eigene Postfach (SMTP). Speichert nichts – die Daten verlassen den eigenen
// Server nur als E-Mail an den Betreiber.
import express from 'express';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import nodemailer from 'nodemailer';
const {
PORT = 3000,
ALLOWED_ORIGIN = '', // z. B. "https://atilgan-gartenbau.de,https://www.atilgan-gartenbau.de"
SMTP_HOST,
SMTP_PORT = 587,
SMTP_SECURE = 'false', // "true" für Port 465
SMTP_USER,
SMTP_PASS,
MAIL_TO = 'info@adnan-atilgan.de',
MAIL_FROM, // Absender; wenn leer, wird SMTP_USER genutzt
MAIL_SUBJECT_PREFIX = '[Webseite] ',
} = process.env;
const app = express();
app.set('trust proxy', 1); // hinter Coolify/Reverse-Proxy korrekte IP
app.use(express.json({ limit: '32kb' }));
// --- CORS: nur die eigene Website darf das Formular abschicken ---
const allowList = ALLOWED_ORIGIN.split(',').map((s) => s.trim()).filter(Boolean);
app.use(
cors({
origin(origin, cb) {
// Anfragen ohne Origin (z. B. Health-Checks) erlauben
if (!origin || allowList.length === 0 || allowList.includes(origin)) return cb(null, true);
return cb(new Error('Origin nicht erlaubt'));
},
methods: ['POST', 'OPTIONS'],
})
);
// --- Rate-Limit gegen Spam/Missbrauch ---
const limiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10 Minuten
max: 5, // max. 5 Nachrichten pro IP und Fenster
standardHeaders: true,
legacyHeaders: false,
message: { ok: false, error: 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.' },
});
// --- SMTP-Transport ---
const transporter = nodemailer.createTransport({
host: SMTP_HOST,
port: Number(SMTP_PORT),
secure: String(SMTP_SECURE) === 'true',
auth: SMTP_USER ? { user: SMTP_USER, pass: SMTP_PASS } : undefined,
});
const isEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v || '');
const clean = (v) => String(v || '').trim().slice(0, 5000);
// --- Health-Check ---
app.get('/', (_req, res) => res.json({ ok: true, service: 'kontakt-api' }));
app.get('/health', (_req, res) => res.json({ ok: true }));
// --- Formular-Endpunkt ---
app.post('/api/kontakt', limiter, async (req, res) => {
try {
const b = req.body || {};
// Honeypot: echtes Formular lässt dieses Feld leer; Bots füllen es aus
if (clean(b.website)) return res.json({ ok: true }); // still & unauffällig „erfolgreich"
const name = clean(b.name);
const email = clean(b.email);
const telefon = clean(b.telefon);
const betreff = clean(b.betreff) || 'Anfrage über die Webseite';
const nachricht = clean(b.nachricht);
if (!name || !nachricht || !isEmail(email)) {
return res.status(400).json({ ok: false, error: 'Bitte Name, gültige E-Mail und Nachricht angeben.' });
}
const text =
`Neue Nachricht über das Kontaktformular\n\n` +
`Name: ${name}\n` +
`E-Mail: ${email}\n` +
`Telefon: ${telefon || '-'}\n` +
`Betreff: ${betreff}\n\n` +
`Nachricht:\n${nachricht}\n`;
await transporter.sendMail({
from: MAIL_FROM || SMTP_USER,
to: MAIL_TO,
replyTo: `${name} <${email}>`,
subject: `${MAIL_SUBJECT_PREFIX}${betreff}`,
text,
});
return res.json({ ok: true });
} catch (err) {
console.error('Mailversand fehlgeschlagen:', err.message);
return res.status(500).json({ ok: false, error: 'Nachricht konnte nicht gesendet werden.' });
}
});
app.listen(PORT, () => {
console.log(`kontakt-api läuft auf Port ${PORT}`);
});