From 964415198a9f8f2ccb70a950f015b0e1943f076c Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 6 Sep 2026 10:11:07 -0400 Subject: [PATCH] fix(games): persist normalized Steam ID and guard scraper failures in create() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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// (plus /agecheck/app// 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 --- controllers/games.js | 22 ++++++++++++++++++---- utils/normalizeSteamId.js | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 utils/normalizeSteamId.js diff --git a/controllers/games.js b/controllers/games.js index 530ef6b..4682672 100644 --- a/controllers/games.js +++ b/controllers/games.js @@ -4,6 +4,7 @@ import Game from "../models/Game.js"; import steamScraper from "../scripts/scraper.js"; import asyncHandler from "../middleware/async.js"; import ErrorResponse from "../utils/errorResponse.js"; +import normalizeSteamId from "../utils/normalizeSteamId.js"; import { decode, isValid } from "js-base64"; const checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$"); @@ -77,11 +78,16 @@ export const show = asyncHandler(async (req, res, next) => { */ export const create = asyncHandler(async (req, res, next) => { let oldGame; - let steamId; - if (req.body.steamId.includes("store.steampowered.com")) - steamId = req.body.steamId.match(/\d+/)[0]; - else steamId = req.body.steamId; + const steamId = normalizeSteamId(req.body.steamId); + if (steamId === null) + return next( + new ErrorResponse( + "Enter a Steam app ID or a store.steampowered.com URL.", + 400, + ), + ); + req.body.steamId = steamId; if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc); @@ -166,6 +172,14 @@ export const create = asyncHandler(async (req, res, next) => { const data = req.body.scrape === true ? await steamScraper(req.body) : req.body; + if ( + req.body.scrape === true && + (!data || typeof data !== "object" || !data.title || !data.frontImage) + ) + return next( + new ErrorResponse("Steam lookup failed for that ID/URL.", 400), + ); + const game = await Game.create(data); res.status(200).json({ diff --git a/utils/normalizeSteamId.js b/utils/normalizeSteamId.js new file mode 100644 index 0000000..909dc6c --- /dev/null +++ b/utils/normalizeSteamId.js @@ -0,0 +1,24 @@ +/** + * normalizeSteamId() + * + * @description Accepts either a plain numeric Steam app ID or a + * store.steampowered.com URL and returns the numeric app ID. + * Returns "" when the input is empty/missing (manual non-scrape flow + * looks games up by title instead) and `null` when the input is + * neither digits nor a recognized Steam Store URL. + */ +const normalizeSteamId = (value) => { + const raw = String(value ?? "").trim(); + if (!raw) return ""; + + const urlMatch = raw.match( + /store\.steampowered\.com\/(?:agecheck\/)?app\/(\d+)/, + ); + if (urlMatch) return urlMatch[1]; + + if (/^\d+$/.test(raw)) return raw; + + return null; +}; + +export default normalizeSteamId;