Add stage 8: passwordless magic-link login

Backend
- POST /api/auth/magic-link (public): emails a one-time login link for an
  address. Always 202 with the same body so accounts can't be enumerated; a
  link is sent only when the account exists and wasn't emailed in the last
  60s. Opening it (existing verify-email endpoint) returns a session and, as a
  side effect, verifies the address. New EmailVerifier::sendLoginLink; the
  60s interval is now EmailVerifier::RESEND_INTERVAL_SECONDS, shared.

Frontend
- LoginView defaults to magic-link mode: email only, "Log in with email". A
  "Log in with password" link reveals the password field, changes the button
  to "Log in", and itself becomes "Get a magic link" to switch back.
- VerifyEmailView copy is now login-neutral ("Signing you in").

Tests: 5 new (magic-link login, implicit verification, enumeration-safety,
throttle, validation). Suite: 37 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 10:29:13 +01:00
co-authored by Claude Sonnet 5
parent 64ba21795b
commit be592f38fc
10 changed files with 195 additions and 21 deletions
+1
View File
@@ -75,6 +75,7 @@ $app->group('/api', function (RouteCollectorProxy $group) use (
$group->post('/auth/register', [$authController, 'register']);
$group->post('/auth/login', [$authController, 'login']);
$group->post('/auth/magic-link', [$authController, 'requestLoginLink']);
$group->post('/auth/verify-email', [$emailController, 'verify']);
$group->get('/me', [$authController, 'me'])->add($authMiddleware);