Admin create had no URL handling at all (and crashed on missing steamId
via .length), and admin update passed req.body straight to the scraper.
Reuse normalizeSteamId in both, 400 on unrecognized input, and reject
failed scrapes (non-object / missing title+frontImage) before
Game.create/findOneAndUpdate so failures report as 'Steam lookup failed'
instead of schema validation noise.
The store-URL support extracted the app ID into a local but never wrote
it back, so steamScraper still received the full URL, keyed the Steam API
response by the URL string, threw, and its caught Error object flowed into
Game.create() — surfacing as misleading 'Path lastModifiedBy / createdBy /
frontImage / title is required' validation errors.
- add utils/normalizeSteamId: digits pass through, /app/<id>/ (plus
/agecheck/app/<id>/ and query strings) extracts the ID, empty -> '',
anything else -> null (no .match()[0] / .includes crash on bad input)
- create(): 400 cleanly on unrecognized input, write the normalized ID
back to req.body.steamId so dup checks, scraper, and stored doc agree
- guard the scrape result (missing title/frontImage, false, or Error) and
400 'Steam lookup failed' instead of leaking it into Game.create
Mongoose 7 removed callback support on Model.prototype.save(),
so the seeder threw 'no longer accepts a callback' on every
boot (caught and logged, non-fatal, but noisy and the success
path never ran). Await the promise and log on success.
One shared 100 req / 10 min limiter counted everything —
401s, CORS preflights, even /health — so a bad-token burst
from a second frontend burned the budget and 429'd POST
/api/auth/login too, leaving no way back in short of
waiting out the window or restarting (MemoryStore reset).
New middleware/rateLimit.js holds two limiters, verified
against express-rate-limit@5.5.1:
- apiLimiter (300 / 15 min) skips OPTIONS, /health, and the
public auth endpoints, so preflights, probes, and login
attempts never drain real API budget.
- authLimiter (20 / 15 min, successful logins free) guards
register/login/forgotpassword/resetpassword against
brute force. 429 there means 20 wrong passwords, never
a busy API.
Storm-tested with live server: 320 bad-token hits burn the
global budget yet login still returns 200; 25 bad logins
trip only the login limiter while API traffic is untouched.
Add .gitea/workflows/release.yml: on v* tag push (or manual
dispatch) build the Docker image and push
git.linuxhg.com/games-database/games-api:<tag> plus :latest,
then create/update the Gitea Release from the annotated tag
message. Main-branch pushes do nothing so WIP never ships.
Add cliff.toml (conventional-commit grouping for release
notes), Makefile release target (cliff notes into annotated
tag, then push), and ./release wrapper (accepts 1.0 or
v1.0). Release flow: ./release v1.0, matching bookhoard.
Requires a REGISTRY_TOKEN repo Actions secret (PAT with
write:package and write:repository); Gitea's auto token
lacks package scope.
Dockerfile: stop baking secrets into the image. Only NODE_ENV
remains; all runtime config (MONGO_URI, SMTP_*, token
secrets) comes from the environment so one image runs
anywhere. Install curl for container healthchecks. App
variable names untouched (singular JWT_EXPIRE as read by
models/User.js).
docker-compose.example.yml: rewrite as a prod run template
for git.linuxhg.com/games-database/games-api (IMAGE_TAG,
default latest) with restart, env_file .env, and a curl
/health healthcheck. Copy to docker-compose.yml next to
the server .env, then pull and up. Full variable list kept
commented out so values can live in the file instead of
.env if preferred.
Returns 200 { status: 'ok' } for container healthchecks and
monitoring. Placed before all /api/* routes so it bypasses
auth; intentionally static with no DB dependency so the
container reports healthy whenever the process is serving.
Add NODE_ENV (used by config/db.js and controllers/auth.js
to switch MONGO_URI/MONGO_DEV_URI and secure cookies).
Drop PATH, BUN_RUNTIME_TRANSPILER_CACHE_PATH, and
BUN_INSTALL_BIN leaked from container runtime env; they are
not read by the app.
Drop JWT_EXPIRES duplicate; app reads JWT_EXPIRE
(models/User.js expiresIn), matching .env.
Restore SECURE=false default (used by utils/sendEmail.js
via yn(Bun.env.SECURE)).
Keep list aligned with Dockerfile and .env keys so the
example stays a valid template for server deployments.
Allow callers to pass either a raw Steam app ID or a full store URL (e.g. https://store.steampowered.com/app/730/...) in req.body.steamId.
In create() in controllers/games.js:
- normalize input into a local steamId variable: if the value includes store.steampowered.com, extract the numeric ID via match(/\d+/)[0], otherwise use the value as-is
- use the normalized steamId for both duplicate checks: user-scoped lookup (steamId + accessedBy.user) and global lookup
This lets users paste a copied store link directly without manually stripping the app ID, while keeping existing raw-ID behavior unchanged.
Apply Prettier default formatting across the games controller with no logic changes:
- single -> double quotes, add missing semicolons
- 2-space indentation and consistent line wrapping
- expand dense ternary/decode blocks in create() and update() for readability
This isolates the upcoming create() Steam URL feature so its functional diff stays small and reviewable.