Add rate limiting, sliding sessions, email verification, GDPR self-service, order cancellation/returns, and critical-error alerting

Complements Payload's per-account login lockout with per-IP rate limiting
on auth routes; proxy.ts silently refreshes an active customer's session
via Payload's built-in refresh-token endpoint instead of a long-lived
token. Registration now sends a non-blocking email-verification link
(doesn't gate login, since checkout registers and immediately logs in
mid-purchase). /konto/profil gets GDPR export/delete; order detail pages
get self-service cancel/return-request, backed by a Payload hook that
closes a real gap (a customer's JWT could previously PATCH any field of
their own order, not just status). Checkout failures now email an alert
independent of Payload's own health, since Kuma's uptime checks can't see
an order silently failing to persist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-22 07:28:01 +00:00
parent 7f37f111e8
commit df05ea5358
22 changed files with 822 additions and 20 deletions
+53
View File
@@ -0,0 +1,53 @@
import nodemailer from "nodemailer";
// Server-only. Deliberately its own SMTP transport, independent of
// Payload's (which now also sends mail — see Customers.ts's verification
// email) — the whole point of this module is alerting when something is
// broken, and if Payload itself is what's broken, routing the alert
// through it could mean the alert never arrives. Same Hostinger account
// either way (already proven working via Diun's update notifications),
// just a second, separate connection to it.
const transport = nodemailer.createTransport({
host: "smtp.hostinger.com",
port: 587,
secure: false,
auth: {
user: process.env.SMTP_USER || "",
pass: process.env.SMTP_PASSWORD || "",
},
});
// Fire-and-forget by design — callers should not await this in a way that
// blocks or fails the actual error response the customer sees. Wrap
// everything in its own try/catch so a broken mail relay never becomes a
// second, worse failure on top of the one being reported.
export function sendCriticalAlert(subject: string, details: Record<string, unknown>): void {
transport
.sendMail({
from: '"einfach produktiv Alerts" <admin@mk360.de>',
to: "admin@mk360.de",
subject: `[einfach produktiv] ${subject}`,
text: JSON.stringify(details, null, 2),
})
.catch((err) => {
console.error("sendCriticalAlert: failed to send alert email", err);
});
}
// The *initial* verification email (on registration) is sent by Payload
// itself, via Customers.ts's own afterChange hook — that one fires
// automatically on create and needs no separate wiring. This one is only
// for the "erneut senden" resend path (app/api/account/resend-verification/
// route.ts), which updates the token via the customer's own session
// (app/lib/customerAuth.ts's resendVerificationEmail) but has no Payload
// hook to piggyback on for a plain update, so it sends directly instead —
// same Hostinger transport as the alert above, just a different template.
export async function sendVerificationEmail(to: string, firstName: string, token: string): Promise<void> {
const url = `https://einfach-produktiv.mk360.de/api/account/verify-email?token=${token}`;
await transport.sendMail({
from: '"einfach produktiv" <admin@mk360.de>',
to,
subject: "Bitte bestätige deine E-Mail-Adresse",
html: `<p>Hallo ${firstName},</p><p>bitte bestätige deine E-Mail-Adresse für dein Konto bei einfach produktiv:</p><p><a href="${url}">${url}</a></p><p>Der Link ist 24 Stunden gültig.</p>`,
});
}