From c63b1239ca0f43e24b6378a4b5452d12a2e67d38 Mon Sep 17 00:00:00 2001 From: Cailyn Sinclair Date: Wed, 15 Jul 2026 15:42:05 -0700 Subject: [PATCH] Enable user-scoped solve history preview Require authentication before checking solve-history availability so the existing production allowlist can authorize the requested user safely. --- README.md | 7 ++++--- docs/data.md | 11 ++++++----- server/api.js | 6 +----- server/api.test.js | 13 ++++++++++++- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index cb9fde3..39383a0 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,10 @@ RaceSession and starts a new one; earlier attempts and solves remain preserved under their earlier session. Solve penalties use dedicated boolean columns rather than JSON so histories and -statistics remain compact and index-friendly. Development-only -`GET /api/solve-history` reads PostgreSQL session-linked history for the -authenticated participant and is explicitly disabled in production. +statistics remain compact and index-friendly. `GET /api/solve-history` reads +PostgreSQL session-linked history for the authenticated participant. It is +enabled in development and may be enabled for selected production users with +`FEATURE_SOLVE_HISTORY_USER_IDS`. Set `POSTGRES_ENABLED=false` to disable mirroring. Production should set `PGHOST`, `PGDATABASE`, `PGUSER`, and `POSTGRES_PASSWORD`, or provide a diff --git a/docs/data.md b/docs/data.md index f73789e..0e8f395 100644 --- a/docs/data.md +++ b/docs/data.md @@ -51,11 +51,12 @@ new one, preserving the earlier session's attempts and solves. Complete snapshots are reserved for explicit backfill behavior. `GET /api/solve-history` is a PostgreSQL-only, authenticated self-history -endpoint enabled only when `NODE_ENV=development`. It returns only -session-linked solves for a current non-banned room participant; hidden, -expired, and soft-deleted rooms remain readable to that participant. It never -falls back to MongoDB or selects access codes, passwords, membership lists, -moderation data, or email data. +endpoint enabled in development and optionally for selected production users +through `FEATURE_SOLVE_HISTORY_USER_IDS`. It returns only session-linked solves +for a current non-banned room participant; hidden, expired, and soft-deleted +rooms remain readable to that participant. It never falls back to MongoDB or +selects access codes, passwords, membership lists, moderation data, or email +data. The mirror intentionally catches database failures and returns control to the MongoDB-backed request. Monitoring must surface mirror errors because users may diff --git a/server/api.js b/server/api.js index bb4c99a..3a4a510 100644 --- a/server/api.js +++ b/server/api.js @@ -37,11 +37,7 @@ module.exports = (app) => { res.json(req.user.toObject()); }); - if (isFeatureEnabled('solveHistory')) { - router.use('/solve-history', auth, createSolveHistoryRouter()); - } else { - router.use('/solve-history', createSolveHistoryRouter()); - } + router.use('/solve-history', auth, createSolveHistoryRouter()); router.put('/updateUsername', auth, async (req, res) => { try { diff --git a/server/api.test.js b/server/api.test.js index bbc1944..bfef8ce 100644 --- a/server/api.test.js +++ b/server/api.test.js @@ -4,9 +4,10 @@ jest.mock('./models', () => ({ User: { findOne: jest.fn() }, })); -jest.mock('./middlewares/auth.js', () => (req, res, next) => next()); +jest.mock('./middlewares/auth.js', () => jest.fn((req, res, next) => next())); const { User } = require('./models'); +const auth = require('./middlewares/auth.js'); const router = require('./api')(); const updateUsernameHandler = router.stack @@ -74,3 +75,13 @@ describe('username API responses', () => { })); }); }); + +describe('solve history API access', () => { + it('requires authentication before checking the feature flag', () => { + const authLayer = router.stack.find((layer) => ( + String(layer.regexp).includes('solve-history') && layer.handle === auth + )); + + expect(authLayer).toBeDefined(); + }); +});