- runDataCleanup() replaces maskOverdueInvestors(): now also anonymizes
never-activated invites after 90 days, deletes sessions + magic links
older than 30 days, NULLs IPs in audit logs older than 30 days, and
redacts email from audit log details JSONB for masked investors
- New /api/admin/cleanup POST endpoint for scheduled invocation
- New .gitea/workflows/pitch-cleanup.yml: daily cron at 02:00 UTC calls
the cleanup endpoint so anonymization is genuinely automatic, not lazy
- Switch masking window from first_activity_at to last_login_at (30 days
of inactivity; resets on each login)
- Both auth pages: DSGVO footer now covers all Art. 13 requirements —
data categories, retention cutoffs, Art. 15–21 rights, contact address,
LfDI Baden-Württemberg as supervisory authority
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Investors who lost their session or whose invite token was already used
can now enter their email on /auth to receive a fresh access link,
without needing a manual re-invite from an admin.
- New /api/auth/request-link endpoint looks up the investor by email,
issues a new pitch_magic_links row, and emails the link via the
existing sendMagicLinkEmail path. Response is generic regardless of
whether the email exists (enumeration resistance) and silently no-ops
for revoked investors.
- Rate-limited both per-IP (authVerify preset) and per-email (magicLink
preset, 3/hour — same ceiling as admin-invite/resend).
- /auth page now renders an email form; submits to the new endpoint and
shows a generic "if invited, link sent" confirmation.
- Route-level tests cover validation, normalization, unknown email,
revoked investor, and both rate-limit paths.
- End-to-end regression test wires request-link + verify against an
in-memory fake DB and asserts the full flow: original invite used →
replay rejected → email submission → fresh link → verify succeeds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>