feat(registration): registration waits for the admin, no invite code
Build, Test & Push Auth Service / quality-check (push) Successful in 50s
Build, Test & Push Auth Service / docker (push) Successful in 1m20s

This commit is contained in:
lobo committed 2026-10-01 09:18:12 +02:00
1 parent 1008034e82
commit 39a4d5ae49
26 files changed
+261 -147

No files matched your search

+35 -11
View File
@@ -13,10 +13,10 @@ To Be Played is split into three repositories:
## Features
- Registration with an invitation code, login with email and password (BCrypt). Emails are stored trimmed and in
- Registration by 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 registrations per IP,
block for 15 minutes with `429 TOO_MANY_ATTEMPTS` and `Retry-After`. Unknown emails cost the same BCrypt time as a
- 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
wrong password, and every failed login gets the same error.
- JWT signed with **EdDSA (Ed25519)**. Auth holds the private key, Core only the public key, so Core can verify
tokens but never create them. Tokens carry a `kid` header, and `GET /.well-known/jwks.json` publishes the
@@ -74,9 +74,9 @@ To Be Played is split into three repositories:
and end time, type `DISMISS` or `ACCEPT`). The dashboard shows the running hints one at a time, newest first.
"Do not show again" on a `DISMISS` hint or "OK" on an `ACCEPT` hint is a row in `hint_done`, so the hint never
comes back for this user on any device. Creating and ending a hint are rows in `admin_audit`.
- **Mails** from the platform mailbox: "new user registered" to the admin (`APP_MAIL_ADMIN_TO`), and to the user
"your account is active" at the registration, "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
- **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
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".
- **Delete my account** with the password (5 wrong tries per 15 minutes, then `429`). It only sets
`users.deleted_at` and revokes every refresh token: from then on no login, no refresh, no access token, no public
@@ -260,7 +260,7 @@ sequenceDiagram
participant B as Browser (Frontend)
participant A as Auth
participant M as SMTP (Hostinger)
B->>A: register / admin blocks / delete account
B->>A: register / admin decides / admin blocks / delete account
A->>A: event + mail_outbox row (PENDING), one transaction
loop every minute (MailOutboxService)
A->>M: up to 20 due rows
@@ -269,6 +269,31 @@ sequenceDiagram
Note over A: after 10 tries FAILED and a warning in the log; SENT rows are removed after 30 days
```
### Registration flow
A registration is a request: the account is PENDING until a PLATFORM_ADMIN decides. While 20 wait, the
registration answers `409 PENDING_LIMIT_REACHED` and stores nothing. Two requests at the same moment can make it 21.
```mermaid
sequenceDiagram
participant U as New user
participant A as Auth
participant D as Admin
U->>A: POST /api/registration/new
A->>A: users.status = PENDING, two mail rows
A-->>D: mail "a new registration waits"
A-->>U: mail "we got your registration"
U->>A: login: 403 ACCOUNT_PENDING
D->>A: PUT /api/admin/users/{id}/decision {accepted}
alt accepted
A->>A: status = ACTIVE
A-->>U: mail "your account is active"
else denied
A->>A: status = DENIED, deleted_at = now (nightly cleanup after 30 days)
A-->>U: mail "your registration was denied"
end
```
### Steam ID and block sync to Core
Core keeps its own copy of the Steam ID and of the block. After a verified Steam link (also for the account that
@@ -307,8 +332,8 @@ other, then start them in this order (Core also starts without Auth and finds it
In Auth, `local` makes a new signing key at each start (`LocalKeysEnvironmentPostProcessor`, only in this
profile, and never together with `prod`), so no key is in the repo; after an Auth restart (also a devtools
restart), log in again. Never use `local` on a server: the dev user's password is public. The
Flyway callback in `db/local` adds the dev user (`PLATFORM_ADMIN`, linked Steam ID). The invite code for more
users is `local`. What Core mocks and how to change the recorded answers: Core readme, "Run locally".
Flyway callback in `db/local` adds the dev user (`PLATFORM_ADMIN`, linked Steam ID). A new local user waits until
the dev user accepts it on the admin page. What Core mocks and how to change the recorded answers: Core readme, "Run locally".
### With your own keys (profile `dev`)
@@ -332,14 +357,13 @@ Configuration comes from environment variables:
|---|---|
| `JWT_PRIVATE_KEY`, `JWT_PUBLIC_KEY` | Ed25519 keypair, Base64 of the DER bytes (PKCS#8 / X.509) |
| `JWT_KID` | Key id written into the token header |
| `AUTH_INVITATION_CODE` | Code required for registration |
| `DB_PASSWORD` | Local PostgreSQL password |
| `APP_PUBLIC_URL` | Auth's public address, the Steam OpenID realm (default `http://localhost:8080`) |
| `APP_FRONTEND_URL` | Where the Steam link ends (default `http://localhost:5173`) |
| `APP_CORE_INTERNAL_URL` | Core for the nightly account cleanup, required (local `http://localhost:8090`, Docker `http://tbp-core-service:8090`) |
| `SPRING_MAIL_HOST`, `SPRING_MAIL_USERNAME`, `SPRING_MAIL_PASSWORD` | SMTP server and its login (port 465, SSL). No host = mail off |
| `APP_MAIL_FROM` | Sender address, the mailbox of the SMTP login. Needed with a host |
| `APP_MAIL_ADMIN_TO` | Gets "new user registered". Needed with a host |
| `APP_MAIL_ADMIN_TO` | Gets "a new registration waits". Needed with a host |
Generate a keypair: