Add stage 7: email verification magic links and a profile page
Backend - New Mail namespace: a Mailer interface with SMTP (phpmailer), PHP mail() (the default fallback), and log-to-file transports, selected by MAIL_TRANSPORT. EmailVerifier issues a hashed, 15-minute magic-link token and sends the link (APP_URL/verify-email?token=...). - Migration 005: email_verifications table + users.verification_email_sent_at. - Registration now emails a verification link (best effort — a send failure doesn't fail registration). - POST /api/auth/verify-email consumes a token and returns a session, so opening the link verifies the address (or applies a pending email change) and logs the user in. Single-use; distinct 400s for invalid/used/expired. - POST /api/email/verification resends; POST /api/email/change requests a deferred change (current password required; link goes to the new address; users.email only updates when that link is opened). Both throttled to once per 60s, returning 429 + retry_after. - GET /api/me and every session payload now include pending_email. Shared SessionPayload builds the user/session JSON for all entry points. Frontend - /verify-email view: posts the token, adopts the returned session, redirects. - /profile view: shows address + status, a resend button with a live cooldown (driven by retry_after / 429), and a change-email form (new address + current password) that surfaces the pending change. - Header shows a "verify email" badge linking to the profile. Tests: 9 new (EmailVerificationTest) covering the link lifecycle, throttle, and deferred change; AuthTest folded into ApiTestCase, which now routes mail to a per-test log. Suite: 32 passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+21
-1
@@ -4,10 +4,17 @@ declare(strict_types=1);
|
||||
|
||||
use App\Auth\AuthMiddleware;
|
||||
use App\Auth\JwtService;
|
||||
use App\Auth\SessionPayload;
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\EmailVerificationController;
|
||||
use App\Http\Controllers\TodoItemController;
|
||||
use App\Http\Controllers\TodoListController;
|
||||
use App\Http\JsonErrorHandler;
|
||||
use App\Mail\EmailVerifier;
|
||||
use App\Mail\LogMailer;
|
||||
use App\Mail\Mailer;
|
||||
use App\Mail\PhpMailerMailer;
|
||||
use App\Repository\EmailVerificationRepository;
|
||||
use App\Repository\TodoItemRepository;
|
||||
use App\Repository\TodoListRepository;
|
||||
use App\Repository\UserRepository;
|
||||
@@ -36,9 +43,18 @@ $errorMiddleware->setDefaultErrorHandler(
|
||||
$users = new UserRepository($database->pdo());
|
||||
$todoLists = new TodoListRepository($database->pdo());
|
||||
$todoItems = new TodoItemRepository($database->pdo());
|
||||
$verificationTokens = new EmailVerificationRepository($database->pdo());
|
||||
$jwt = new JwtService($config->jwtSecret, $config->jwtTtl);
|
||||
$session = new SessionPayload($jwt, $verificationTokens);
|
||||
|
||||
$authController = new AuthController($users, $jwt);
|
||||
/** @var Mailer $mailer */
|
||||
$mailer = $config->mail->transport === 'log'
|
||||
? new LogMailer($config->mail->logPath)
|
||||
: new PhpMailerMailer($config->mail);
|
||||
$verifier = new EmailVerifier($verificationTokens, $users, $mailer, $config->appUrl);
|
||||
|
||||
$authController = new AuthController($users, $session, $verifier);
|
||||
$emailController = new EmailVerificationController($users, $verificationTokens, $verifier, $session);
|
||||
$listController = new TodoListController($todoLists);
|
||||
$itemController = new TodoItemController($todoLists, $todoItems);
|
||||
$authMiddleware = new AuthMiddleware($jwt, $users);
|
||||
@@ -47,6 +63,7 @@ $authMiddleware = new AuthMiddleware($jwt, $users);
|
||||
|
||||
$app->group('/api', function (RouteCollectorProxy $group) use (
|
||||
$authController,
|
||||
$emailController,
|
||||
$listController,
|
||||
$itemController,
|
||||
$authMiddleware,
|
||||
@@ -58,8 +75,11 @@ $app->group('/api', function (RouteCollectorProxy $group) use (
|
||||
|
||||
$group->post('/auth/register', [$authController, 'register']);
|
||||
$group->post('/auth/login', [$authController, 'login']);
|
||||
$group->post('/auth/verify-email', [$emailController, 'verify']);
|
||||
|
||||
$group->get('/me', [$authController, 'me'])->add($authMiddleware);
|
||||
$group->post('/email/verification', [$emailController, 'resend'])->add($authMiddleware);
|
||||
$group->post('/email/change', [$emailController, 'requestChange'])->add($authMiddleware);
|
||||
|
||||
$group->group('/lists', function (RouteCollectorProxy $lists) use ($listController, $itemController) {
|
||||
$lists->get('', [$listController, 'index']);
|
||||
|
||||
Reference in New Issue
Block a user