# LudosData A personal video game library: catalogue what you own, what you've dumped, played and finished. Originally built in 2018 on Angular 5 + PHP + MySQL. Rebuilt in 2026 on **Angular 22** and **ASP.NET Core 10** with **SQLite**, running in Docker. --- ## Quick start ```bash cp .env.example .env # Generate a signing key and put it in .env as JWT_KEY: openssl rand -base64 48 # Also set SEED_USERNAME / SEED_EMAIL / SEED_PASSWORD for the first account. docker compose up --build ``` Then open and sign in with the seed credentials. On first run the API creates that account and imports the **105 games** recovered from the 2018 database dump. Seeding only happens while the database has no users. > **Password rules:** 12+ characters, with an uppercase, a lowercase and a digit. > The API refuses to start if `JWT_KEY` is missing or shorter than 32 characters — > that is deliberate, so a misconfigured deployment fails loudly instead of > signing tokens with a guessable key. --- ## Layout ``` backend/ ASP.NET Core 10 Web API (C#) src/LudosData.Api/ Domain/ Game, AppUser Data/ DbContext, migrations, seeder, games.json Auth/ JWT options, token service Controllers/ auth, games, images Services/ image storage frontend/ Angular 22 SPA src/app/ core/ models, services, guard, HTTP interceptor features/ login, register, game-grid, game-edit, account shared/ toolbar, confirm dialog archive/ the original 2018 MySQL dump, for provenance ``` Everything stateful lives in one Docker volume (`ludos-data`): the SQLite file, uploaded box art, and the Data Protection keys. Back that volume up and you have backed up the whole application. --- ## Development Host tooling (Node 24, .NET 10) is installed via Homebrew. `dotnet-ef` needs `~/.dotnet/tools` on `PATH`, which `~/.bashrc.d/dotnet.sh` sets up. ```bash # API on http://localhost:5099 cd backend/src/LudosData.Api Jwt__Key="a-dev-key-of-at-least-32-characters!!" dotnet run # SPA on http://localhost:4200, proxying /api and /uploads to :5099 cd frontend npm start ``` ```bash cd frontend && npm test # vitest cd backend && dotnet build # 0 warnings expected ``` ### Cover art `tools/cover-art/fetch_art.py` fills in box art from [libretro-thumbnails](https://thumbnails.libretro.com), matching on title + system and pushing each image through the app's own `POST /api/images`, so it gets the same validation and WebP re-encoding as a manual upload. Standard library only — no virtualenv needed. ```bash cd tools/cover-art python3 fetch_art.py --password '...' --dry-run # report matches, change nothing python3 fetch_art.py --password '...' # download and attach python3 fetch_art.py --password '...' --overwrite # also replace existing art ``` Always dry-run first; it prints every match with a similarity score and flags anything below 0.95 for eyeballing. Matching handles the gaps between a personal catalogue and a ROM-naming one: accents (`Pokemon` → `Pokémon`), roman numerals (our SNES `Final Fantasy 2` is the catalogue's `Final Fantasy II`), trailing articles (`Sims 2, The`), missing subtitles in either direction, and outright typos — `Brett Hull Hocky 95` finds `Brett Hull Hockey 95`. A sequel guard stops `Donkey Kong Country 2` from silently taking `Donkey Kong Country`'s box. **Xbox 360 is not covered** — libretro has no thumbnail set for it, so those 10 titles are reported as unsupported. They need a source such as IGDB, which requires a free Twitch developer client ID and secret. Art is publisher copyright. Fetching it for a private collection is ordinary practice for library software; redistributing it is a different question. ### Database changes ```bash cd backend/src/LudosData.Api dotnet ef migrations add --output-dir Data/Migrations ``` Migrations are applied automatically at startup. --- ## API All `/api/games` and `/api/images` routes require `Authorization: Bearer `. | Method | Route | Notes | | --- | --- | --- | | `POST` | `/api/auth/register` | Returns a token; signs the new user straight in | | `POST` | `/api/auth/login` | Returns `{ token, expiresAt, user }` | | `GET` | `/api/auth/me` | Current user | | `GET` | `/api/auth/available?userName=` / `?email=` | Returns only a boolean | | `GET` | `/api/games` | `search, system, genre, own, dumped, played, finished, page, pageSize, sort, dir` | | `GET` | `/api/games/{id}` | | | `GET` | `/api/games/facets` | Distinct systems and genres, for filter dropdowns | | `POST` | `/api/games` | | | `PUT` | `/api/games/{id}` | | | `DELETE` | `/api/games/{id}` | | | `POST` | `/api/images` | multipart `file`; re-encodes to WebP | | `GET` | `/health` | Anonymous | **Ownership is always taken from the JWT subject, never from the request.** A game belonging to another user returns `404`, not `403`, so the response does not confirm that the id exists. --- ## Security notes ### Rotate the old database password The 2018 code committed live MySQL credentials to this repository (`interfaceServices/dbConfig.php`, and again in four other files). They are in git history. **That password must be considered compromised and rotated**, regardless of this rewrite. Removing the files does not remove them from history. The new stack keeps secrets in `.env`, which is gitignored. ### What was fixed in the rewrite The old backend was ~16,700 lines of PHP, of which ~16,400 were vendored third-party code — four near-identical copies of `php-crud-api` plus `class.upload.php`. Only ~150 lines were application logic. These problems were not carried across: | Old behaviour | Now | | --- | --- | | Two endpoints exposed unauthenticated CRUD over every table | Every data route requires a valid token | | Client chose whose rows to read (`filter[]=userId,eq,N`) | Owner comes from the JWT subject, server-side | | Login hardcoded to a single username | Any registered user can sign in | | `crypt()` with one global salt, silently truncating passwords to 8 chars | ASP.NET Core Identity (PBKDF2, per-user salt) | | JWT secret was the literal string `"testing"`, tokens never expired | Key required from config, 12-hour expiry | | Token passed in the query string | `Authorization: Bearer` header | | Uploads anonymous, path built from the client filename | Authenticated, server-generated name, per-user folder, must decode as an image | | `Access-Control-Allow-Origin: *` | Explicit origin allowlist | Passwords could not be migrated — the old hashes are unrecoverable by design. ### Known accepted risk `npm audit` reports a moderate advisory in `@hono/node-server`, reached transitively through `@angular/cli`'s MCP server feature. It is: - **dev-only** — `npm audit --omit=dev` reports 0 vulnerabilities, and it is not in the browser bundle - a **Windows-only** path traversal, on a Linux-only toolchain here `npm audit fix --force` would downgrade Angular CLI to 21.0.4, a breaking change. Overriding the dependency means forcing a major bump the MCP SDK does not accept (`^1.19.9`). Left as-is deliberately; revisit when Angular CLI updates the SDK. --- ## Notable version facts (as of 2026-08) - **Angular 22.1** is **zoneless** — there is no `zone.js` in the dependency tree. Component state must be signal-based for change detection to see it. - **`@angular/animations` is deprecated in v22**; Material 22 no longer depends on it. There is no `provideAnimations()` in `app.config.ts`, and a test pins that Material still renders without one. - Unit tests run on **Vitest**, not Karma/Jasmine. - Fonts and Material icons are bundled from `node_modules`, so the app makes no third-party requests at runtime. - The backend pins two transitive packages (`Microsoft.OpenApi`, `SQLitePCLRaw.lib.e_sqlite3`) to clear high-severity advisories. See the comment in `LudosData.Api.csproj`. - Image processing uses **SkiaSharp**, not ImageSharp: ImageSharp v4 requires a paid licence key at build time.