Files
LudosData/README.md
T
ckochandClaude Opus 5 2a7d90b2d5 Rebuild on Angular 22 + ASP.NET Core 10, containerised
The 2018 stack (Angular 5.2 / CLI 1.7, PHP, MySQL) had not been touched since
July 2018. Rebuilt rather than upgraded in place: the frontend was 17 major
versions behind, and of ~16,700 lines of PHP only ~150 were application logic —
the rest was four near-identical vendored copies of php-crud-api plus
class.upload.php.

Backend — ASP.NET Core 10, EF Core, SQLite
  * ASP.NET Core Identity (PBKDF2) + JWT bearer auth
  * Clean REST API replacing php-crud-api's filter[]/transform query syntax
  * Box art uploads re-encoded to WebP via SkiaSharp
  * Imports the 105 games recovered from the 2018 dump on first run

Frontend — Angular 22, zoneless, signals, Material 22
  * Standalone components, lazy routes, functional guards and interceptor
  * Vitest replaces Karma/Jasmine; fonts and icons bundled, no CDN calls
  * No provideAnimations: @angular/animations is deprecated in v22 and
    Material no longer depends on it (pinned by a test)

Docker
  * Multi-stage builds for both services, non-root at runtime
  * nginx serves the SPA and reverse-proxies the API, so everything is
    same-origin; one volume holds the database, uploads and DP keys

Security issues in the old code, not carried across:
  * Two endpoints exposed unauthenticated CRUD over every table
  * The client chose whose rows to read (filter[]=userId,eq,N); ownership now
    comes from the JWT subject server-side
  * Login was hardcoded to a single username
  * crypt() with one global salt, silently truncating passwords to 8 chars
  * JWT secret was the literal string "testing", tokens never expired
  * Token travelled in the query string rather than a header
  * Uploads were anonymous with the path built from the client filename
  * Access-Control-Allow-Origin: *

The live MySQL password committed in 2018 remains in git history and must be
rotated independently of this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 18:46:33 -04:00

6.4 KiB

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

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 http://localhost:8080 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.

# 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
cd frontend && npm test          # vitest
cd backend  && dotnet build      # 0 warnings expected

Database changes

cd backend/src/LudosData.Api
dotnet ef migrations add <Name> --output-dir Data/Migrations

Migrations are applied automatically at startup.


API

All /api/games and /api/images routes require Authorization: Bearer <token>.

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.