mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-09-30 03:23:16 +00:00
* feat(api): serve portal-shaped health and error envelope
Move liveness to GET /api/health { stage, sha } with a Node 24 image on 8080 so ALB probes and deploy verify do not need auth or a database ping.
* feat(api): switch live auth to host cookie BFF
Replace Bearer as the documented session path with Cognito hosted UI plus __Host-ap_* cookies so the SPA can call /api with credentials include.
* feat(web): add unused cookie SPA API client
Land a credentials-include fetch helper and hand-synced health/me types without wiring pages or domain hooks, so mocks stay the default data path.
* feat(api): add master-data OpenAPI and Hono stubs
* feat(api): add invoice, line, and document stubs
* feat(api): add approval policy, inbox, and activity stubs
* test(web): fix SPA client fetch mock types
* test(web): cast fetch mock call args for tsc
* fix(api): do not default DEV_AUTH_BYPASS outside local migrate
* fix(api): replace invoice lines in a single transaction
* fix(api): create invoices and lines in one transaction
* fix(api): inline GIT_SHA from the image build arg
* fix(api): stop PATCH from skipping the approval workflow
* fix(api): address review feedback
* fix(ci): format upsert-user test
* fix(api): document only the auth statuses the routes return
* fix(api): drop health 400 responses the routes never return
80 lines
3 KiB
TypeScript
80 lines
3 KiB
TypeScript
import { eq } from "drizzle-orm";
|
|
import { Hono } from "hono";
|
|
import type { Db } from "../db/client.js";
|
|
import { departments } from "../db/schema/index.js";
|
|
import type { AppBindings } from "../auth/middleware.js";
|
|
import { errorJson } from "../http.js";
|
|
import { asString, iso, isUuid, parseJsonBody, requireCan } from "./helpers.js";
|
|
|
|
function toDepartment(row: typeof departments.$inferSelect) {
|
|
return {
|
|
id: row.id,
|
|
code: row.code,
|
|
name: row.name,
|
|
createdAt: iso(row.createdAt),
|
|
updatedAt: iso(row.updatedAt),
|
|
};
|
|
}
|
|
|
|
export function createDepartmentRoutes(handle: Db) {
|
|
const routes = new Hono<AppBindings>();
|
|
|
|
routes.get("/departments", async (c) => {
|
|
const denied = requireCan(c, "read:invoices");
|
|
if (denied) return denied;
|
|
const rows = await handle.db.select().from(departments);
|
|
return c.json({ items: rows.map(toDepartment) });
|
|
});
|
|
|
|
routes.get("/departments/:id", async (c) => {
|
|
const denied = requireCan(c, "read:invoices");
|
|
if (denied) return denied;
|
|
const id = c.req.param("id");
|
|
if (!isUuid(id)) return errorJson(c, 400, "VALIDATION_ERROR", "Invalid department id.");
|
|
const row = await handle.db.query.departments.findFirst({ where: eq(departments.id, id) });
|
|
if (!row) return errorJson(c, 404, "NOT_FOUND", "Department not found.");
|
|
return c.json(toDepartment(row));
|
|
});
|
|
|
|
routes.post("/departments", async (c) => {
|
|
const denied = requireCan(c, "admin:settings");
|
|
if (denied) return denied;
|
|
const body = await parseJsonBody(c);
|
|
const code = asString(body.code).trim();
|
|
const name = asString(body.name).trim();
|
|
if (!code || !name) return errorJson(c, 400, "VALIDATION_ERROR", "Code and name are required.");
|
|
const [row] = await handle.db.insert(departments).values({ code, name }).returning();
|
|
return c.json(toDepartment(row), 201);
|
|
});
|
|
|
|
routes.patch("/departments/:id", async (c) => {
|
|
const denied = requireCan(c, "admin:settings");
|
|
if (denied) return denied;
|
|
const id = c.req.param("id");
|
|
if (!isUuid(id)) return errorJson(c, 400, "VALIDATION_ERROR", "Invalid department id.");
|
|
const existing = await handle.db.query.departments.findFirst({
|
|
where: eq(departments.id, id),
|
|
});
|
|
if (!existing) return errorJson(c, 404, "NOT_FOUND", "Department not found.");
|
|
const body = await parseJsonBody(c);
|
|
const patch: Partial<typeof departments.$inferInsert> = { updatedAt: new Date() };
|
|
if (body.code !== undefined) {
|
|
const code = asString(body.code).trim();
|
|
if (!code) return errorJson(c, 400, "VALIDATION_ERROR", "Code is required.");
|
|
patch.code = code;
|
|
}
|
|
if (body.name !== undefined) {
|
|
const name = asString(body.name).trim();
|
|
if (!name) return errorJson(c, 400, "VALIDATION_ERROR", "Name is required.");
|
|
patch.name = name;
|
|
}
|
|
const [row] = await handle.db
|
|
.update(departments)
|
|
.set(patch)
|
|
.where(eq(departments.id, id))
|
|
.returning();
|
|
return c.json(toDepartment(row));
|
|
});
|
|
|
|
return routes;
|
|
}
|