From d68e8679eecdb94991c63d67f3692125ea75fb3d Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:22:09 +0100 Subject: [PATCH] Merge pull request #28380 from overleaf/td-zod-history Migrate history-related web back end to Zod GitOrigin-RevId: 38ed56927bee4f5670604d7178b096e382c8cb65 --- .../Features/History/HistoryController.mjs | 40 ++++++++++++++++--- .../src/Features/History/HistoryManager.js | 4 +- .../src/Features/History/HistoryRouter.mjs | 32 --------------- .../web/app/src/infrastructure/Validation.js | 1 + 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/services/web/app/src/Features/History/HistoryController.mjs b/services/web/app/src/Features/History/HistoryController.mjs index 8c50e5b213..a393ab5702 100644 --- a/services/web/app/src/Features/History/HistoryController.mjs +++ b/services/web/app/src/Features/History/HistoryController.mjs @@ -25,6 +25,7 @@ import ProjectEntityUpdateHandler from '../Project/ProjectEntityUpdateHandler.js import RestoreManager from './RestoreManager.mjs' import { prepareZipAttachment } from '../../infrastructure/Response.js' import Features from '../../infrastructure/Features.js' +import { z, zz, validateReq } from '../../infrastructure/Validation.js' // Number of seconds after which the browser should send a request to revalidate // blobs @@ -44,8 +45,19 @@ async function headBlob(req, res) { await requestBlob('HEAD', req, res) } +const requestBlobSchema = z.object({ + params: z.object({ + project_id: zz.coercedObjectId(), + hash: zz.hex().length(40), + }), + query: z.object({ + fallback: zz.coercedObjectId().optional(), + }), +}) + async function requestBlob(method, req, res) { - const { project_id: projectId, hash } = req.params + const { params } = validateReq(req, requestBlobSchema) + const { project_id: projectId, hash } = params // Handle conditional GET request if (req.get('If-None-Match') === hash) { @@ -461,17 +473,35 @@ async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) { } } +const getLatestHistorySchema = z.object({ + params: z.object({ + project_id: zz.objectId(), + }), +}) + async function getLatestHistory(req, res, next) { - const projectId = req.params.project_id + const { params } = validateReq(req, getLatestHistorySchema) + const projectId = params.project_id const history = await HistoryManager.promises.getLatestHistory(projectId) res.json(history) } +const getChangesSchema = z.object({ + params: z.object({ + project_id: zz.objectId(), + }), + query: z.object({ + since: z.coerce.number().int().min(0).optional(), + paginated: z.stringbool().optional(), + }), +}) + async function getChanges(req, res, next) { - const projectId = req.params.project_id - let since = req.query.since + const { params, query } = validateReq(req, getChangesSchema) + const projectId = params.project_id + let since = query.since // TODO: Transition flag; remove after a while - const paginated = req.query.paginated === 'true' + const paginated = query.paginated if (paginated) { const changes = await HistoryManager.promises.getChanges(projectId, { diff --git a/services/web/app/src/Features/History/HistoryManager.js b/services/web/app/src/Features/History/HistoryManager.js index 39911a25cb..46d8831a55 100644 --- a/services/web/app/src/Features/History/HistoryManager.js +++ b/services/web/app/src/Features/History/HistoryManager.js @@ -280,8 +280,8 @@ async function getLatestHistory(projectId) { * Get history changes since a given version * * @param {string} projectId - * @param {object} opts - * @param {number} opts.since - The start version of changes to get + * @param {object} [opts] + * @param {number} [opts.since] - The start version of changes to get */ async function getChanges(projectId, opts = {}) { const historyId = await getHistoryId(projectId) diff --git a/services/web/app/src/Features/History/HistoryRouter.mjs b/services/web/app/src/Features/History/HistoryRouter.mjs index 89d68caf7f..38c338a061 100644 --- a/services/web/app/src/Features/History/HistoryRouter.mjs +++ b/services/web/app/src/Features/History/HistoryRouter.mjs @@ -1,7 +1,6 @@ // @ts-check import Settings from '@overleaf/settings' -import { Joi, validate } from '../../infrastructure/Validation.js' import { RateLimiter } from '../../infrastructure/RateLimiter.js' import AuthenticationController from '../Authentication/AuthenticationController.js' import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs' @@ -29,30 +28,12 @@ function apply(webRouter, privateApiRouter) { webRouter.head( '/project/:project_id/blob/:hash', - validate({ - params: Joi.object({ - project_id: Joi.objectId().required(), - hash: Joi.string().required().hex().length(40), - }), - query: Joi.object({ - fallback: Joi.objectId().optional(), - }), - }), RateLimiterMiddleware.rateLimit(rateLimiters.getProjectBlob), AuthorizationMiddleware.ensureUserCanReadProject, HistoryController.headBlob ) webRouter.get( '/project/:project_id/blob/:hash', - validate({ - params: Joi.object({ - project_id: Joi.objectId().required(), - hash: Joi.string().required().hex().length(40), - }), - query: Joi.object({ - fallback: Joi.objectId().optional(), - }), - }), RateLimiterMiddleware.rateLimit(rateLimiters.getProjectBlob), AuthorizationMiddleware.ensureUserCanReadProject, HistoryController.getBlob @@ -151,25 +132,12 @@ function apply(webRouter, privateApiRouter) { webRouter.get( '/project/:project_id/latest/history', - validate({ - params: Joi.object({ - project_id: Joi.objectId().required(), - }), - }), AuthorizationMiddleware.blockRestrictedUserFromProject, AuthorizationMiddleware.ensureUserCanReadProject, HistoryController.getLatestHistory ) webRouter.get( '/project/:project_id/changes', - validate({ - params: Joi.object({ - project_id: Joi.objectId().required(), - }), - query: Joi.object({ - since: Joi.number().integer().min(0).optional(), - }), - }), AuthorizationMiddleware.blockRestrictedUserFromProject, AuthorizationMiddleware.ensureUserCanReadProject, HistoryController.getChanges diff --git a/services/web/app/src/infrastructure/Validation.js b/services/web/app/src/infrastructure/Validation.js index cb7db67af0..427cede539 100644 --- a/services/web/app/src/infrastructure/Validation.js +++ b/services/web/app/src/infrastructure/Validation.js @@ -48,6 +48,7 @@ const zz = { .string() .refine(ObjectId.isValid, { message: 'invalid Mongo ObjectId' }) .transform(val => new ObjectId(val)), + hex: () => z.string().regex(/^[0-9a-f]*$/), } /**