Compare commits

..
46 Commits
Author SHA1 Message Date
lobo af386bb8f7 feat(user): pick or turn off the dashboard banner
Build, Test & Push Auth Service / quality-check (push) Successful in 26s
Build, Test & Push Auth Service / docker (push) Successful in 1m26s
2026-09-24 15:35:48 +02:00
lobo e04fd27b08 feat(user): synced appearance settings 2026-09-24 15:06:16 +02:00
lobo c9ae7853e5 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.
2026-09-24 14:18:32 +02:00
lobo 779c4d296d test: share one Postgres container across test classes
The per-class container was stopped under the Spring context that the next
integration test class reused, so health answered 503 there.
2026-09-24 14:18:32 +02:00
lobo 2941e3f905 feat(user): optional display name (player tag)
Build, Test & Push Auth Service / quality-check (push) Successful in 37s
Build, Test & Push Auth Service / docker (push) Successful in 1m19s
users.display_name (V6), 3-24 letters, digits, space, _ . -, unique ignoring case. Set in settings (409 DISPLAY_NAME_ALREADY_IN_USE, also for the DB constraint race) and optionally at registration. Returned by /api/users/me.
2026-09-24 10:39:04 +02:00
lobo 4579b05d2c fix(cors): expose Retry-After so the login page can show the block time 2026-09-24 10:29:57 +02:00
lobo d28147b2ab fix(security): hide health details and close Prometheus to the public
/actuator/health showed database and disk details to anyone, and /actuator/prometheus was open. Only health stays public, as plain UP/DOWN for the Docker health check.
2026-09-24 10:26:53 +02:00
lobo 5512453774 fix(login): same error for every failed login, block disabled users, rate limit
Unknown email, wrong password and disabled account now all return 401 INVALID_CREDENTIALS; unknown emails still run bcrypt against a dummy hash so timing does not leak them. After 5 failures per email or 20 per client IP, login returns 429 with Retry-After for 15 minutes. LoginRequest is now validated.
2026-09-24 10:22:30 +02:00
lobo 3d21bb667d fix(db): remove the seeded test user from all environments
V1 inserted test@example.com / password123 everywhere, including prod. V5 deletes that row only when email and seed hash both match.
2026-09-24 10:14:58 +02:00
lobo fe691b0558 chore(deps): update Spring Boot to 4.1.1 and ArchUnit to 1.5.0
Build, Test & Push Auth Service / quality-check (push) Successful in 1m40s
Build, Test & Push Auth Service / docker (push) Successful in 1m33s
2026-09-24 08:56:45 +02:00
lobo a3aeb666a8 fix(user): send Steam ID as string and refresh token on settings save
JavaScript rounds 17-digit Steam IDs, so 76561198005679788 was stored as
76561198005679790. Steam IDs are now JSON strings (17 digits, numbers
rejected), and null means "no Steam ID" instead of 0 (V4 migrates old rows).

