From 9ad4c4b1bdf37a5321e9575da982866f300b1ee2 Mon Sep 17 00:00:00 2001 From: lobo Date: Fri, 25 Sep 2026 08:22:56 +0200 Subject: [PATCH] docs: rewrite the readme for the public showcase Also ignore local secrets. --- .gitignore | 3 + readme.md | 220 ++++++++++++++++++++--------------------------------- 2 files changed, 85 insertions(+), 138 deletions(-) diff --git a/.gitignore b/.gitignore index 667aaef..52d8a0c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ build/ ### VS Code ### .vscode/ + +### Local secrets ### +.env diff --git a/readme.md b/readme.md index 6bfa806..e4c2430 100644 --- a/readme.md +++ b/readme.md @@ -1,169 +1,113 @@ -# To Be Played (TBP) - Core Service +# To Be Played โ€“ Core Service -This is the Core Service for the "To Be Played" project. It acts as the central modulith, handling all business logic in vertical micro-service ready slices. +Business service of **To Be Played**, a Steam backlog tracker. It syncs the user's Steam library, stores the backlog +(status, rating, notes, priority), tracks playtime per day, syncs the wishlist and adds time-to-beat data from IGDB. -## ๐Ÿš€ Tech Stack +To Be Played is split into three repositories: -* **Language:** Java 25 -* **Framework:** Spring Boot 4.1.x -* **Security:** Spring Security & JSON Web Tokens (JWT) -* **Database:** PostgreSQL 17 -* **Migrations:** Flyway -* **Observability:** Spring Boot Actuator & Micrometer (Prometheus) -* **Build Tool:** Maven Wrapper (`mvnw`) -* **CI/CD:** Gitea Actions (Forgejo) +| Repository | Role | +|---|---| +| ToBePlayed-Auth | Users, login, settings, signs JWTs | +| **ToBePlayed-Core** (this one) | Steam library, backlog, wishlist, stats; verifies JWTs | +| ToBePlayed-Frontend | React single-page app | -## ๐Ÿ—๏ธ Architecture (Clean Architecture) +## Features -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. +- **Steam sync**: owned games, playtime and achievements through the Steam Web API. Manual sync plus a nightly + sync. Resilience4j retries and rate-limits every Steam call; Caffeine caches game counts and player summaries. +- **Backlog**: status (`BACKLOG`, `PLAYING`, `FINISHED`, `COMPLETED` = 100 %, `DROPPED`), rating 1โ€“5, notes, priority, + filters and sorting. "Pile of Shame" summary with a random suggestion of what to play. +- **Playtime history**: one row per game and day, for the activity view and the "most played in the last two weeks" + dashboard banner. +- **Wishlist**: synced from Steam with store price and discount. +- **Time to beat**: story, extras and completionist hours from the IGDB API. Optional: without credentials the + feature is off and the rest works. +- **Public profile data**: totals, status counts, games in progress and finished games with rating. Readable only + with a profile ticket issued by Auth. Never returns notes, priority or the Steam ID. -### Layer Breakdown +## Authentication -1. **API / Web Layer (`api`)** - * Contains REST endpoints, DTOs (`Request`/`Response` records), and Global Exception Handlers. - * Uses `ApiMapper` to map between DTOs and Domain Models. - * **Rule:** This layer delegates to the Service layer and never accesses persistence directly. +Core never issues tokens. It verifies Auth's EdDSA (Ed25519) JWTs with the public key only (`JWT_PUBLIC_KEY`) and +reads `userId` and `steamId` from the claims. A token with a `typ` claim (for example a profile ticket) is rejected +on normal endpoints; the profile ticket is accepted only by `GET /api/public/profiles/library` in the +`X-Profile-Ticket` header. -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. +## API -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. +| Method | Path | Auth | +|---|---|---| +| `GET` | `/api/library/games`, `/api/library/games/{appid}` | JWT | +| `PUT` | `/api/library/games/{appid}/backlog` | JWT | +| `POST` | `/api/library/sync` | JWT | +| `GET` | `/api/library/sync-status`, `/summary`, `/activity`, `/wishlist`, `/steam-profile`, `/games-count` | JWT | +| `GET` | `/api/public/profiles/library` | profile ticket | +| `GET` | `/api/stats/public` | public | -Architectural boundaries are enforced via ArchUnit (`src/test/java/core_service/architecture/ArchitectureTest.java`), which runs as part of the standard `./mvnw test` quality gate. Abstract test bases live under `core_service.shared.test` (`AbstractUnitTest`, `AbstractIntegrationTest`, `AbstractPersistenceTest`, `AbstractWebMvcTest`, `AbstractArchitectureTest`). +## Tech stack -> **Core-specific structure note.** Unlike `auth-service`, Core keeps JPA `@Entity` classes in `..domain..` (rather than a separate `..persistence.entities..` package) and repository interfaces in `..repo..`. The ArchUnit rules are adapted to this layout. See "Roadmap / Open Topics" below for the planned tightening. +- Java 25, Spring Boot 4.1 (Web MVC, Security, Data JPA, Validation, Cache with Caffeine), virtual threads +- PostgreSQL 17, Flyway migrations +- Resilience4j: retry and rate limiter for Steam, rate limiter for IGDB +- JJWT with Ed25519 public key +- Spring Boot Actuator + Micrometer (Prometheus) +- Tests: JUnit 5, Testcontainers (PostgreSQL), ArchUnit +- CI: Forgejo Actions, multi-stage Docker image -## โš™๏ธ Local Development +## Architecture -Running the application locally is fully automated via Spring Boot's Docker Compose Support. +A modular monolith. Each module under `core_service.modules` is a vertical slice that could move to its own +service later: -### Prerequisites +| Module | Content | +|---|---| +| `library` | Sync, backlog, playtime history, wishlist, public profile data | +| `stats` | Public numbers for the landing page | +| `steam_adapter` | Steam Web API client | +| `igdb_adapter` | IGDB client (Twitch OAuth) | -* Java 25 installed -* Docker & Docker Compose running +Inside a module: `api` (controllers, DTOs), `service` (business logic), `domain` (JPA entities) and `repo` +(Spring Data repositories). ArchUnit enforces cycle freedom, controller-to-repository direction, naming and module +isolation in `src/test/java/core_service/architecture/ArchitectureTest.java`. -### How to start +## Run locally -1. Simply run the application from your IDE (e.g., IntelliJ) via the `CoreServiceApplication.java` main class. -2. Spring Boot will automatically detect the `compose.yaml` and spin up a local PostgreSQL container (`tbp_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:8090`. - -To stop the environment and clean the database volume (if needed): +Needs Java 25 and Docker. ```bash -docker compose down -v +./mvnw spring-boot:run -Dspring-boot.run.profiles=dev ``` -## ๐Ÿ” API Versioning +Spring Boot's Docker Compose support starts PostgreSQL from `compose.yaml`, Flyway applies the migrations, and the +service listens on `http://localhost:8090`. Health: `/actuator/health`, metrics: `/actuator/prometheus`. -All API endpoints utilize Query Parameter Versioning to keep URLs clean and manageable. -Example: GET /api/users/me?v=1 +In the `dev` profile a request without an `Authorization` header gets a dummy user, so the API is usable without a +running Auth service. -## ๐Ÿณ CI/CD Pipeline +Configuration comes from environment variables: -The project uses Gitea Actions for Continuous Integration and Deployment. +| Variable | Purpose | +|---|---| +| `JWT_PUBLIC_KEY` | Auth's Ed25519 public key, Base64 of the X.509 DER bytes | +| `STEAM_API_KEY` | Steam Web API key | +| `IGDB_CLIENT_ID`, `IGDB_CLIENT_SECRET` | Twitch app for IGDB (optional) | +| `DB_PASSWORD` | Local PostgreSQL password | -Quality Gate: Runs ./mvnw clean verify to ensure tests and builds pass. +## Tests -Docker Build: Packages the application using a multi-stage Dockerfile. +```bash +./mvnw clean verify +``` -Registry: Pushes the lightweight Alpine-based image to the private Gitea container registry (git.andreas-dahm.eu/lobo_corp/tbp-core-service:latest). +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: +- "What to play next" picker: time budget and mood on top of the current random suggestion. +- Achievement summary per game and library-wide, "closest to 100 %". +- Sale alerts for wishlist games. +- Move `steam_adapter` DTOs out of `api`, then turn on strict module independence in ArchUnit. +- Fetch the public key from a JWKS endpoint in Auth instead of an environment variable. -Health: http://localhost:8090/actuator/health +## License -Prometheus Metrics: http://localhost:8090/actuator/prometheus - -## ๐Ÿ—บ๏ธ Roadmap / Open Topics - -Tracking known work that is not yet implemented. Each item is independently actionable. - -### Architecture / ArchUnit follow-ups - -Currently active rules cover: cycles, controllerโ†’repository direction, repository naming, controller/service annotations, shared/configuration isolation, and a relaxed slice rule. The following rules are intentionally **not** yet enforced because they would fail today; addressing them is a separate refactor. - -- **Strict module independence.** `library` currently imports from `steam_adapter` (e.g. `SteamApiService`, response DTOs). The ArchUnit rule exempts that edge via `ignoreDependency(library.., steam_adapter..)`. Long-term: either move `steam_adapter` out of `modules` (into `core_service.adapters.steam` or `core_service.shared.steam`) to make its role as a shared outbound port explicit, then enable strict `notDependOnEachOther()` without exemptions. -- **"DTOs only in `..api..`" rule.** Steam adapter response records currently live in `..api.dto..` and are consumed by `library` services. Move them to `core_service.modules.steam_adapter.model` (or `.port`) so they read as domain types, not API DTOs, then re-enable the strict rule. -- **"Entities only in `..persistence..`" rule.** Skipped because Core mixes entities with the domain package. Decide whether to (a) accept that pattern and keep the rule off, or (b) split entities into `..repo.entity..` and re-enable the rule for consistency with `auth-service`. - -### Security - -- ~~**Drop the shared `JWT_SECRET`.**~~ Done. Core now holds only `JWT_PUBLIC_KEY` and verifies with the Ed25519 public key via `Jwts.parser().verifyWith(publicKey).build()`. `JwtConfig.secret` removed. -- ~~**Add a `JwtServiceTest`.**~~ Done. `src/test/java/core_service/configuration/security/JwtServiceTest.java` covers extract / valid / wrong-key paths with an Ed25519 fixture. -- **Consume a JWKS endpoint** once Auth exposes one (`GET https://auth.tbp.andreas-dahm.eu/.well-known/jwks.json`). Cache keys at startup, refresh on `kid` miss. Avoids redeploying Core when keys rotate. With the `kid` header now present in issued tokens (`JWT_KID`), this is unblocked. - -### Testing - -- **First Steam adapter integration test.** `SteamApiServiceTest` is a unit test; an integration test with WireMock (or Spring's `MockRestServiceServer`) would lock in the contract with the external Steam API and the Resilience4j retry/rate-limit configuration. -- **Persistence-layer tests.** `library/repo/*Repository` has no `AbstractPersistenceTest`-based test today. Good first targets: `UserGameRepository`, `UserSteamSyncRepository`. -- **Controller WebMvc tests.** `LibraryController`, `PublicStatsController`, `SteamController` have no `AbstractWebMvcTest`-based tests yet. The Auth project's `*ControllerTest` classes are the pattern to follow. - -### CI - -- **Wire the ArchUnit + scaffolding into the existing Forgejo Actions quality gate.** `./mvnw verify` already runs them; confirm CI fails (not just warns) on architecture violations so drift is caught before merge. - -## ๐ŸŽฏ Product Roadmap (Core-owned slices) - -The full cross-service product roadmap lives in `ToBePlayed-Deployment/README.md`. This section lists only the slices that land in Core, in suggested execution order. - -### 1. Per-game tracking โ€” the namesake feature - -Today `user_game` mirrors Steam ownership and playtime, nothing else. Closing this gap turns the library from a Steam mirror into an actual backlog tracker. - -- Add a `status` column on `user_game` (`BACKLOG / PLAYING / COMPLETED / DROPPED / ON_HOLD / WISHLIST`) with a Flyway migration. Default at import: playtime > 0 โ†’ `PLAYING`, otherwise `BACKLOG`. -- Add `rating` (smallint 1โ€“10, nullable), `completed_at` (timestamp, nullable), `notes` (text, markdown, server-side rendered or sanitised on read). -- Endpoint: `PATCH /api/library/games/{appid}` for partial updates of these fields. Stick with query-parameter versioning (`?v=1`). -- Endpoint: filter `GET /api/library/games?status=BACKLOG` (extend the existing pagination contract). -- New module candidate or extend `library` โ€” keep it in `library` until tracking grows its own queries / reports, then split. - -### 2. Picker / "what to play next" - -- Endpoint: `GET /api/library/picker?budget=&mood=&count=3`. Pure read, uses status + (eventually) HLTB hours + tags as filters. Randomised within filter set. -- No new persistence yet; build it on top of (1). - -### 3. HowLongToBeat integration - -- New module `core_service.modules.hltb` (or `library/hltb` if kept internal). External adapter pattern mirrors `steam_adapter`. -- Schema: `game_completion_time(appid, main_hours, plus_sides_hours, completionist_hours, fetched_at)`. -- Scheduled job to backfill + refresh (low cadence: monthly). -- Risk: HLTB has no official API; community libs scrape. Cache aggressively, degrade gracefully when missing. - -### 4. Achievement aggregation (data already in place) - -- `UserGameAchievement` + `GameAchievementSchema` are already synced. Add aggregate endpoints: - - `GET /api/library/games/{appid}/achievements/summary` โ†’ `{ unlocked, total, percent }`. - - `GET /api/library/me/achievements/summary` โ†’ library-wide unlocked / total / percent. - - `GET /api/library/me/achievements/closest-to-completion?limit=10`. -- Pure SQL views or projection DTOs; no new external calls. - -### 5. Goals and digests - -- Schema: `user_goal(user_id, year, target_games_completed, ...)`. -- Endpoint: `GET/PUT /api/library/me/goal?v=1`. -- Scheduled weekly digest job: aggregate per-user "played hours, backlog delta, status changes" for last 7 days. Push to Auth (which owns email) or to a notification queue. - -### 6. Wishlist + sale alerts - -- Extend `steam_adapter` with a wishlist endpoint. -- New module / adapter for IsThereAnyDeal price polling. -- Scheduled job; threshold per-user; trigger an email via Auth. - -### 7. Library enrichment - -- Genre / tag filters: Steam store tags exist in some Steam API responses โ€” verify which calls return them and add to the cache. -- DLC awareness: Steam's `GetAppDetails` returns DLC; persist a `game_dlc(appid_base, appid_dlc)` table and a `GET .../games/{appid}/dlc` endpoint. -- Series / franchise grouping: needs IGDB integration (separate adapter, requires a free IGDB OAuth token). -- **Multi-platform support**: bigger schema change. Decouple `Game` (platform-agnostic) from `SteamGame` (platform-specific). New `source` enum (`STEAM`, `GOG`, `EPIC`, `MANUAL`, ...). Defer until at least one non-Steam adapter is on the immediate horizon. - -### 8. ~~Public profile data~~ Done - -- `GET /api/public/profiles/library` with header `X-Profile-Ticket` (issued by Auth for a public profile, valid 10 minutes, see the Auth readme) returns totals, status counts, up to 8 playing and 12 finished games with rating. Never notes, priority or the Steam ID. Core keeps no copy of the privacy flag; the ticket is the proof. \ No newline at end of file +All rights reserved. The source is public for reading only.