From 4649c824323234350ef77d8665cf36855d8454aa Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 16 Sep 2026 10:02:51 +0200 Subject: [PATCH] feat(api): owner card move route using the caller's own token --- src/app/api/owner/cards/[id]/move/route.ts | 69 ++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/app/api/owner/cards/[id]/move/route.ts diff --git a/src/app/api/owner/cards/[id]/move/route.ts b/src/app/api/owner/cards/[id]/move/route.ts new file mode 100644 index 0000000..67c31ea --- /dev/null +++ b/src/app/api/owner/cards/[id]/move/route.ts @@ -0,0 +1,69 @@ +import { getServerPb } from "@/lib/pb/server"; +import { requireSession } from "@/lib/auth/session"; +import { AccessError } from "@/lib/auth/access"; +import { jsonError, jsonOk } from "@/lib/http/json"; +import type { CardRecord } from "@/lib/pb/types"; + +/** + * PATCH /api/owner/cards/:id/move — the owner drops a card in a new place. + * + * Unlike the /api/client/* routes, this one uses the caller's OWN PocketBase + * token, not a superuser's. The owner-only write rule from Plan 1a is what + * authorises the update, so a client manager reaching this route is refused by + * the database even though the handler itself is simple. + * + * That is why there is no field whitelist here: PocketBase is already + * enforcing who may write. Only the two positional fields are forwarded + * regardless, so a stray body field cannot ride along. + */ +export async function PATCH( + request: Request, + context: { params: Promise<{ id: string }> } +): Promise { + try { + const session = await requireSession(); + const { id } = await context.params; + + if (session.role !== "owner") { + return jsonError("Card not found", 404); + } + + let body: unknown; + try { + body = await request.json(); + } catch { + return jsonError("Invalid request body", 400); + } + + const record = body as Record | null; + const column = record?.column; + const position = record?.position; + + if (typeof column !== "string" || !column) { + return jsonError("A destination column is required", 400); + } + if (typeof position !== "number" || !Number.isFinite(position)) { + return jsonError("A numeric position is required", 400); + } + + const pb = await getServerPb(); + + const updated = await pb + .collection("cards") + .update(id, { column, position }); + + return jsonOk({ + card: { + id: updated.id, + column: updated.column, + position: updated.position, + }, + }); + } catch (error) { + if (error instanceof AccessError) { + return jsonError(error.message, error.status); + } + console.error("[owner-move] failed", error); + return jsonError("Could not move the card", 500); + } +} \ No newline at end of file