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>
This commit is contained in:
commit
a617846aa9
6 files changed
+994
No files matched your search
@@ -0,0 +1,108 @@
|
||||
// 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}`);
|
||||
});
|
||||
Reference in new issue
Block a user