diff --git a/src/app/api/assignments/[id]/route.ts b/src/app/api/assignments/[id]/route.ts new file mode 100644 index 0000000..08cf615 --- /dev/null +++ b/src/app/api/assignments/[id]/route.ts @@ -0,0 +1,54 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; +import { auth } from "@/auth"; +import { returnAssignment, validateReturnAssignmentInput } from "@/lib/assignments"; + +// First dynamic-segment ([id]) route in the codebase — see +// docs/asset-license-management.md's "Assignment (chốt 2026-08-02)" section, +// bullet (b): update routes use PATCH /api//[id], not an +// action-based sub-route (e.g. NOT /api/assignments/[id]/return). Next.js +// 14 App Router route handler params are a plain synchronous object (not a +// Promise) — that changed starting in Next.js 15, which this project does +// not use (see package.json / CLAUDE.md). +export async function PATCH( + request: Request, + { params }: { params: { id: string } } +): Promise { + const session = await auth(); + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Validate the [id] path segment's format up front — an invalid UUID + // passed straight to Prisma's `findUnique({ where: { id } })` (a `@db.Uuid` + // column) throws a raw PrismaClientKnownRequestError that + // returnAssignment()'s try/catch doesn't specifically handle, which would + // otherwise leak as an uncaught 500 instead of a clean 400. + if (!z.string().uuid().safeParse(params.id).success) { + return NextResponse.json({ error: "id must be a valid UUID string" }, { status: 400 }); + } + + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Request body must be valid JSON" }, { status: 400 }); + } + + if (typeof body !== "object" || body === null) { + return NextResponse.json({ error: "Request body must be a JSON object" }, { status: 400 }); + } + + const { returnedAt } = body as Record; + const validation = validateReturnAssignmentInput({ returnedAt }); + + if (!validation.ok) { + return NextResponse.json({ error: validation.error }, { status: 400 }); + } + + const updated = await returnAssignment(params.id, validation.value); + if (!updated.ok) { + return NextResponse.json({ error: updated.error }, { status: updated.status }); + } + return NextResponse.json(updated.value, { status: 200 }); +} diff --git a/src/app/api/assignments/route.ts b/src/app/api/assignments/route.ts new file mode 100644 index 0000000..904c4ac --- /dev/null +++ b/src/app/api/assignments/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { + createAssignment, + listAssignments, + validateCreateAssignmentInput, +} from "@/lib/assignments"; + +// Mirrors src/app/api/licenses/route.ts's pattern — middleware.ts excludes +// /api from route protection, so this checks the session itself. Flat +// permission model (CLAUDE.md): only "logged in or not", no role check. +export async function POST(request: Request): Promise { + const session = await auth(); + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Request body must be valid JSON" }, { status: 400 }); + } + + if (typeof body !== "object" || body === null) { + return NextResponse.json({ error: "Request body must be a JSON object" }, { status: 400 }); + } + + const { employeeId, assetItemId, licenseId } = body as Record; + const validation = validateCreateAssignmentInput({ employeeId, assetItemId, licenseId }); + + if (!validation.ok) { + return NextResponse.json({ error: validation.error }, { status: 400 }); + } + + const created = await createAssignment(validation.value); + if (!created.ok) { + return NextResponse.json({ error: created.error }, { status: 400 }); + } + return NextResponse.json(created.value, { status: 201 }); +} + +export async function GET(): Promise { + const session = await auth(); + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const assignments = await listAssignments(); + return NextResponse.json(assignments, { status: 200 }); +}