Workflow-App/Workflow/app/api/tasks/[id]/route.ts
sjcoetzeesj-coder 11d0178ab1 feat: commit Workflow app source; move CIPC credentials to env
Adds the full Next.js app (app/, components/, lib/, scripts/, assets/,
docs/, Dockerfile, compose, configs) to version control.

Security:
- CIPC BizPortal customer-code logins (Login 1 + Login 3) now read
  CIPC_LOGIN1_CODE / CIPC_LOGIN1_PASSWORD / CIPC_LOGIN3_CODE /
  CIPC_LOGIN3_PASSWORD from .env.local instead of source.
- BizPortal personal login in apply-bizportal route reads
  BIZPORTAL_ID_NO / BIZPORTAL_PASSWORD.
- Added .env.example as the committed template.

.gitignore excludes .env*, node_modules, .next, logs, sqlite, Playwright
storage_state.json (live CIPC cookies), scratch dirs, client PDFs and
debug screenshots.
2026-10-18 15:21:45 +02:00

94 lines
3.1 KiB
TypeScript

// app/api/tasks/[id]/route.ts
// GET /api/tasks/[id] — single task
// PATCH /api/tasks/[id] — partial update (used by UI for edits + column moves)
// DELETE /api/tasks/[id] — soft delete (archive)
import { NextRequest, NextResponse } from "next/server";
import { fetchRecord, updateRecord, deleteRecord, PBRecord } from "@/lib/pb";
export const dynamic = "force-dynamic";
const COLLECTION = "tasks";
const EDITABLE = new Set([
"title",
"description",
"priority",
"payload",
"assignee",
]);
const STATUS_TRANSITIONS: Record<string, string[]> = {
backlog: ["ready", "blocked", "in_progress"],
ready: ["backlog", "in_progress", "blocked"],
in_progress: ["blocked", "done", "failed"],
blocked: ["backlog", "ready", "in_progress"],
done: ["backlog"], // can re-queue if needed
failed: ["backlog", "ready"], // can re-queue
};
export async function GET(_req: NextRequest, { params }: { params: { id: string } }) {
try {
const rec = await fetchRecord<PBRecord>(COLLECTION, params.id);
return NextResponse.json(rec);
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
export async function PATCH(req: NextRequest, { params }: { params: { id: string } }) {
let body: any;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "invalid JSON body" }, { status: 400 });
}
// Whitelist fields the UI is allowed to set directly. The agent
// uses /claim, /complete, /fail endpoints for state transitions.
const fields: Record<string, unknown> = {};
for (const [k, v] of Object.entries(body ?? {})) {
if (EDITABLE.has(k)) fields[k] = v;
}
// Status changes from the UI are allowed but only along valid
// edges — drag-and-drop an "in_progress" card to "backlog" silently
// no-ops (the UI should use the explicit /cancel endpoint).
if (typeof body.status === "string") {
if (!Object.prototype.hasOwnProperty.call(STATUS_TRANSITIONS, body.status)) {
return NextResponse.json(
{ error: `invalid status: ${body.status}` },
{ status: 400 },
);
}
const current = await fetchRecord<PBRecord>(COLLECTION, params.id);
const allowed = STATUS_TRANSITIONS[current.status as string] || [];
if (!allowed.includes(body.status)) {
return NextResponse.json(
{ error: `cannot transition from '${current.status}' to '${body.status}' via PATCH` },
{ status: 409 },
);
}
fields.status = body.status;
}
if (Object.keys(fields).length === 0) {
return NextResponse.json({ error: "no editable fields supplied" }, { status: 400 });
}
try {
const updated = await updateRecord<PBRecord>(COLLECTION, params.id, fields);
return NextResponse.json(updated);
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
export async function DELETE(_req: NextRequest, { params }: { params: { id: string } }) {
try {
await deleteRecord(COLLECTION, params.id);
return NextResponse.json({ ok: true });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}