2 Commits
Author SHA1 Message Date
john-okeefe ee7aa9921b fix(adminGames): apply Steam URL normalization and scraper guard to create/update
Release / build-and-push (push) Successful in 1m1s
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.
2026-09-06 10:11:07 -04:00
john-okeefe 964415198a fix(games): persist normalized Steam ID and guard scraper failures in create()
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
2026-09-06 10:11:07 -04:00
3 changed files with 81 additions and 6 deletions
+39 -2
View File
@@ -2,6 +2,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'
const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$')
const checkForTwelveRegExp = new RegExp('^[0-9a-fA-F]{12}$')
@@ -41,8 +42,17 @@ export const show = asyncHandler(async (req, res, next) => {
*/
export const create = asyncHandler(async (req, res, next) => {
let oldGame
req.body.steamId.length > 0
? (oldGame = await Game.findOne({ 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
steamId.length > 0
? (oldGame = await Game.findOne({ steamId }))
: (oldGame = await Game.findOne({ title: req.body.title }))
if (oldGame)
@@ -56,6 +66,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)
)
if (req.body.scrape === false) {
if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc)
if (req.body.reviews && isValid(req.body.reviews)) req.body.reviews = decode(req.body.reviews)
@@ -96,6 +114,17 @@ export const update = asyncHandler(async (req, res, next) => {
req.body.lastModifiedBy = req.user.id
if (req.body.steamId !== undefined) {
const normalizedSteamId = normalizeSteamId(req.body.steamId)
if (normalizedSteamId === null)
return next(
new ErrorResponse(
'Enter a Steam app ID or a store.steampowered.com URL.',
400,
)
)
req.body.steamId = normalizedSteamId
}
if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc)
if (req.body.reviews && isValid(req.body.reviews)) req.body.reviews = decode(req.body.reviews)
if (req.body.summary && isValid(req.body.summary)) req.body.summary = decode(req.body.summary)
@@ -109,6 +138,14 @@ export const update = 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)
)
game = await Game.findOneAndUpdate({ [gameId]: id }, data, {
new: true,
runValidators: true,
+18 -4
View File
@@ -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({
+24
View File
@@ -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;