Merge pull request #28380 from overleaf/td-zod-history

Migrate history-related web back end to Zod

GitOrigin-RevId: 38ed56927bee4f5670604d7178b096e382c8cb65
This commit is contained in:
Tim Down
2025-09-18 08:05:34 +00:00
committed by Copybot
parent ebed4b828a
commit d2f5cd4e46
4 changed files with 38 additions and 39 deletions
@@ -25,6 +25,7 @@ import ProjectEntityUpdateHandler from '../Project/ProjectEntityUpdateHandler.js
import RestoreManager from './RestoreManager.mjs' import RestoreManager from './RestoreManager.mjs'
import { prepareZipAttachment } from '../../infrastructure/Response.js' import { prepareZipAttachment } from '../../infrastructure/Response.js'
import Features from '../../infrastructure/Features.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 // Number of seconds after which the browser should send a request to revalidate
// blobs // blobs
@@ -44,8 +45,19 @@ async function headBlob(req, res) {
await requestBlob('HEAD', 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) { 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 // Handle conditional GET request
if (req.get('If-None-Match') === hash) { 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) { 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) const history = await HistoryManager.promises.getLatestHistory(projectId)
res.json(history) 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) { async function getChanges(req, res, next) {
const projectId = req.params.project_id const { params, query } = validateReq(req, getChangesSchema)
let since = req.query.since const projectId = params.project_id
let since = query.since
// TODO: Transition flag; remove after a while // TODO: Transition flag; remove after a while
const paginated = req.query.paginated === 'true' const paginated = query.paginated
if (paginated) { if (paginated) {
const changes = await HistoryManager.promises.getChanges(projectId, { const changes = await HistoryManager.promises.getChanges(projectId, {
@@ -280,8 +280,8 @@ async function getLatestHistory(projectId) {
* Get history changes since a given version * Get history changes since a given version
* *
* @param {string} projectId * @param {string} projectId
* @param {object} opts * @param {object} [opts]
* @param {number} opts.since - The start version of changes to get * @param {number} [opts.since] - The start version of changes to get
*/ */
async function getChanges(projectId, opts = {}) { async function getChanges(projectId, opts = {}) {
const historyId = await getHistoryId(projectId) const historyId = await getHistoryId(projectId)
@@ -1,7 +1,6 @@
// @ts-check // @ts-check
import Settings from '@overleaf/settings' import Settings from '@overleaf/settings'
import { Joi, validate } from '../../infrastructure/Validation.js'
import { RateLimiter } from '../../infrastructure/RateLimiter.js' import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import AuthenticationController from '../Authentication/AuthenticationController.js' import AuthenticationController from '../Authentication/AuthenticationController.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs' import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
@@ -29,30 +28,12 @@ function apply(webRouter, privateApiRouter) {
webRouter.head( webRouter.head(
'/project/:project_id/blob/:hash', '/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), RateLimiterMiddleware.rateLimit(rateLimiters.getProjectBlob),
AuthorizationMiddleware.ensureUserCanReadProject, AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.headBlob HistoryController.headBlob
) )
webRouter.get( webRouter.get(
'/project/:project_id/blob/:hash', '/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), RateLimiterMiddleware.rateLimit(rateLimiters.getProjectBlob),
AuthorizationMiddleware.ensureUserCanReadProject, AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.getBlob HistoryController.getBlob
@@ -151,25 +132,12 @@ function apply(webRouter, privateApiRouter) {
webRouter.get( webRouter.get(
'/project/:project_id/latest/history', '/project/:project_id/latest/history',
validate({
params: Joi.object({
project_id: Joi.objectId().required(),
}),
}),
AuthorizationMiddleware.blockRestrictedUserFromProject, AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject, AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.getLatestHistory HistoryController.getLatestHistory
) )
webRouter.get( webRouter.get(
'/project/:project_id/changes', '/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.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject, AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.getChanges HistoryController.getChanges
@@ -48,6 +48,7 @@ const zz = {
.string() .string()
.refine(ObjectId.isValid, { message: 'invalid Mongo ObjectId' }) .refine(ObjectId.isValid, { message: 'invalid Mongo ObjectId' })
.transform(val => new ObjectId(val)), .transform(val => new ObjectId(val)),
hex: () => z.string().regex(/^[0-9a-f]*$/),
} }
/** /**