Files
kontakt-api/server.js
T

117 lines
4.0 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';
import dns from 'node:dns';
// Container haben oft keine IPv6-Route. Strato (smtp.strato.de) löst sonst auf
// eine IPv6-Adresse auf -> SMTP-Verbindung läuft in den Timeout. IPv4 bevorzugen:
dns.setDefaultResultOrder('ipv4first');
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,
connectionTimeout: 10000, // schnell abbrechen statt hängen
greetingTimeout: 10000,
socketTimeout: 15000,
});
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', '/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}`);
});