feat(admin): add roles, view as and the admin users API
Build, Test & Push Auth Service / quality-check (push) Successful in 39s
Build, Test & Push Auth Service / docker (push) Successful in 1m20s

users.role (USER < AUDITOR < PLATFORM_ADMIN) goes into the access token as the active role. POST /api/auth/view-as lowers the own login and is kept per refresh token, never above the stored role. /api/admin/users lists, blocks and deletes users; every action is written to admin_audit. A blocked login answers 403 ACCOUNT_BLOCKED, only after the right password.
This commit is contained in:
lobo committed 2026-09-27 12:18:10 +02:00
1 parent 7dc2fb613e
commit 2ba0d99148
55 files changed
+1184 -43

No files matched your search

+19
View File
@@ -38,6 +38,15 @@ To Be Played is split into three repositories:
10 minutes, `typ=public-profile`) that Core accepts for the public library data only. Its `profileStats` claim
names the stat groups the user chose to show there (none by default). The ticket is not a login:
both services reject every token with a `typ` claim on normal endpoints.
- **Roles**: one per user, a ladder `USER < AUDITOR < PLATFORM_ADMIN` (`users.role`, the first admin is set by
SQL). The access token carries the active role in the `role` claim. **View as**: an admin switches the own login
down to USER or AUDITOR and back (`POST /api/auth/view-as`); the choice is stored per login in
`refresh_tokens.active_role` and never goes above the stored role, so a demoted admin loses the rights at the next
refresh. Core does not check roles yet.
- **Admin users page** (`/api/admin/**`, active role PLATFORM_ADMIN): search and page all users, block and unblock
(`users.enabled`; blocking ends every login, the login then answers `403 ACCOUNT_BLOCKED`, but only after the
right password), delete (the same flag and nightly cleanup as "Delete my account"). No action on the own account
or on another admin. Every action and every "view as" is a row in `admin_audit`.
- **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 public profile, no Steam
link, not in the landing stats. Email and display name stay taken; the Steam ID is freed, so an undo needs a
@@ -57,6 +66,10 @@ To Be Played is split into three repositories:
| `PUT` | `/api/users/settings` | access token |
| `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=&page=&size=` (`{content, page, size, totalElements, totalPages}`) | PLATFORM_ADMIN |
| `PUT` | `/api/admin/users/{id}/blocked` (`{blocked}`; `409 SELF_ACTION`, `TARGET_IS_ADMIN`, `ALREADY_DELETED`) | PLATFORM_ADMIN |
| `DELETE` | `/api/admin/users/{id}` (`409 SELF_ACTION`, `TARGET_IS_ADMIN`) | PLATFORM_ADMIN |
| `DELETE` | `/api/users/me` (`{password}`; `204` and both cookies cleared, `403 INVALID_PASSWORD`, `429`) | access token |
| `GET` | `/api/steam-link/callback` | public, Steam's redirect back; binding cookie; redirects to the frontend settings |
| `PUT` | `/api/users/settings/appearance` | access token |
@@ -91,6 +104,9 @@ layers:
`version` has only an `api` package: `GET /api/version` answers the commit and build time of the running image.
`account` (delete and nightly cleanup) sets the flag with `JdbcTemplate` in a `repo` package too, and calls Core
through `CoreAccountService`.
`admin` (users page, audit) works the same way. It deletes through the port `shared/account/AccountDeleter`, and
the login module writes "view as" to the audit table through `shared/audit/AdminAuditLog`, because modules never
use each other.
ArchUnit enforces these rules in `src/test/java/auth_service/architecture/ArchitectureTest.java`, so a layer
violation fails the build. It also keeps the modules independent of each other and free of cycles. Test base
@@ -113,6 +129,9 @@ sequenceDiagram
B->>A: POST /api/auth/refresh with tbp_refresh
A-->>B: New tbp_access and tbp_refresh, old refresh token rotated
B->>C: Same API call again
Note over B,A: View as (admin only)
B->>A: POST /api/auth/view-as {role} with tbp_refresh
A-->>B: New tbp_access (role claim) and tbp_refresh, active role stored for this login
```
1. `LoginService` checks the password. `LoginController` sets both HttpOnly cookies (`AuthCookies`), so no script