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>
72 lines
2.5 KiB
Nginx Configuration File
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;
|
|
}
|
|
}
|