feat(profile): opt-in public profile with a ticket for Core

user_settings.public_profile (off by default, needs a display name).
GET /api/public/profiles/{displayName} answers only for enabled users with a
public profile and returns a 10-minute EdDSA ticket for Core. A token with a
typ claim is no login, so the ticket cannot be used as one.
This commit is contained in:
lobo committed 2026-09-24 14:18:32 +02:00
1 parent 07ccb489ec
commit 973b980e05
21 files changed
+353 -12

No files matched your search

+10 -6
View File
@@ -106,13 +106,17 @@ Tracking known work that is not yet implemented. Each item is independently acti
The full cross-service product roadmap lives in `ToBePlayed-Deployment/README.md`. This section lists only the slices that land in Auth.
### 1. Username + public-profile opt-in
### 1. ~~Username + public-profile opt-in~~ Done
The public profile feature (`tbp.andreas-dahm.eu/u/<username>`) requires:
- **Unique, human-readable username** distinct from email. Schema: add `username` column on `users` with a unique index and a Flyway migration. Validation: 3–30 chars, `[a-z0-9_-]`, case-insensitive uniqueness.
- **`profile_public` flag** (boolean, default `false`) on `users`. Exposed via `PUT /api/users/settings`.
- New endpoint: `GET /api/public/users/{username}` returning `{ username, displayName, steamId?, joinedAt }` — the public-facing identity contract that Core's profile endpoint can compose with.
- Display name (3–24 chars, unique case-insensitive) is the public handle; `/u/<display name>` in the frontend.
- `user_settings.public_profile` (default `false`), set via `PUT /api/users/settings` (`publicProfile`). A public
profile needs a display name (400 otherwise).
- `GET /api/public/profiles/{displayName}` → `{displayName, memberSince, ticket}` for enabled users with a public
profile; everything else is the same 404 `PROFILE_NOT_FOUND`.
- The `ticket` is an EdDSA JWT (`typ=public-profile`, `profileUserId`, **valid 10 minutes**) for Core's
`GET /api/public/profiles/library` (header `X-Profile-Ticket`). It is no login: `JwtService.isTokenValid` rejects
every token with a `typ` claim, in Auth and in Core. Accepted trade-off: after a user makes the profile private, a
ticket issued before still reads the public data for up to 10 minutes; Core stores no copy of the flag.
### 2. Friends / social graph