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.
113 lines
4.2 KiB
TypeScript
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 });
|
|
}
|
|
}
|