feat: add tags/albums UI

This commit is contained in:
William Valentin
2026-02-02 19:46:24 -08:00
parent e455425d2e
commit eb712ac9e9
4 changed files with 565 additions and 3 deletions

View File

@@ -0,0 +1,78 @@
import { getAdminToken, isAdminRequest } from "@tline/config";
import { getDb } from "@tline/db";
import { z } from "zod";
export const runtime = "nodejs";
const ADMIN_HEADER = "X-Porthole-Admin-Token";
const paramsSchema = z.object({
id: z.string().uuid(),
});
const bodySchema = z
.object({
tagId: z.string().uuid(),
})
.strict();
function getAdminOk(headers: Headers) {
const headerToken = headers.get(ADMIN_HEADER);
return isAdminRequest({ adminToken: getAdminToken() }, { headerToken });
}
export async function POST(
request: Request,
context: { params: Promise<{ id: string }> },
): Promise<Response> {
if (!getAdminOk(request.headers)) {
return Response.json({ error: "admin_required" }, { status: 401 });
}
const rawParams = await context.params;
const paramsParsed = paramsSchema.safeParse(rawParams);
if (!paramsParsed.success) {
return Response.json(
{ error: "invalid_params", issues: paramsParsed.error.issues },
{ status: 400 },
);
}
const bodyJson = await request.json().catch(() => ({}));
const bodyParsed = bodySchema.safeParse(bodyJson);
if (!bodyParsed.success) {
return Response.json(
{ error: "invalid_body", issues: bodyParsed.error.issues },
{ status: 400 },
);
}
const db = getDb();
const rows = await db<
{
asset_id: string;
tag_id: string;
}[]
>`
insert into asset_tags (asset_id, tag_id)
values (${paramsParsed.data.id}, ${bodyParsed.data.tagId})
on conflict (asset_id, tag_id)
do nothing
returning asset_id, tag_id
`;
const created =
rows[0] ??
({ asset_id: paramsParsed.data.id, tag_id: bodyParsed.data.tagId } as const);
const payload = JSON.stringify({
asset_id: created.asset_id,
tag_id: created.tag_id,
});
await db`
insert into audit_log (actor, action, entity_type, entity_id, payload)
values ('admin', 'add_tag', 'asset', ${created.asset_id}, ${payload}::jsonb)
`;
return Response.json(created, { status: 200 });
}