// 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}`); });