Public Access
feat(registration): add email confirmation flow
Registrations become two-phase: first UNCONFIRMED with a 24-hour single-use email link, then PENDING after confirmation and password entry. Prevents registering with someone else's address. Admins see unconfirmed requests and can delete them; they cannot accept them.
This commit is contained in:
1 parent
878ede914b
commit
0af76da9c7
43 files changed
+959
-145
No files matched your search
@@ -13,7 +13,8 @@ To Be Played is split into three repositories:
|
||||
|
||||
## Features
|
||||
|
||||
- Registration by admin approval, login with email and password (BCrypt). Emails are stored trimmed and in
|
||||
- Registration by email confirmation (a single-use link, valid 24 hours, on whose page the owner chooses the
|
||||
password; unconfirmed requests are deleted after 7 days) and then admin approval, login with email and password (BCrypt). Emails are stored trimmed and in
|
||||
lower case (V19, unique on `lower(email)`); a password has 8 characters and at most 72 bytes (BCrypt's limit).
|
||||
- Brute-force protection in memory: 5 failed logins per email or 20 per IP, and 10 failed or 3 taken registrations
|
||||
per IP, block for 15 minutes with `429 TOO_MANY_ATTEMPTS` and `Retry-After`. Unknown emails cost the same BCrypt time as a
|
||||
@@ -83,7 +84,7 @@ To Be Played is split into three repositories:
|
||||
set it (`PUT /api/users/settings/welcome-seen`). V24 sets it for every existing user, so only users registered after
|
||||
the deploy get the page.
|
||||
- **Mails** from the platform mailbox: "a new registration waits" to the admin (`APP_MAIL_ADMIN_TO`, with the link
|
||||
to the admin page), and to the user "we got your registration", then "your account is active" or "your
|
||||
to the admin page), and to the user "confirm your address" (`CONFIRM_EMAIL`, sent first; the admin mail follows the click), then "your account is active" or "your
|
||||
registration was denied", later "your account is blocked" and "your account will be deleted from <date>". The registration sends the browser language (`de` for a German browser, else `en`), which also becomes
|
||||
the user's app language. Plain text only. See "Mail flow".
|
||||
- **Broadcast mail**: a PLATFORM_ADMIN writes one mail with subject and text in English and German (plain text),
|
||||
@@ -105,6 +106,8 @@ To Be Played is split into three repositories:
|
||||
| Method | Path | Auth |
|
||||
|---|---|---|
|
||||
| `POST` | `/api/registration/new` | public |
|
||||
| `POST` | `/api/registration/confirm` (`{token, password}`; `204`; `410 CONFIRMATION_INVALID`) | public |
|
||||
| `POST` | `/api/registration/resend` (`{email}`; always `204`) | public |
|
||||
| `POST` | `/api/auth/login` | public, sets both cookies |
|
||||
| `POST` | `/api/auth/refresh` | refresh cookie, replaces both cookies |
|
||||
| `POST` | `/api/auth/logout` | refresh cookie, clears both cookies |
|
||||
@@ -113,11 +116,12 @@ To Be Played is split into three repositories:
|
||||
| `POST` | `/api/users/steam-link` | access token, returns Steam's login URL and sets the binding cookie |
|
||||
| `DELETE` | `/api/users/steam-link` | access token, removes the Steam ID and sets a new access cookie |
|
||||
| `POST` | `/api/auth/view-as` (`{role}`, null = own role; new cookies; `403 ROLE_NOT_ALLOWED` above the own role) | refresh cookie |
|
||||
| `GET` | `/api/admin/users?query=&status=&page=&size=` (`{content, page, size, totalElements, totalPages}`; `status` PENDING, ACTIVE or DENIED, empty for all; each user has `status`) | PLATFORM_ADMIN |
|
||||
| `GET` | `/api/admin/users?query=&status=&page=&size=` (`{content, page, size, totalElements, totalPages}`; `status` UNCONFIRMED, PENDING, ACTIVE or DENIED, empty for all; each user has `status`) | PLATFORM_ADMIN |
|
||||
| `GET` | `/api/admin/users/pending-count` (`{count}`: registrations that wait) | PLATFORM_ADMIN |
|
||||
| `PUT` | `/api/admin/users/{id}/decision` (`{accepted}`; `204`; `409 NOT_PENDING`) | PLATFORM_ADMIN |
|
||||
| `PUT` | `/api/admin/users/{id}/decision` (`{accepted}`; `204`; `409 NOT_PENDING`, also for an unconfirmed request) | PLATFORM_ADMIN |
|
||||
| `PUT` | `/api/admin/users/{id}/blocked` (`{blocked}`; `409 SELF_ACTION`, `TARGET_IS_ADMIN`, `ALREADY_DELETED`, `TARGET_IS_PENDING`) | PLATFORM_ADMIN |
|
||||
| `DELETE` | `/api/admin/users/{id}` (`409 SELF_ACTION`, `TARGET_IS_ADMIN`, `TARGET_IS_PENDING`) | PLATFORM_ADMIN |
|
||||
| `DELETE` | `/api/admin/users/{id}` (`409 SELF_ACTION`, `TARGET_IS_ADMIN`, `TARGET_IS_PENDING`, `TARGET_IS_UNCONFIRMED`) | PLATFORM_ADMIN |
|
||||
| `DELETE` | `/api/admin/users/{id}/unconfirmed` (hard delete of an unconfirmed request; `204`; `404`; `409 NOT_UNCONFIRMED`, `SELF_ACTION`, `TARGET_IS_ADMIN`) | PLATFORM_ADMIN |
|
||||
| `GET` | `/api/hints` (`{results}`: running hints not closed for good, newest first) | access token |
|
||||
| `PUT` | `/api/hints/{id}/done` (`204`; `404 HINT_NOT_FOUND`) | access token |
|
||||
| `GET` | `/api/admin/hints` (`{results}` with `status` PLANNED/ACTIVE/ENDED by the server clock and `doneCount`, newest first) | PLATFORM_ADMIN |
|
||||
@@ -304,7 +308,14 @@ sequenceDiagram
|
||||
|
||||
### Registration flow
|
||||
|
||||
A registration is a request: the account is PENDING until a PLATFORM_ADMIN decides. While 20 wait, the
|
||||
A registration is a request: the account is UNCONFIRMED until the address owner opens the link in the mail (24 hours,
|
||||
single use, only its SHA-256 is stored; a resend replaces it) and chooses the password there, then PENDING until a
|
||||
PLATFORM_ADMIN decides. The registration form has no password: an unconfirmed user has none and cannot log in (login
|
||||
answers the same `401` as for an unknown address), so nobody can set the password of an address that is not theirs.
|
||||
The admin list shows an unconfirmed request as such; it cannot be accepted, the admin can delete it for good. The badge counts PENDING only and the admin mail follows the confirmation. The nightly cleanup deletes it after 7 days (not while a resent link is still valid), also in dry run.
|
||||
Registering again with an unconfirmed address is a resend: a new link, no second row. At most 3 confirmation mails per
|
||||
address and hour (over all IPs, registration and resend together); over that the answer stays the same and no mail is
|
||||
sent. Unconfirmed requests count toward the 20 that wait. While 20 wait, the
|
||||
registration answers `409 PENDING_LIMIT_REACHED` and stores nothing. Two requests at the same moment can make it 21.
|
||||
|
||||
```mermaid
|
||||
@@ -313,10 +324,13 @@ sequenceDiagram
|
||||
participant A as Auth
|
||||
participant D as Admin
|
||||
U->>A: POST /api/registration/new
|
||||
A->>A: users.status = PENDING, two mail rows
|
||||
A->>A: users.status = UNCONFIRMED, one mail row
|
||||
A-->>U: mail "confirm your address" (link)
|
||||
U->>A: POST /api/registration/confirm {token, password}
|
||||
A->>A: users.status = PENDING, password hash stored
|
||||
A-->>D: mail "a new registration waits"
|
||||
A-->>U: mail "we got your registration"
|
||||
U->>A: login: 403 ACCOUNT_PENDING
|
||||
Note over A: nightly: UNCONFIRMED older than 7 days is deleted
|
||||
D->>A: PUT /api/admin/users/{id}/decision {accepted}
|
||||
alt accepted
|
||||
A->>A: status = ACTIVE
|
||||
|
||||
Reference in new issue
Block a user