Merge pull request #36225 from overleaf/jpa-zod-web-final

[web] fully adopt strict zod schema and enforce in tests

GitOrigin-RevId: 1bb03ed3a819be6de38c8ca5758b3909cfa23318
This commit is contained in:
Jakob Ackermann
2026-09-17 11:55:44 +00:00
committed by Copybot
parent de0d1a7226
commit a6792af5b1
2 changed files with 258 additions and 25 deletions
@@ -40,9 +40,19 @@ export default {
grantAccessRequest: expressify(grantAccessRequest),
}
const removeUserFromProjectSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
user_id: zz.objectId(),
}),
})
async function removeUserFromProject(req, res, next) {
const projectId = req.params.Project_id
const userId = req.params.user_id
const { params } = parseReq(req, removeUserFromProjectSchema, {
logOnly: true,
})
const projectId = params.Project_id
const userId = params.user_id
const sessionUserId = SessionManager.getLoggedInUserId(req.session)
await _removeUserIdFromProject(projectId, userId)
EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {
@@ -68,8 +78,17 @@ async function removeUserFromProject(req, res, next) {
res.sendStatus(204)
}
const removeSelfFromProjectSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
}),
})
async function removeSelfFromProject(req, res, next) {
const projectId = req.params.Project_id
const { params } = parseReq(req, removeSelfFromProjectSchema, {
logOnly: true,
})
const projectId = params.Project_id
const userId = SessionManager.getLoggedInUserId(req.session)
await _removeUserIdFromProject(projectId, userId)
EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {
@@ -86,8 +105,15 @@ async function removeSelfFromProject(req, res, next) {
res.sendStatus(204)
}
const getAllMembersSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
}),
})
async function getAllMembers(req, res, next) {
const projectId = req.params.Project_id
const { params } = parseReq(req, getAllMembersSchema, { logOnly: true })
const projectId = params.Project_id
logger.debug({ projectId }, 'getting all active members for project')
let members
try {
@@ -98,11 +124,20 @@ async function getAllMembers(req, res, next) {
res.json({ members })
}
const getAccessRequestsSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
}),
})
// The pending access requests carry requester identities, so this is admin
// gated by the router (ensureUserCanAdminProject). A requester's own
// `myAccessRequest` is delivered via the editor bootstrap instead.
async function getAccessRequests(req, res) {
const projectId = req.params.Project_id
const { params } = parseReq(req, getAccessRequestsSchema, {
logOnly: true,
})
const projectId = params.Project_id
const projectAccess =
await CollaboratorsGetter.promises.getProjectAccess(projectId)
const editAccessRequests = await projectAccess.loadAccessRequestsView()
@@ -110,6 +145,22 @@ async function getAccessRequests(req, res) {
}
const setCollaboratorInfoSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
user_id: zz.objectId(),
}),
body: z.strictObject({
privilegeLevel: z.enum([
PrivilegeLevels.READ_ONLY,
PrivilegeLevels.READ_AND_WRITE,
PrivilegeLevels.REVIEW,
]),
}),
})
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
const setCollaboratorInfoFallbackSchema = z.object({
params: z.object({
Project_id: zz.objectId(),
user_id: zz.objectId(),
@@ -125,7 +176,9 @@ const setCollaboratorInfoSchema = z.object({
async function setCollaboratorInfo(req, res, next) {
try {
const { params, body } = parseReq(req, setCollaboratorInfoSchema)
const { params, body } = parseReq(req, setCollaboratorInfoSchema, {
fallbackSchema: setCollaboratorInfoFallbackSchema,
})
const projectId = params.Project_id
const userId = params.user_id
const { privilegeLevel } = body
@@ -172,6 +225,17 @@ async function setCollaboratorInfo(req, res, next) {
}
const transferOwnershipSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
}),
body: z.strictObject({
user_id: zz.objectId(),
}),
})
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
const transferOwnershipFallbackSchema = z.object({
params: z.object({
Project_id: zz.objectId(),
}),
@@ -182,7 +246,9 @@ const transferOwnershipSchema = z.object({
async function transferOwnership(req, res, next) {
const sessionUser = SessionManager.getSessionUser(req.session)
const { params, body } = parseReq(req, transferOwnershipSchema)
const { params, body } = parseReq(req, transferOwnershipSchema, {
fallbackSchema: transferOwnershipFallbackSchema,
})
const projectId = params.Project_id
const toUserId = body.user_id
try {
@@ -214,6 +280,20 @@ async function transferOwnership(req, res, next) {
}
const requestAccessSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
}),
body: z.strictObject({
privilegeLevel: z.enum([
PrivilegeLevels.READ_AND_WRITE,
PrivilegeLevels.REVIEW,
]),
}),
})
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
const requestAccessFallbackSchema = z.object({
params: z.object({
Project_id: zz.objectId(),
}),
@@ -238,7 +318,9 @@ const REQUESTABLE_LEVELS_BY_CURRENT_LEVEL = {
}
async function requestAccess(req, res) {
const { params, body } = parseReq(req, requestAccessSchema)
const { params, body } = parseReq(req, requestAccessSchema, {
fallbackSchema: requestAccessFallbackSchema,
})
const projectId = params.Project_id
const { privilegeLevel } = body
const userId = SessionManager.getLoggedInUserId(req.session)
@@ -279,6 +361,18 @@ async function requestAccess(req, res) {
}
const declineAccessRequestSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
user_id: zz.objectId(),
}),
body: z.strictObject({
notify: z.boolean().optional(),
}),
})
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
const declineAccessRequestFallbackSchema = z.object({
params: z.object({
Project_id: zz.objectId(),
user_id: zz.objectId(),
@@ -289,7 +383,9 @@ const declineAccessRequestSchema = z.object({
})
async function declineAccessRequest(req, res) {
const { params, body } = parseReq(req, declineAccessRequestSchema)
const { params, body } = parseReq(req, declineAccessRequestSchema, {
fallbackSchema: declineAccessRequestFallbackSchema,
})
const projectId = params.Project_id
const userId = params.user_id
const notify = body.notify === true
@@ -323,6 +419,22 @@ async function declineAccessRequest(req, res) {
}
const grantAccessRequestSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
user_id: zz.objectId(),
}),
body: z.strictObject({
privilegeLevel: z.enum([
PrivilegeLevels.READ_AND_WRITE,
PrivilegeLevels.REVIEW,
]),
notify: z.boolean().optional(),
}),
})
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
const grantAccessRequestFallbackSchema = z.object({
params: z.object({
Project_id: zz.objectId(),
user_id: zz.objectId(),
@@ -337,7 +449,9 @@ const grantAccessRequestSchema = z.object({
})
async function grantAccessRequest(req, res) {
const { params, body } = parseReq(req, grantAccessRequestSchema)
const { params, body } = parseReq(req, grantAccessRequestSchema, {
fallbackSchema: grantAccessRequestFallbackSchema,
})
const projectId = params.Project_id
const requesterId = params.user_id
const { privilegeLevel, notify } = body
@@ -507,8 +621,15 @@ async function _removeUserIdFromProject(projectId, userId) {
await TagsHandler.promises.removeProjectFromAllTags(userId, projectId)
}
const getShareTokensSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
}),
})
async function getShareTokens(req, res) {
const projectId = req.params.Project_id
const { params } = parseReq(req, getShareTokensSchema, { logOnly: true })
const projectId = params.Project_id
const userId = SessionManager.getLoggedInUserId(req.session)
if (!Features.hasFeature('link-sharing')) {
@@ -40,8 +40,15 @@ const rateLimiter = new RateLimiter('invite-to-project-by-user-id', {
duration: 60 * 30,
})
const getAllInvitesSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
}),
})
async function getAllInvites(req, res) {
const projectId = req.params.Project_id
const { params } = parseReq(req, getAllInvitesSchema, { logOnly: true })
const projectId = params.Project_id
logger.debug({ projectId }, 'getting all active invites for project')
const invites =
await CollaboratorsInviteGetter.promises.getAllInvites(projectId)
@@ -89,6 +96,21 @@ async function _checkRateLimit(userId) {
}
const inviteToProjectSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
}),
body: z.strictObject({
email: z.string(),
privileges: z.enum([
PrivilegeLevels.READ_ONLY,
PrivilegeLevels.READ_AND_WRITE,
PrivilegeLevels.REVIEW,
]),
}),
})
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
const inviteToProjectFallbackSchema = z.object({
params: z.object({
Project_id: zz.objectId(),
}),
@@ -103,7 +125,9 @@ const inviteToProjectSchema = z.object({
})
async function inviteToProject(req, res) {
const { params, body } = parseReq(req, inviteToProjectSchema)
const { params, body } = parseReq(req, inviteToProjectSchema, {
fallbackSchema: inviteToProjectFallbackSchema,
})
const projectId = params.Project_id
let { email, privileges } = body
const sendingUser = SessionManager.getSessionUser(req.session)
@@ -189,9 +213,17 @@ async function inviteToProject(req, res) {
})
res.json({ invite })
}
const revokeInviteSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
invite_id: zz.objectId(),
}),
})
async function revokeInvite(req, res) {
const projectId = req.params.Project_id
const inviteId = req.params.invite_id
const { params } = parseReq(req, revokeInviteSchema, { logOnly: true })
const projectId = params.Project_id
const inviteId = params.invite_id
const user = SessionManager.getSessionUser(req.session)
logger.debug({ projectId, inviteId }, 'revoking invite')
@@ -223,9 +255,19 @@ async function revokeInvite(req, res) {
res.sendStatus(204)
}
const generateNewInviteSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
invite_id: zz.objectId(),
}),
})
async function generateNewInvite(req, res) {
const projectId = req.params.Project_id
const inviteId = req.params.invite_id
const { params } = parseReq(req, generateNewInviteSchema, {
logOnly: true,
})
const projectId = params.Project_id
const inviteId = params.invite_id
const user = SessionManager.getSessionUser(req.session)
logger.debug({ projectId, inviteId }, 'resending invite')
@@ -275,9 +317,17 @@ const _renderInvalidPage = function (res, projectId, sharingUpdates) {
}
}
const viewInviteSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
token: z.string(),
}),
})
async function viewInvite(req, res) {
const projectId = req.params.Project_id
const { token } = req.params
const { params } = parseReq(req, viewInviteSchema, { logOnly: true })
const projectId = params.Project_id
const { token } = params
const { variant: sharingUpdates } =
await SplitTestHandler.promises.getAssignment(req, res, 'sharing-updates')
@@ -361,8 +411,15 @@ async function viewInvite(req, res) {
}
}
const viewSharingLinkSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
}),
})
async function viewSharingLink(req, res) {
const projectId = req.params.Project_id
const { params } = parseReq(req, viewSharingLinkSchema, { logOnly: true })
const projectId = params.Project_id
// ensure the project exists
const project = await ProjectGetter.promises.getProject(projectId, {
@@ -397,9 +454,22 @@ async function viewSharingLink(req, res) {
})
}
const acceptInviteSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
token: z.string().optional(),
}),
body: z.strictObject({
token: z.string().optional(),
}),
})
async function acceptInvite(req, res) {
const { Project_id: projectId, token: urlToken } = req.params
const { token: bodyToken } = req.body
const { params, body } = parseReq(req, acceptInviteSchema, {
logOnly: true,
})
const { Project_id: projectId, token: urlToken } = params
const { token: bodyToken } = body
const currentUser = SessionManager.getSessionUser(req.session)
logger.debug(
{ projectId, userId: currentUser._id },
@@ -519,8 +589,16 @@ async function acceptInvite(req, res) {
}
}
const getSharingLinkSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
}),
})
async function getSharingLink(req, res) {
const { Project_id: projectId } = req.params
const { Project_id: projectId } = parseReq(req, getSharingLinkSchema, {
logOnly: true,
}).params
const invite =
await CollaboratorsInviteGetter.promises.getSharingLinkInvite(projectId)
@@ -541,6 +619,26 @@ async function getSharingLink(req, res) {
}
const updateSharingLinkSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
}),
body: z.strictObject({
// We have to use a union here, as Zod enums must be strings,
// but NONE is defined as boolean false
privileges: z.union([
z.literal(PrivilegeLevels.NONE),
z.enum([
PrivilegeLevels.READ_ONLY,
PrivilegeLevels.READ_AND_WRITE,
PrivilegeLevels.REVIEW,
]),
]),
subscriptionId: zz.objectId().optional(),
}),
})
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
const updateSharingLinkFallbackSchema = z.object({
params: z.object({
Project_id: zz.objectId(),
}),
@@ -560,7 +658,9 @@ const updateSharingLinkSchema = z.object({
})
async function updateSharingLink(req, res) {
const { params, body } = parseReq(req, updateSharingLinkSchema)
const { params, body } = parseReq(req, updateSharingLinkSchema, {
fallbackSchema: updateSharingLinkFallbackSchema,
})
const projectId = params.Project_id
const privileges = body.privileges
const subscriptionId = body.subscriptionId
@@ -635,6 +735,16 @@ async function updateSharingLink(req, res) {
}
const validateSharingLinkSchema = z.object({
params: z.strictObject({
Project_id: zz.objectId(),
}),
body: z.strictObject({
token: z.string(),
}),
})
// Rollout-temporary fallback (pre-refinement schema from main); delete
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
const validateSharingLinkFallbackSchema = z.object({
params: z.object({
Project_id: zz.objectId(),
}),
@@ -644,7 +754,9 @@ const validateSharingLinkSchema = z.object({
})
async function validateSharingLink(req, res) {
const { params, body } = parseReq(req, validateSharingLinkSchema)
const { params, body } = parseReq(req, validateSharingLinkSchema, {
fallbackSchema: validateSharingLinkFallbackSchema,
})
const projectId = params.Project_id
const { token } = body