Workflow-App/Workflow/app/api/tasks/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

113 lines
4.2 KiB
TypeScript

// app/api/tasks/route.ts
// GET /api/tasks — list with filters
// POST /api/tasks — create a task
//
// Server-side proxy to PocketBase. The browser calls these routes
// over the existing /api/* contract; PB_TOKEN never leaves the
// server.
import { NextRequest, NextResponse } from "next/server";
import { fetchCollection, createRecord, PBRecord } from "@/lib/pb";
export const dynamic = "force-dynamic";
const COLLECTION = "tasks";
const VALID_STATUS = ["backlog", "ready", "in_progress", "blocked", "done", "failed"] as const;
const VALID_PRIORITY = ["low", "normal", "high", "urgent"] as const;
const VALID_TASK_TYPE = ["bizportal_search", "bizportal_reserve", "pdf_generate", "email_send", "custom"] as const;
export async function GET(req: NextRequest) {
const url = new URL(req.url);
const status = url.searchParams.get("status");
const taskType = url.searchParams.get("task_type");
const priority = url.searchParams.get("priority");
const assignee = url.searchParams.get("assignee");
const includeDone = url.searchParams.get("include_done") === "true";
const limit = parseInt(url.searchParams.get("limit") || "200", 10);
const filters: string[] = [];
if (status && status !== "all") {
if (!(VALID_STATUS as readonly string[]).includes(status)) {
return NextResponse.json({ error: `invalid status: ${status}` }, { status: 400 });
}
filters.push(`status="${status}"`);
} else if (!includeDone) {
// Default: don't pull done/failed tasks older than 200 unless asked
filters.push(`(status!="done" && status!="failed")`);
}
if (taskType) {
if (!(VALID_TASK_TYPE as readonly string[]).includes(taskType)) {
return NextResponse.json({ error: `invalid task_type: ${taskType}` }, { status: 400 });
}
filters.push(`task_type="${taskType}"`);
}
if (priority) {
if (!(VALID_PRIORITY as readonly string[]).includes(priority)) {
return NextResponse.json({ error: `invalid priority: ${priority}` }, { status: 400 });
}
filters.push(`priority="${priority}"`);
}
if (assignee) {
filters.push(`assignee="${assignee.replace(/"/g, '\\"')}"`);
}
try {
const result = await fetchCollection<PBRecord>(COLLECTION, {
page: 1,
perPage: Math.min(limit, 500),
// PB on this instance doesn't support sort on the auto-managed
// `created` column. Use priority (desc) then id (desc) — id is
// a nanoid prefix so newer tasks come last when sorted desc, and
// the worker can poll deterministically.
sort: "-priority,-id",
filter: filters.length > 0 ? filters.join(" && ") : undefined,
});
return NextResponse.json(result);
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
let body: any;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "invalid JSON body" }, { status: 400 });
}
const { title, description, task_type, payload, status, priority, assignee, created_by } = body ?? {};
if (!title || typeof title !== "string") {
return NextResponse.json({ error: "title is required" }, { status: 400 });
}
if (!task_type || !(VALID_TASK_TYPE as readonly string[]).includes(task_type)) {
return NextResponse.json({ error: `task_type must be one of: ${VALID_TASK_TYPE.join(", ")}` }, { status: 400 });
}
const finalStatus = status || "backlog";
if (!(VALID_STATUS as readonly string[]).includes(finalStatus)) {
return NextResponse.json({ error: `invalid status: ${finalStatus}` }, { status: 400 });
}
const finalPriority = priority || "normal";
if (!(VALID_PRIORITY as readonly string[]).includes(finalPriority)) {
return NextResponse.json({ error: `invalid priority: ${finalPriority}` }, { status: 400 });
}
try {
const created = await createRecord<PBRecord>(COLLECTION, {
title,
description: description || null,
task_type,
payload: payload || {},
status: finalStatus,
priority: finalPriority,
assignee: assignee || "agent",
retry_count: 0,
created_by: created_by || "user",
});
return NextResponse.json(created, { status: 201 });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}