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>
70 lines
2.3 KiB
YAML
70 lines
2.3 KiB
YAML
# LudosData — full stack.
|
|
#
|
|
# cp .env.example .env # then edit the secrets
|
|
# docker compose up --build
|
|
#
|
|
# The SPA is served by nginx on http://localhost:8080, which also reverse-proxies
|
|
# /api and /uploads to the API container. Because everything is same-origin in
|
|
# this setup, the browser never issues a cross-origin request and CORS is not in
|
|
# play at all — the API's CORS policy only matters for `ng serve` on :4200.
|
|
|
|
services:
|
|
api:
|
|
build:
|
|
context: ./backend
|
|
image: ludosdata-api
|
|
restart: unless-stopped
|
|
environment:
|
|
ASPNETCORE_ENVIRONMENT: Production
|
|
|
|
# Required. Startup fails loudly if this is missing or under 32 chars.
|
|
Jwt__Key: ${JWT_KEY:?JWT_KEY is required — see .env.example}
|
|
Jwt__Issuer: ${JWT_ISSUER:-LudosData}
|
|
Jwt__Audience: ${JWT_AUDIENCE:-LudosData}
|
|
Jwt__LifetimeMinutes: ${JWT_LIFETIME_MINUTES:-720}
|
|
|
|
# Creates the first account and imports the 105 games from the 2018 dump,
|
|
# but only while the database has no users at all.
|
|
Seed__Enabled: ${SEED_ENABLED:-true}
|
|
Seed__UserName: ${SEED_USERNAME:-}
|
|
Seed__Email: ${SEED_EMAIL:-}
|
|
Seed__Password: ${SEED_PASSWORD:-}
|
|
|
|
# Only consulted when the SPA is served from somewhere other than nginx.
|
|
Cors__AllowedOrigins__0: ${CORS_ORIGIN:-http://localhost:8080}
|
|
Cors__AllowedOrigins__1: http://localhost:4200
|
|
volumes:
|
|
# SQLite file and uploaded box art. This is the only stateful thing in the
|
|
# stack — back this volume up and you have backed up everything.
|
|
- ludos-data:/data
|
|
expose:
|
|
- "8080"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"]
|
|
interval: 15s
|
|
timeout: 3s
|
|
retries: 5
|
|
start_period: 20s
|
|
|
|
web:
|
|
build:
|
|
context: ./frontend
|
|
image: ludosdata-web
|
|
restart: unless-stopped
|
|
depends_on:
|
|
api:
|
|
condition: service_healthy
|
|
ports:
|
|
- "${WEB_PORT:-8080}:8080"
|
|
healthcheck:
|
|
# 127.0.0.1, not localhost: nginx listens on IPv4 only, and BusyBox wget
|
|
# resolves localhost to ::1 first and gets connection-refused.
|
|
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8080/ || exit 1"]
|
|
interval: 15s
|
|
timeout: 3s
|
|
retries: 5
|
|
start_period: 10s
|
|
|
|
volumes:
|
|
ludos-data:
|