import { Body, Controller, Delete, Get, Inject, Param, Patch, Post, Put, Query, Req, Res } from "@nestjs/common"; import type { Request, Response } from "express"; import { FilesService } from "./files.service.js"; import { ZodValidate } from "../../common/pipes/zod-validation.pipe.js"; import { requireActor } from "../../common/types.js"; import { ListFilesQuery, type ListFilesInput, RenameFileDto, type RenameFileInput, RollbackFileDto, type RollbackFileInput, SetPermissionsDto, type SetPermissionsInput, } from "./dto.js"; /** * TASK-0101/0103/0104 — files: * reads GET /files (list) · GET /files/:id (detail) · GET /files/:id/download * GET /files/:id/preview · PATCH /files/:id (rename) * versions GET /files/:id/versions · POST /files/:id/rollback * POST /files/:id/versions/prune (delete old versions) * trash GET /files/trash · DELETE /files/:id (soft) · DELETE /files/:id?purge=true * POST /files/:id/restore * perms GET /files/:id/permissions · PUT /files/:id/permissions * * ROUTE ORDER: `files/trash` MUST be declared before `files/:id` — the router * matches in registration order, otherwise "trash" is captured as an id. * * `download`/`preview` redirect via the raw Express response — the global * exception filter would otherwise JSON-encode every thrown error, which is * exactly what we want for permission failures (403 envelope + audit), but a * 302 must reach the client as a real redirect. */ @Controller() export class FilesController { constructor(@Inject(FilesService) private readonly files: FilesService) {} @Get("files") list(@Query(ZodValidate(ListFilesQuery)) query: ListFilesInput, @Req() req: Request) { const workspaceId = req.auth?.workspaceId; if (!workspaceId) { return { data: [], nextCursor: null }; } return this.files.list(workspaceId, requireActor(req), query); } /** TASK-0104 — workspace trash (deletedAt set). BEFORE `files/:id`. */ @Get("files/trash") trash(@Req() req: Request) { const workspaceId = req.auth?.workspaceId; if (!workspaceId) return []; return this.files.trashList(workspaceId, requireActor(req)); } @Get("files/:id") get(@Param("id") id: string, @Req() req: Request) { return this.files.get(id, requireActor(req), req); } @Get("files/:id/download") download(@Param("id") id: string, @Req() req: Request, @Res() res: Response) { // A rejection (403/404/410/415) propagates to the global exception // filter, which writes the canonical envelope — nothing is sent to the // socket before that point, so the redirect and the error paths are // mutually exclusive. return this.files .download(id, requireActor(req), req) .then((r) => res.redirect(302, r.location)); } @Get("files/:id/preview") preview(@Param("id") id: string, @Req() req: Request, @Res() res: Response) { return this.files .preview(id, requireActor(req), req) .then((r) => res.redirect(302, r.location)); } /** TASK-0104 — version list. */ @Get("files/:id/versions") versions(@Param("id") id: string, @Req() req: Request) { return this.files.versions(id, requireActor(req), req); } /** TASK-0104 — make version n current (key swap). */ @Post("files/:id/rollback") rollback( @Param("id") id: string, @Body(ZodValidate(RollbackFileDto)) body: RollbackFileInput, @Req() req: Request, ) { return this.files.rollback(id, requireActor(req), body, req); } /** TASK-0104 — delete old versions (admin "Delete old versions"). */ @Post("files/:id/versions/prune") prune(@Param("id") id: string, @Req() req: Request) { return this.files.deleteOldVersions(id, requireActor(req), req); } /** TASK-0104 — trash (soft) or purge (hard, `?purge=true`). */ @Delete("files/:id") remove(@Param("id") id: string, @Query("purge") purge: string | undefined, @Req() req: Request) { return this.files.remove(id, requireActor(req), req, purge === "true" || purge === "1"); } /** TASK-0104 — restore from trash. */ @Post("files/:id/restore") restore(@Param("id") id: string, @Req() req: Request) { return this.files.restore(id, requireActor(req), req); } /** TASK-0104 — file-level permission overrides (list). */ @Get("files/:id/permissions") listPermissions(@Param("id") id: string, @Req() req: Request) { return this.files.listPermissions(id, requireActor(req), req); } /** TASK-0104 — file-level permission overrides (replace). */ @Put("files/:id/permissions") setPermissions( @Param("id") id: string, @Body(ZodValidate(SetPermissionsDto)) body: SetPermissionsInput, @Req() req: Request, ) { return this.files.setPermissions(id, requireActor(req), body, req); } @Patch("files/:id") rename( @Param("id") id: string, @Body(ZodValidate(RenameFileDto)) body: RenameFileInput, @Req() req: Request, ) { return this.files.rename(id, requireActor(req), body, req); } }