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

@@ -9,6 +9,14 @@ Auth: Bearer token (Sanctum)
- `POST /auth/login`
- `POST /auth/logout`
- `GET /auth/me`
- `POST /auth/forgot-password` — request password reset (public, rate-limited). Body: `{ email, app: "app"|"portal"|"admin" }`. Always returns 200 (no email enumeration).
- `POST /auth/reset-password` — reset password with token (public). Body: `{ token, email, password, password_confirmation }`.
## Account Management (authenticated)
- `POST /me/change-password` — change own password. Body: `{ current_password, password, password_confirmation }`. Revokes other sessions.
- `POST /me/change-email` — request email change (sends verification to new address). Body: `{ new_email, password, app: "app"|"portal"|"admin" }`.
- `POST /verify-email-change` — verify email change token (public). Body: `{ token }`. Revokes all sessions.
## Organisations
@@ -18,6 +26,7 @@ Auth: Bearer token (Sanctum)
- `PUT /organisations/{org}` — update
- `GET /organisations/{org}/members` — members
- `POST /organisations/{org}/invite` — invite user
- `POST /organisations/{org}/members/{user}/change-email` — admin initiates email change for a member (org_admin only). Body: `{ new_email }`.
## Events

View File

@@ -243,6 +243,26 @@ scopeFestivals() // WHERE event_type IN ('festival', 'series')
---
### `email_change_requests`
| Column | Type | Notes |
| ---------------------- | ------------ | --------------------------------- |
| `id` | ULID PK | |
| `user_id` | ULID FK | → users (cascade delete) |
| `current_email` | string | Email at time of request |
| `new_email` | string | Requested new email |
| `token` | string | SHA-256 hashed verification token |
| `requested_by_user_id` | ULID FK null | → users (null on delete) — self or admin |
| `status` | string | pending / verified / expired / cancelled |
| `expires_at` | timestamp | 24h from request |
| `verified_at` | timestamp? | When verification completed |
| `created_at` | timestamp | |
| `updated_at` | timestamp | |
**Indexes:** `(user_id, status)`, `(token)`
---
## 3.5.2 Locations
> Locations are event-scoped and reusable across sections within an event.