diff --git a/src/app/api/v1/document-folders/[id]/route.ts b/src/app/api/v1/document-folders/[id]/route.ts new file mode 100644 index 00000000..3b788822 --- /dev/null +++ b/src/app/api/v1/document-folders/[id]/route.ts @@ -0,0 +1,55 @@ +import { NextResponse } from 'next/server'; +import { z } from 'zod'; + +import { withAuth, withPermission } from '@/lib/api/helpers'; +import { parseBody } from '@/lib/api/route-helpers'; +import { errorResponse, NotFoundError } from '@/lib/errors'; +import { + renameFolderSchema, + moveFolderSchema, +} from '@/lib/validators/document-folders'; +import { + renameFolder, + moveFolder, + deleteFolderSoftRescue, +} from '@/lib/services/document-folders.service'; + +/** + * PATCH supports either { name } (rename) or { parentId } (move). + * Refuses both in the same body — keeps the audit log clean + * (one operation per call) and prevents the rep from accidentally + * doing two unrelated changes in one click. + */ +const patchBodySchema = z.union([renameFolderSchema, moveFolderSchema]); + +export const PATCH = withAuth( + withPermission('documents', 'manage_folders', async (req, ctx, params) => { + try { + const folderId = params.id; + if (!folderId) throw new NotFoundError('Folder'); + const body = await parseBody(req, patchBodySchema); + let updated; + if ('name' in body) { + updated = await renameFolder(ctx.portId, folderId, body.name, ctx.userId); + } else { + updated = await moveFolder(ctx.portId, folderId, body.parentId, ctx.userId); + } + return NextResponse.json({ data: updated }); + } catch (error) { + return errorResponse(error); + } + }), +); + +export const DELETE = withAuth( + withPermission('documents', 'manage_folders', async (_req, ctx, params) => { + try { + const folderId = params.id; + if (!folderId) throw new NotFoundError('Folder'); + await deleteFolderSoftRescue(ctx.portId, folderId, ctx.userId); + return new NextResponse(null, { status: 204 }); + } catch (error) { + return errorResponse(error); + } + }), +); diff --git a/src/app/api/v1/document-folders/route.ts b/src/app/api/v1/document-folders/route.ts new file mode 100644 index 00000000..c0d48c83 --- /dev/null +++ b/src/app/api/v1/document-folders/route.ts @@ -0,0 +1,47 @@ +import { NextResponse } from 'next/server'; + +import { withAuth, withPermission } from '@/lib/api/helpers'; +import { parseBody } from '@/lib/api/route-helpers'; +import { errorResponse } from '@/lib/errors'; +import { createFolderSchema } from '@/lib/validators/document-folders'; +import { listTree, createFolder } from '@/lib/services/document-folders.service'; + +/** + * GET /api/v1/document-folders + * + * Returns the entire folder tree for the caller's port. Roots come + * back at the top level with `children` nested. Cached on the client + * via TanStack — folders change rarely; the manager mutations + * invalidate the query. + * + * Permission: documents.view (read-only; everyone in the port can + * browse the tree even if they can't manage it). + */ +export const GET = withAuth( + withPermission('documents', 'view', async (_req, ctx) => { + try { + const tree = await listTree(ctx.portId); + return NextResponse.json({ data: tree }); + } catch (error) { + return errorResponse(error); + } + }), +); + +/** + * POST /api/v1/document-folders + * Body: { name, parentId } + * + * Permission: documents.manage_folders. + */ +export const POST = withAuth( + withPermission('documents', 'manage_folders', async (req, ctx) => { + try { + const body = await parseBody(req, createFolderSchema); + const folder = await createFolder(ctx.portId, ctx.userId, body); + return NextResponse.json({ data: folder }, { status: 201 }); + } catch (error) { + return errorResponse(error); + } + }), +);