diff --git a/server/routes/api/fileOperations.test.ts b/server/routes/api/fileOperations/fileOperations.test.ts similarity index 100% rename from server/routes/api/fileOperations.test.ts rename to server/routes/api/fileOperations/fileOperations.test.ts diff --git a/server/routes/api/fileOperations.ts b/server/routes/api/fileOperations/fileOperations.ts similarity index 75% rename from server/routes/api/fileOperations.ts rename to server/routes/api/fileOperations/fileOperations.ts index dc05073e0..1fb0b2d04 100644 --- a/server/routes/api/fileOperations.ts +++ b/server/routes/api/fileOperations/fileOperations.ts @@ -1,26 +1,27 @@ import Router from "koa-router"; import { WhereOptions } from "sequelize"; -import { FileOperationType } from "@shared/types"; import fileOperationDeleter from "@server/commands/fileOperationDeleter"; import { ValidationError } from "@server/errors"; import auth from "@server/middlewares/authentication"; +import validate from "@server/middlewares/validate"; import { FileOperation, Team } from "@server/models"; import { authorize } from "@server/policies"; import { presentFileOperation } from "@server/presenters"; import { APIContext } from "@server/types"; import { getSignedUrl } from "@server/utils/s3"; -import { assertIn, assertSort, assertUuid } from "@server/validation"; -import pagination from "./middlewares/pagination"; +import pagination from "../middlewares/pagination"; +import * as T from "./schema"; const router = new Router(); router.post( "fileOperations.info", auth({ admin: true }), - async (ctx: APIContext) => { - const { id } = ctx.request.body; - assertUuid(id, "id is required"); + validate(T.FileOperationsInfoSchema), + async (ctx: APIContext) => { + const { id } = ctx.input.body; const { user } = ctx.state.auth; + const fileOperation = await FileOperation.findByPk(id, { rejectOnEmpty: true, }); @@ -37,16 +38,11 @@ router.post( "fileOperations.list", auth({ admin: true }), pagination(), - async (ctx: APIContext) => { - let { direction } = ctx.request.body; - const { sort = "createdAt", type } = ctx.request.body; - assertIn(type, Object.values(FileOperationType)); - assertSort(sort, FileOperation); - - if (direction !== "ASC") { - direction = "DESC"; - } + validate(T.FileOperationsListSchema), + async (ctx: APIContext) => { + const { direction, sort, type } = ctx.input.body; const { user } = ctx.state.auth; + const where: WhereOptions = { teamId: user.teamId, type, @@ -73,11 +69,12 @@ router.post( } ); -const handleFileOperationsRedirect = async (ctx: APIContext) => { - const id = ctx.request.body?.id ?? ctx.request.query?.id; - assertUuid(id, "id is required"); - +const handleFileOperationsRedirect = async ( + ctx: APIContext +) => { + const id = (ctx.input.body.id ?? ctx.input.query.id) as string; const { user } = ctx.state.auth; + const fileOperation = await FileOperation.unscoped().findByPk(id, { rejectOnEmpty: true, }); @@ -94,22 +91,24 @@ const handleFileOperationsRedirect = async (ctx: APIContext) => { router.get( "fileOperations.redirect", auth({ admin: true }), + validate(T.FileOperationsRedirectSchema), handleFileOperationsRedirect ); router.post( "fileOperations.redirect", auth({ admin: true }), + validate(T.FileOperationsRedirectSchema), handleFileOperationsRedirect ); router.post( "fileOperations.delete", auth({ admin: true }), - async (ctx: APIContext) => { - const { id } = ctx.request.body; - assertUuid(id, "id is required"); - + validate(T.FileOperationsDeleteSchema), + async (ctx: APIContext) => { + const { id } = ctx.input.body; const { user } = ctx.state.auth; + const fileOperation = await FileOperation.unscoped().findByPk(id, { rejectOnEmpty: true, }); diff --git a/server/routes/api/fileOperations/index.ts b/server/routes/api/fileOperations/index.ts new file mode 100644 index 000000000..584b3b5ae --- /dev/null +++ b/server/routes/api/fileOperations/index.ts @@ -0,0 +1 @@ +export { default } from "./fileOperations"; diff --git a/server/routes/api/fileOperations/schema.ts b/server/routes/api/fileOperations/schema.ts new file mode 100644 index 000000000..08ae41dcd --- /dev/null +++ b/server/routes/api/fileOperations/schema.ts @@ -0,0 +1,67 @@ +import { isEmpty } from "lodash"; +import z from "zod"; +import { FileOperationType } from "@shared/types"; +import { FileOperation } from "@server/models"; +import BaseSchema from "../BaseSchema"; + +const CollectionsSortParamsSchema = z.object({ + /** The attribute to sort by */ + sort: z + .string() + .refine((val) => Object.keys(FileOperation.getAttributes()).includes(val), { + message: "Invalid sort parameter", + }) + .default("createdAt"), + + /** The direction of the sorting */ + direction: z + .string() + .optional() + .transform((val) => (val !== "ASC" ? "DESC" : val)), +}); + +export const FileOperationsInfoSchema = BaseSchema.extend({ + body: z.object({ + /** Id of the file operation to be retrieved */ + id: z.string().uuid(), + }), +}); + +export type FileOperationsInfoReq = z.infer; + +export const FileOperationsListSchema = BaseSchema.extend({ + body: CollectionsSortParamsSchema.extend({ + /** File Operation Type */ + type: z.nativeEnum(FileOperationType), + }), +}); + +export type FileOperationsListReq = z.infer; + +export const FileOperationsRedirectSchema = BaseSchema.extend({ + body: z.object({ + /** Id of the file operation to access */ + id: z.string().uuid().optional(), + }), + query: z.object({ + /** Id of the file operation to access */ + id: z.string().uuid().optional(), + }), +}).refine((req) => !(isEmpty(req.body.id) && isEmpty(req.query.id)), { + message: "id is required", +}); + +export type FileOperationsRedirectReq = z.infer< + typeof FileOperationsRedirectSchema +>; + +export const FileOperationsDeleteSchema = BaseSchema.extend({ + body: z.object({ + /** Id of the file operation to delete */ + id: z.string().uuid(), + }), +}); + +export type FileOperationsDeleteReq = z.infer< + typeof FileOperationsDeleteSchema +>;