feat: password reset, email change with verification, and password change

Password reset: multi-app support with custom notification linking to correct
frontend (app/portal/admin). Email change: self-service with password
confirmation and admin-initiated, both sending verification to new address
with 24h expiry. Confirmation sent to old email on completion. Password
change: authenticated endpoint revoking other sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 15:38:54 +02:00
parent 53100d4f6d
commit 836cffa232
42 changed files with 2643 additions and 67 deletions

View File

@@ -29,6 +29,8 @@ use App\Http\Controllers\Api\V1\VolunteerAvailabilityController;
use App\Http\Controllers\Api\V1\VolunteerRegistrationController;
use App\Http\Controllers\Api\V1\PublicRegistrationDataController;
use App\Http\Controllers\Api\V1\PortalTokenController;
use App\Http\Controllers\Api\V1\AccountController;
use App\Http\Controllers\Api\V1\EmailChangeController;
use App\Http\Controllers\Api\V1\PasswordResetController;
use App\Http\Controllers\Api\V1\PortalMeController;
use App\Http\Controllers\Api\V1\Portal\PortalShiftController;
@@ -66,6 +68,9 @@ Route::post('auth/forgot-password', [PasswordResetController::class, 'sendResetL
->middleware('throttle:5,1');
Route::post('auth/reset-password', [PasswordResetController::class, 'resetPassword']);
// Email change verification (public — token from email link)
Route::post('verify-email-change', [EmailChangeController::class, 'verify']);
// Public portal routes
Route::get('public/events/{slug}/registration-data', PublicRegistrationDataController::class);
Route::post('public/check-email', CheckEmailController::class)->middleware('throttle:10,1');
@@ -78,6 +83,10 @@ Route::middleware('auth:sanctum')->group(function () {
Route::get('auth/me', MeController::class);
Route::post('auth/logout', LogoutController::class);
// Account management (self-service)
Route::post('me/change-password', [AccountController::class, 'changePassword']);
Route::post('me/change-email', [EmailChangeController::class, 'request']);
// Portal (authenticated)
Route::get('portal/me', [PortalMeController::class, 'index']);
Route::put('portal/profile', [PortalMeController::class, 'updateProfile']);
@@ -148,6 +157,7 @@ Route::middleware('auth:sanctum')->group(function () {
Route::get('members', [MemberController::class, 'index']);
Route::put('members/{user}', [MemberController::class, 'update']);
Route::delete('members/{user}', [MemberController::class, 'destroy']);
Route::post('members/{user}/change-email', [MemberController::class, 'changeEmail']);
// Event sub-resources (all nested under organisation prefix — A01-13)
Route::prefix('events/{event}')->group(function () {