Files
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

72 lines
2.5 KiB
Nginx Configuration File

# Serves the built Angular bundle and reverse-proxies the API, so the browser
# sees a single origin and never makes a cross-origin request.
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Client uploads are capped server-side too; this stops oversized bodies
# from being buffered all the way to the API first.
client_max_body_size 6m;
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
# Do not advertise the exact nginx version.
server_tokens off;
include /etc/nginx/snippets/security-headers.conf;
# Hashed build assets are immutable, so they can be cached hard.
location ~* \.(?:js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|webp|ico)$ {
include /etc/nginx/snippets/security-headers.conf;
# add_header alone, not `expires`: using both emits two Cache-Control
# headers with overlapping directives.
add_header Cache-Control "public, max-age=31536000, immutable" always;
try_files $uri =404;
}
location /api/ {
proxy_pass http://api:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Uploaded box art, served straight off the API's volume.
#
# `^~` matters: without it, a regex location wins over a prefix location, so
# /uploads/<id>/<name>.webp would fall into the static-asset block above and
# 404 against nginx's own filesystem instead of being proxied.
location ^~ /uploads/ {
include /etc/nginx/snippets/security-headers.conf;
proxy_pass http://api:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
add_header Cache-Control "public, max-age=2592000" always;
}
location /health {
proxy_pass http://api:8080/health;
}
# index.html must never be cached, or clients keep booting old bundles.
# Declared before `location /` so the internal rewrite below lands here.
location = /index.html {
include /etc/nginx/snippets/security-headers.conf;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}
# Angular owns routing: any unknown path returns index.html so a deep link
# or a refresh on /games/12 does not 404.
location / {
try_files $uri $uri/ /index.html;
}
}