PUT /api/users/settings returns a new token, because Core reads the Steam ID
from the token. A Steam ID owned by another user returns 409.
2026-09-24 08:51:01 +02:00
Andreas Dahm 39f08c9563 fix(user): null-safe steamId comparison in updateSettings
Build, Test & Push Auth Service / quality-check (push) Successful in 37s
Build, Test & Push Auth Service / docker (push) Successful in 1m48s
2026-05-12 10:09:33 +02:00
Andreas Dahm 73dfeb4e7d Small fixed
Build, Test & Push Auth Service / quality-check (push) Successful in 38s
Build, Test & Push Auth Service / docker (push) Successful in 2m3s
2026-05-12 09:51:09 +02:00
lobo 3159736c16 Merge pull request 'Changed from JWT token to public/privat key' (#2) from feat/jwt-eddsa into main
Build, Test & Push Auth Service / quality-check (push) Failing after 38s
Build, Test & Push Auth Service / docker (push) Has been skipped
Reviewed-on: #2
2026-05-12 08:58:57 +02:00
Andreas Dahm 8505faa578 Changed from JWT token to public/privat key 2026-05-12 08:56:51 +02:00
Andreas DahmandClaude Opus 4.7 874f8dee58 Add Auth-owned product roadmap to README
Build, Test & Push Auth Service / quality-check (push) Failing after 37s
Build, Test & Push Auth Service / docker (push) Has been skipped
Captures the Auth slices of the cross-service product roadmap:
username + public-profile opt-in, friends graph, email/notification
preferences, invitation system enhancements, and GDPR delete/export.
Cross-links to the umbrella roadmap in ToBePlayed-Deployment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:51:24 +02:00
Andreas Dahm f30e089739 Changed readme.md
Build, Test & Push Auth Service / quality-check (push) Failing after 38s
Build, Test & Push Auth Service / docker (push) Has been skipped
2026-05-11 14:40:54 +02:00
Andreas Dahm 2beb9aaddf Fixed problems with update of settings also added public area
Build, Test & Push Auth Service / quality-check (push) Failing after 36s
Build, Test & Push Auth Service / docker (push) Has been skipped
2026-05-04 15:16:14 +02:00
Andreas Dahm 3fd12637d6 Extended pricipal to add steam id
Build, Test & Push Auth Service / quality-check (push) Successful in 38s
Build, Test & Push Auth Service / docker (push) Successful in 1m42s
2026-04-28 15:43:27 +02:00
Andreas Dahm 6b8aa4458c Added registration endpoint to whitelist
Build, Test & Push Auth Service / quality-check (push) Successful in 39s
Build, Test & Push Auth Service / docker (push) Successful in 1m43s
2026-04-24 10:22:15 +02:00
lobo c312d271ad Support now null as not initialized for SteamId
Build, Test & Push Auth Service / quality-check (push) Successful in 37s
Build, Test & Push Auth Service / docker (push) Successful in 1m50s
2026-04-19 14:59:27 +02:00
lobo dda7e50963 Support now null as not initialized for SteamId
Build, Test & Push Auth Service / quality-check (push) Successful in 37s
Build, Test & Push Auth Service / docker (push) Successful in 1m43s
2026-04-19 14:35:02 +02:00
lobo 7d32c7e04a Added new db migration to set default SteamId to
Build, Test & Push Auth Service / quality-check (push) Failing after 36s
Build, Test & Push Auth Service / docker (push) Has been skipped
2026-04-19 14:17:49 +02:00
lobo fd390ac072 Refactor package structure: domain -> modules
Build, Test & Push Auth Service / quality-check (push) Successful in 38s
Build, Test & Push Auth Service / docker (push) Successful in 1m41s
Move domain packages into a new modules.* hierarchy and rename controller packages to api.* across login, registration, and user modules. Update package declarations and imports (including shared.api DTOs and JwtService -> modules login TokenGenerator), adjust tests and persistence/mapper paths accordingly, and apply minor formatting fixes (missing EOF newlines). This reorganizes the project to better separate API surface and module boundaries.
2026-04-19 12:50:01 +02:00
lobo 815347325a Merge pull request 'feature-cleanup' (#1) from feature-cleanup into main
Build, Test & Push Auth Service / quality-check (push) Successful in 1m23s
Build, Test & Push Auth Service / docker (push) Successful in 1m57s
Reviewed-on: #1
2026-04-18 11:52:46 +02:00
lobo fabab0ca45 Fixed tests 2026-04-18 11:49:09 +02:00
lobo 0cd4d7328a Fixed tests 2026-04-18 11:47:58 +02:00
lobo d63fbd4155 Added new tests fixed some problem 2026-04-18 11:28:06 +02:00
lobo 3f80777e37 Fixed tests
Build, Test & Push Auth Service / quality-check (push) Successful in 21s
Build, Test & Push Auth Service / docker (push) Successful in 1m19s
2026-04-11 12:32:32 +02:00
lobo e21dea1e18 Added lots of new stuff
- extended the jwt and put userid into it
- completed update settings
- added settings also in me call
2026-04-11 12:07:27 +02:00
lobo d498ef8827 Added transactional
Build, Test & Push Auth Service / quality-check (push) Successful in 21s
Build, Test & Push Auth Service / docker (push) Successful in 1m15s
2026-04-06 09:57:39 +02:00
lobo ed98d703c6 changed default profile to prod
Build, Test & Push Auth Service / quality-check (push) Successful in 19s
Build, Test & Push Auth Service / docker (push) Successful in 1m18s
2026-04-06 09:35:31 +02:00
lobo 8465524849 Finished registration Process and added tests
Build, Test & Push Auth Service / quality-check (push) Successful in 24s
Build, Test & Push Auth Service / docker (push) Successful in 1m18s
2026-04-06 09:12:45 +02:00
lobo c224cbf679 Started with:
Build, Test & Push Auth Service / quality-check (push) Successful in 19s
Build, Test & Push Auth Service / docker (push) Successful in 1m21s
- Registration logic for new players
- Extended User table with more information
2026-04-04 17:11:13 +02:00
lobo dbc76e6158 Added tests for all services and controller
Build, Test & Push Auth Service / quality-check (push) Successful in 19s
Build, Test & Push Auth Service / docker (push) Successful in 1m18s
2026-04-04 16:01:05 +02:00
lobo f4b7ae75a9 Added arch unit tests
Build, Test & Push Auth Service / quality-check (push) Successful in 50s
Build, Test & Push Auth Service / docker (push) Successful in 1m15s
2026-04-04 11:22:26 +02:00
lobo 5aee23f00e Fixed problem with token response 2026-04-04 10:55:39 +02:00
lobo 71e4f38357 Cleaned a little bit up 2026-04-04 10:43:18 +02:00
lobo 41ce5d93e9 Implemented user service 2026-04-04 09:58:09 +02:00
lobo 8af7170c46 Disabled test for now until db config is ready for testing
Build, Test & Push Auth Service / quality-check (push) Successful in 15s
Build, Test & Push Auth Service / docker (push) Successful in 1m51s
2026-04-03 12:32:40 +02:00
lobo 64b84b1dc0 Added jwt token validation and api endpoint versioning
Build, Test & Push Auth Service / quality-check (push) Failing after 41s
Build, Test & Push Auth Service / docker (push) Has been skipped
2026-04-03 12:25:03 +02:00
lobo 2e8ff1b079 Cleaned up the architecture
Build, Test & Push Auth Service / quality-check (push) Failing after 30s
Build, Test & Push Auth Service / docker (push) Has been skipped
2026-04-03 11:35:53 +02:00
lobo be4d4df351 Updated pipeline
Build, Test & Push Auth Service / quality-check (push) Failing after 42s
Build, Test & Push Auth Service / docker (push) Has been skipped
2026-03-31 19:54:02 +02:00
lobo 85e4b62f32 Updated pipeline
Build, Test & Push Auth Service / quality-check (push) Failing after 3s
Build, Test & Push Auth Service / docker (push) Has been skipped
2026-03-31 19:52:09 +02:00
lobo 7e8a04d83c Login should work now, tests are missing
Build, Test & Push Auth Service / quality-check (push) Failing after 37s
Build, Test & Push Auth Service / docker (push) Has been skipped
2026-03-31 19:48:23 +02:00
lobo 8dd17f84f6 Setup of springboot 2026-03-31 18:57:06 +02:00
11 changed files with 10882 additions and 133 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# .env file for local development
DB_PASSWORD=my_super_secret_local_password
-8
View File
@@ -31,11 +31,3 @@ build/
### VS Code ### ### VS Code ###
.vscode/ .vscode/
### Local secrets and generated files ###
.env
effective-pom
### Local dev config, never committed ###
application-dev.yml
application-dev.yaml
+10713
View File
File diff suppressed because it is too large. Load diff
+119 -79
View File
@@ -1,110 +1,150 @@
# To Be Played – Auth Service # To Be Played (TBP) - Authentication Service
Identity service of **To Be Played**, a Steam backlog tracker. It handles registration, login, user settings and the This is the Authentication and User Management Service for the "To Be Played" project. It acts as the central identity provider, handling user authentication, JWT generation, and profile management for the TBP frontend.
public profile, and it signs the JWTs that the Core service trusts.
To Be Played is split into three repositories: ## 🚀 Tech Stack
| Repository | Role | * **Language:** Java 25
|---|---| * **Framework:** Spring Boot 4.1.x
| **ToBePlayed-Auth** (this one) | Users, login, settings, signs JWTs | * **Security:** Spring Security & JSON Web Tokens (JWT)
| ToBePlayed-Core | Steam library, backlog, wishlist, stats; verifies JWTs | * **Database:** PostgreSQL 17
| ToBePlayed-Frontend | React single-page app | * **Migrations:** Flyway
* **Observability:** Spring Boot Actuator & Micrometer (Prometheus)
* **Build Tool:** Maven Wrapper (`mvnw`)
* **CI/CD:** Gitea Actions (Forgejo)
## Features ## 🏗️ Architecture (Clean Architecture)
- Registration with an invitation code, login with email and password (BCrypt). The project strictly follows a Domain-Driven / Clean Architecture (Ports & Adapters) approach to ensure high maintainability and testability. The codebase is divided into three main layers to prevent framework lock-in and keep business logic isolated.
- 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 for key rotation.
- User settings: display name (unique, case-insensitive), Steam ID, language, dark mode.
- Appearance settings synced across devices: accent color, corner style, wide layout, background and dashboard
banner from the user's own game art. Only whitelisted presets and Steam app ids are stored, never raw CSS or URLs.
- Opt-in public profile at `/u/<display name>`. Auth answers with a short-lived **profile ticket** (EdDSA JWT,
10 minutes, `typ=public-profile`) that Core accepts for the public library data only. The ticket is not a login:
both services reject every token with a `typ` claim on normal endpoints.
## API ### Layer Breakdown
| Method | Path | Auth | 1. **API / Web Layer (`api`)**
|---|---|---| * Contains REST endpoints, DTOs (`Request`/`Response` records), and Global Exception Handlers.
| `POST` | `/api/registration/new` | public | * Uses `ApiMapper` to map between DTOs and Domain Models.
| `POST` | `/api/auth/login` | public | * **Rule:** This layer delegates to the Service layer and never accesses persistence directly.
| `GET` | `/api/users/me` | JWT |
| `PUT` | `/api/users/settings` | JWT |
| `PUT` | `/api/users/settings/appearance` | JWT |
| `GET` | `/api/public/profiles/{displayName}` | public |
| `GET` | `/api/stats/public` | public |
Endpoints use Spring's API versioning with a query parameter: `?version=1.0` (default `1.0`). 2. **Business / Core Layer (`model`, `service`, `exception`)**
* The heart of the application. Contains pure Java classes (Models) and Business Logic (Services).
* Throws specific business exceptions (e.g., `InvalidCredentialsException`).
* **Rule:** This layer has **no dependencies** on the API or Persistence layer. It defines interfaces that the persistence layer must implement.
## Tech stack 3. **Persistence Layer (`persistence`)**
* Handles database operations. Contains JPA Entities (`UserEntity`), Spring Data Repositories, and the `EntityMapper`.
* Implements the interfaces defined in the Domain layer.
* **Rule:** Entities never leak into the API layer.
- Java 25, Spring Boot 4.1 (Web MVC, Security, Data JPA, Validation, Cache with Caffeine), virtual threads Architectural boundaries are enforced via ArchUnit (`src/test/java/auth_service/architecture/ArchitectureTest.java`), which runs as part of the standard `./mvnw test` quality gate. Abstract test bases live under `auth_service.shared.test` (`AbstractUnitTest`, `AbstractIntegrationTest`, `AbstractPersistenceTest`, `AbstractWebMvcTest`, `AbstractArchitectureTest`).
- PostgreSQL 17, Flyway migrations
- JJWT with Ed25519 keys
- Spring Boot Actuator + Micrometer (Prometheus)
- Tests: JUnit 5, Testcontainers (PostgreSQL), ArchUnit
- CI: Forgejo Actions, multi-stage Docker image
## Architecture ## ⚙️ Local Development
Package by feature (`modules/login`, `modules/registration`, `modules/user`, ...), and inside each feature three Running the application locally is fully automated via Spring Boot's Docker Compose Support.
layers:
| Layer | Package | Rule | ### Prerequisites
|---|---|---|
| API | `api` | REST controllers, request/response records, `ApiMapper`. Calls services, never repositories. |
| Domain | `model`, `service`, `exception` | Plain Java models and business logic. No dependency on API or persistence. |
| Persistence | `persistence` | JPA entities, Spring Data repositories, `EntityMapper`. Entities never reach the API layer. |
ArchUnit enforces these rules in `src/test/java/auth_service/architecture/ArchitectureTest.java`, so a layer * Java 25 installed
violation fails the build. Test base classes (`AbstractUnitTest`, `AbstractWebMvcTest`, `AbstractPersistenceTest`, * Docker & Docker Compose running
`AbstractIntegrationTest`) live in `auth_service.shared.test`; all integration tests share one PostgreSQL container.
## Run locally ### How to start
Needs Java 25 and Docker. 1. Simply run the application from your IDE (e.g., IntelliJ) via the `AuthServiceApplication.java` main class.
2. Spring Boot will automatically detect the `compose.yaml` and spin up a local PostgreSQL container (`tbp_auth_db_local`) on port `5432`.
3. **Flyway** will automatically apply all database migrations located in `src/main/resources/db/migration`.
4. The application will be available at `http://localhost:8080`.
To stop the environment and clean the database volume (if needed):
```bash ```bash
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev docker compose down -v
``` ```
Spring Boot's Docker Compose support starts PostgreSQL from `compose.yaml`, Flyway applies the migrations, and the ## 🔐 API Versioning
service listens on `http://localhost:8080`. Health: `/actuator/health`, metrics: `/actuator/prometheus`.
Configuration comes from environment variables: All API endpoints utilize Query Parameter Versioning to keep URLs clean and manageable.
Example: GET /api/users/me?v=1
| Variable | Purpose | ## 🐳 CI/CD Pipeline
|---|---|
| `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 |
Generate a keypair: The project uses Gitea Actions for Continuous Integration and Deployment.
```bash Quality Gate: Runs ./mvnw clean verify to ensure tests and builds pass.
openssl genpkey -algorithm ed25519 -out private.pem
openssl pkey -in private.pem -outform DER | base64 -w0 # JWT_PRIVATE_KEY
openssl pkey -in private.pem -pubout -outform DER | base64 -w0 # JWT_PUBLIC_KEY
```
## Tests Docker Build: Packages the application using a multi-stage Dockerfile.
```bash Registry: Pushes the lightweight Alpine-based image to the private Gitea container registry (git.andreas-dahm.eu/lobo_corp/tbp-auth-service:latest).
./mvnw clean verify
```
Runs unit, WebMvc, persistence and integration tests plus the ArchUnit rules. Needs Docker for Testcontainers. ## 📈 Observability
## Roadmap Metrics and health checks are exposed out-of-the-box via Spring Boot Actuator:
- Rate limit `/api/auth/login` and `/api/registration/**`. Health: http://localhost:8080/actuator/health
- JWKS endpoint (`/.well-known/jwks.json`), so Core fetches the public key and keys rotate without a redeploy.
- Short-lived access token plus rotating refresh token in an httpOnly cookie, with CSRF protection. Today the token
lives 24 h in `localStorage` and cannot be revoked.
- Account deletion and data export.
## License Prometheus Metrics: http://localhost:8080/actuator/prometheus
All rights reserved. The source is public for reading only. ## 🗺️ Roadmap / Open Topics
Tracking known work that is not yet implemented. Each item is independently actionable.
### Security
- ~~**Switch JWT signing from HS256 → EdDSA (Ed25519).**~~ Done. Auth signs with an Ed25519 private key (`JWT_PRIVATE_KEY`), Core verifies with the matching public key (`JWT_PUBLIC_KEY`). `kid` header (`JWT_KID`) is emitted to support future rotation. See `JwtService`, `JwtConfig`, `application.yml`, `JwtServiceTest`, and `ToBePlayed-Deployment/README.md` for the rollout / rotation procedure.
- **Refresh-token rotation.** Current access tokens last 24h with no revocation path. Split into a short-lived (5–15 min) access token and a long-lived (7–30 day) refresh token, rotated on every use, persisted server-side so logout / password change / "log out everywhere" actually revoke. Add a `token_version` column on `users` (or a `jti` blacklist table) checked on each refresh.
- **Move JWT delivery to an httpOnly `Secure` cookie.** Tokens are currently returned in the response body and stored by the frontend in `localStorage`, exposing them to any XSS payload. Set the cookie server-side, scope it to `.tbp.andreas-dahm.eu`, and add CSRF protection (Spring Security's `CookieCsrfTokenRepository.withHttpOnlyFalse()`).
- **Tighten CORS for cookie auth.** Restrict allowed origins to `https://tbp.andreas-dahm.eu` (plus the local dev origin) and set `allowCredentials=true`. Required before switching to cookie-based JWT.
- **Rate-limit `/api/auth/login` and `/api/registration/**`.** Currently no brake on credential stuffing or registration spam. Bucket4j or Spring Security's built-in `RateLimiter` are both options; key by IP + email.
### Observability and API
- **Publish a JWKS endpoint** (`GET /.well-known/jwks.json`) once on EdDSA. Lets Core (and any future verifier) fetch the public key automatically and supports key rotation without redeploying every consumer.
- **API versioning review.** Query-parameter versioning (`?v=1`) is unusual; path- or `Accept`-header-based versioning is more conventional and plays better with HTTP caches. Worth a deliberate decision before more endpoints accumulate.
### Testing
- The abstract test bases under `auth_service.shared.test` already cover unit / integration / persistence / WebMvc slices. Continue adding controller-level `AbstractWebMvcTest` coverage for any new endpoint and persistence-layer tests for any new repository.
## 🎯 Product Roadmap (Auth-owned slices)
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~~ Done
- 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
Once usernames exist:
- Schema: `user_friendship(user_id, friend_id, status, requested_at, accepted_at)` with `status` in `PENDING / ACCEPTED / BLOCKED`. Symmetric pair or single-row with a canonical ordering — pick one and stick with it.
- Endpoints: `POST /api/users/me/friends?username=...` (request), `POST /api/users/me/friends/{id}/accept`, `DELETE /api/users/me/friends/{id}`, `GET /api/users/me/friends`.
- Optional: discovery via the Steam friends API (Auth holds the `steam_id`; the friends list call itself can live in Core's `steam_adapter`, with Auth resolving Steam IDs back to TBP users).
### 3. Email-driven retention
The weekly digest, sale alerts, and friend-request notifications all need email infrastructure:
- Spring's `JavaMailSender` + templating (Thymeleaf or plain interpolation). Configure SMTP via env vars (see Deployment roadmap).
- Per-user notification preferences: `notify_digest_weekly`, `notify_sale_alerts`, `notify_friend_requests` — all booleans on `users` with sensible defaults.
- Endpoint to render `/api/users/me/notification-preferences` and update via `PUT`.
- Triggered by either Core scheduled jobs (digest, sale alerts) calling an internal Auth endpoint, or by a shared message queue if/when one is introduced.
### 4. Invitation system enhancements
Today registration validates an `INVALID_INVITATION_CODE` — so invites already exist. Worth surfacing:
- Endpoint: `POST /api/users/me/invitations` to generate a single-use code (rate-limited, expires after N days). Used for friend invites and onboarding referrals.
- Endpoint: `GET /api/users/me/invitations` to list outstanding codes.
- Optional: tie invitation acceptance to an auto-friend-request so referred users are already connected on first login.
### 5. Account deletion / GDPR
- `DELETE /api/users/me` that soft-deletes (sets `deleted_at`, anonymises PII, keeps the row for foreign-key integrity), or hard-deletes with cascade depending on policy.
- Data export endpoint: `GET /api/users/me/export` returning a downloadable JSON bundle of everything Auth knows about the user. Core should expose an equivalent and the Frontend can stitch them.
- Document the data-retention policy here once decided.
+27
View File
@@ -0,0 +1,27 @@
# ─────────────────────────────────────────────
# DEVELOPMENT CONFIGURATION
# ─────────────────────────────────────────────
# Overrides properties from application.yml when the 'dev' profile is active
app:
cors:
# Local development: allow all origins to prevent CORS issues
allowed-origins: "*"
# Optional: Make SQL logs visible for debugging
spring:
jpa:
show-sql: true
security:
jwt:
# Dev-only Ed25519 keypair. DO NOT use in production — regenerate per-env with:
# openssl genpkey -algorithm Ed25519 -outform DER | base64 -w0
# openssl pkey -inform DER -in <(base64 -d <<< "$PRIV") -pubout -outform DER | base64 -w0
private-key: "MC4CAQAwBQYDK2VwBCIEIG9nxZFw9Fj7lGUqA7oi94oo/xollxwlZIZENlpfp+tc"
public-key: "MCowBQYDK2VwAyEAPSmUISg0IUO1LrRPhF8RZAT/Yjh9HEVf5hzR84AL1Zw="
kid: "tbp-auth-dev"
auth:
registration:
invitation-code: "Us*kksUuRpTE2CQRsv+^k0n)ksR}X}?#Fo2D4d8ZKG"
@@ -60,8 +60,8 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
var request = RegistrationRequest.builder() var request = RegistrationRequest.builder()
.email("test@example.com") .email("test@example.com")
.password("SuperSecret123!") .password("SuperSecret123!")
.inviteCode("test-invite-code") .inviteCode("Us*kksUuRpTE2CQRsv+^k0n)ksR}X}?#Fo2D4d8ZKG")
.steamId("76561198000000001") .steamId("76561198005679788")
.build(); .build();
long expectedUserId = 42L; long expectedUserId = 42L;
@@ -83,8 +83,8 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
var request = RegistrationRequest.builder() var request = RegistrationRequest.builder()
.email("test@example.com") .email("test@example.com")
.password("SuperSecret123!") .password("SuperSecret123!")
.inviteCode("test-invite-code") .inviteCode("Us*kksUuRpTE2CQRsv+^k0n)ksR}X}?#Fo2D4d8ZKG")
.steamId("76561198000000001") .steamId("76561198005679788")
.build(); .build();
when(registrationService.register(any(Registration.class))) when(registrationService.register(any(Registration.class)))
@@ -106,7 +106,7 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
.email("test@example.com") .email("test@example.com")
.password("SuperSecret123!") .password("SuperSecret123!")
.inviteCode("code") .inviteCode("code")
.steamId("76561198000000001") .steamId("76561198005679788")
.build(); .build();
when(registrationService.register(any(Registration.class))).thenReturn(42L); when(registrationService.register(any(Registration.class))).thenReturn(42L);
@@ -119,7 +119,7 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
// Assert // Assert
var captor = ArgumentCaptor.forClass(Registration.class); var captor = ArgumentCaptor.forClass(Registration.class);
verify(registrationService).register(captor.capture()); verify(registrationService).register(captor.capture());
assertEquals(76561198000000001L, captor.getValue().getSteamId()); assertEquals(76561198005679788L, captor.getValue().getSteamId());
} }
@Test @Test
@@ -42,8 +42,8 @@ class RegistrationServiceTest extends AbstractUnitTest {
validRegistration = Registration.builder() validRegistration = Registration.builder()
.email("test@example.com") .email("test@example.com")
.rawPassword("SuperSecret123!") .rawPassword("SuperSecret123!")
.inviteCode("test-invite-code") .inviteCode("Us*kksUuRpTE2CQRsv+^k0n)ksR}X}?#Fo2D4d8ZKG")
.steamId(76561198000000001L) .steamId(76561198005679788L)
.build(); .build();
} }
@@ -84,7 +84,7 @@ class UserControllerTest extends AbstractWebMvcTest {
// Arrange // Arrange
var email = "test@example.com"; var email = "test@example.com";
var settings = UserSettings.builder().language("en").theme("dark").build(); var settings = UserSettings.builder().language("en").theme("dark").build();
var user = User.builder().email(email).steamId(76561198000000001L).settings(settings).build(); var user = User.builder().email(email).steamId(76561198005679788L).settings(settings).build();
var principal = new UserPrincipal("1", email, null); var principal = new UserPrincipal("1", email, null);
SecurityContextHolder.getContext().setAuthentication( SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList()) new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
@@ -95,7 +95,7 @@ class UserControllerTest extends AbstractWebMvcTest {
mockMvc.perform(get("/api/users/me") mockMvc.perform(get("/api/users/me")
.accept(MediaType.APPLICATION_JSON)) .accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.steamId").value("76561198000000001")); .andExpect(jsonPath("$.steamId").value("76561198005679788"));
} }
@Test @Test
@@ -141,13 +141,13 @@ class UserControllerTest extends AbstractWebMvcTest {
SecurityContextHolder.getContext().setAuthentication( SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList()) new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
); );
when(userService.updateUserSettings(eq(1L), eq(email), eq(76561198000000001L), any(), any())).thenReturn("new-token"); when(userService.updateUserSettings(eq(1L), eq(email), eq(76561198005679788L), any(), any())).thenReturn("new-token");
// Act & Assert // Act & Assert
mockMvc.perform(put("/api/users/settings") mockMvc.perform(put("/api/users/settings")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(""" .content("""
{"useDarkTheme":true,"language":"en","steamId":"76561198000000001"}""")) {"useDarkTheme":true,"language":"en","steamId":"76561198005679788"}"""))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.token").value("new-token")); .andExpect(jsonPath("$.token").value("new-token"));
} }
@@ -197,14 +197,14 @@ class UserControllerTest extends AbstractWebMvcTest {
SecurityContextHolder.getContext().setAuthentication( SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList()) new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
); );
when(userService.updateUserSettings(eq(1L), eq(email), eq(76561198000000001L), any(), any())) when(userService.updateUserSettings(eq(1L), eq(email), eq(76561198005679788L), any(), any()))
.thenThrow(new DataIntegrityViolationException("duplicate key value violates unique constraint \"uc_steam_id\"")); .thenThrow(new DataIntegrityViolationException("duplicate key value violates unique constraint \"uc_steam_id\""));
// Act & Assert // Act & Assert
mockMvc.perform(put("/api/users/settings") mockMvc.perform(put("/api/users/settings")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(""" .content("""
{"useDarkTheme":true,"language":"en","steamId":"76561198000000001"}""")) {"useDarkTheme":true,"language":"en","steamId":"76561198005679788"}"""))
.andExpect(status().isConflict()) .andExpect(status().isConflict())
.andExpect(jsonPath("$.code").value("STEAM_ID_ALREADY_IN_USE")); .andExpect(jsonPath("$.code").value("STEAM_ID_ALREADY_IN_USE"));
} }
@@ -235,14 +235,14 @@ class UserControllerTest extends AbstractWebMvcTest {
SecurityContextHolder.getContext().setAuthentication( SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList()) new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
); );
when(userService.updateUserSettings(eq(1L), eq(email), eq(76561198000000001L), any(), any())) when(userService.updateUserSettings(eq(1L), eq(email), eq(76561198005679788L), any(), any()))
.thenThrow(new SteamIdAlreadyInUseException("taken")); .thenThrow(new SteamIdAlreadyInUseException("taken"));
// Act & Assert // Act & Assert
mockMvc.perform(put("/api/users/settings") mockMvc.perform(put("/api/users/settings")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(""" .content("""
{"useDarkTheme":true,"language":"en","steamId":"76561198000000001"}""")) {"useDarkTheme":true,"language":"en","steamId":"76561198005679788"}"""))
.andExpect(status().isConflict()) .andExpect(status().isConflict())
.andExpect(jsonPath("$.code").value("STEAM_ID_ALREADY_IN_USE")); .andExpect(jsonPath("$.code").value("STEAM_ID_ALREADY_IN_USE"));
} }
@@ -123,7 +123,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
.email("owner@example.com") .email("owner@example.com")
.password("password") .password("password")
.enabled(true) .enabled(true)
.steamId(76561198000000001L) .steamId(76561198005679788L)
.registrationDate(LocalDateTime.now()) .registrationDate(LocalDateTime.now())
.build()); .build());
var other = entityManager.persist(UserEntity.builder() var other = entityManager.persist(UserEntity.builder()
@@ -135,8 +135,8 @@ class UserRepositoryTest extends AbstractPersistenceTest {
entityManager.flush(); entityManager.flush();
// Act & Assert // Act & Assert
assertFalse(userRepository.isSteamIdUsedByOtherUser(owner.getId(), 76561198000000001L)); assertFalse(userRepository.isSteamIdUsedByOtherUser(owner.getId(), 76561198005679788L));
assertTrue(userRepository.isSteamIdUsedByOtherUser(other.getId(), 76561198000000001L)); assertTrue(userRepository.isSteamIdUsedByOtherUser(other.getId(), 76561198005679788L));
assertFalse(userRepository.isSteamIdUsedByOtherUser(other.getId(), 76561198000000000L)); assertFalse(userRepository.isSteamIdUsedByOtherUser(other.getId(), 76561198000000000L));
} }
@@ -64,7 +64,7 @@ class UserServiceTest extends AbstractUnitTest {
// Arrange // Arrange
long userId = 1L; long userId = 1L;
var email = "test@example.com"; var email = "test@example.com";
Long steamId = 76561198000000001L; Long steamId = 76561198005679788L;
var userSettings = UserSettings.builder().language("en").build(); var userSettings = UserSettings.builder().language("en").build();
when(userRepository.isSteamIdUsedByOtherUser(userId, steamId)).thenReturn(false); when(userRepository.isSteamIdUsedByOtherUser(userId, steamId)).thenReturn(false);
when(tokenGenerator.generateToken("1", email, steamId)).thenReturn("new-token"); when(tokenGenerator.generateToken("1", email, steamId)).thenReturn("new-token");
@@ -98,7 +98,7 @@ class UserServiceTest extends AbstractUnitTest {
void updateUserSettings_SteamIdOfOtherUser() { void updateUserSettings_SteamIdOfOtherUser() {
// Arrange // Arrange
long userId = 1L; long userId = 1L;
Long steamId = 76561198000000001L; Long steamId = 76561198005679788L;
var userSettings = UserSettings.builder().language("en").build(); var userSettings = UserSettings.builder().language("en").build();
when(userRepository.isSteamIdUsedByOtherUser(userId, steamId)).thenReturn(true); when(userRepository.isSteamIdUsedByOtherUser(userId, steamId)).thenReturn(true);
@@ -4,15 +4,8 @@ import org.junit.jupiter.api.Tag;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.containers.PostgreSQLContainer;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/** /**
* Base class for integration tests. * Base class for integration tests.
* Full Spring context with Testcontainers. * Full Spring context with Testcontainers.
@@ -30,23 +23,5 @@ public abstract class AbstractIntegrationTest {
static { static {
postgres.start(); postgres.start();
} }
// No key is committed: a fresh one per JVM.
private static final KeyPair KEYS = generateKeys();
@DynamicPropertySource
static void secrets(DynamicPropertyRegistry registry) {
registry.add("security.jwt.private-key", () -> Base64.getEncoder().encodeToString(KEYS.getPrivate().getEncoded()));
registry.add("security.jwt.public-key", () -> Base64.getEncoder().encodeToString(KEYS.getPublic().getEncoded()));
registry.add("auth.registration.invitation-code", () -> "test-invite-code");
}
private static KeyPair generateKeys() {
try {
return KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
} }