Files
porthole/apps/web/app/api/albums/handlers.ts
2026-02-01 18:01:25 -08:00

185 lines
4.6 KiB
TypeScript

import { getAdminToken, isAdminRequest } from "@tline/config";
import { getDb } from "@tline/db";
import { z } from "zod";
const ADMIN_HEADER = "X-Porthole-Admin-Token";
const createAlbumBodySchema = z
.object({
name: z.string().min(1),
})
.strict();
const albumParamsSchema = z.object({
id: z.string().uuid(),
});
const albumAssetBodySchema = z
.object({
assetId: z.string().uuid(),
ord: z.coerce.number().int().optional(),
})
.strict();
type DbLike = ReturnType<typeof getDb>;
export function getAdminOk(headers: Headers) {
const headerToken = headers.get(ADMIN_HEADER);
return isAdminRequest({ adminToken: getAdminToken() }, { headerToken });
}
export async function handleListAlbums(input: {
adminOk: boolean;
db?: DbLike;
}): Promise<{ status: number; body: unknown }> {
if (!input.adminOk) {
return { status: 401, body: { error: "admin_required" } };
}
const db = (input.db ?? getDb()) as DbLike;
const rows = await db<
{
id: string;
name: string;
created_at: string;
}[]
>`
select id, name, created_at
from albums
order by created_at desc
`;
return { status: 200, body: rows };
}
export async function handleCreateAlbum(input: {
adminOk: boolean;
body: unknown;
db?: DbLike;
}): Promise<{ status: number; body: unknown }> {
if (!input.adminOk) {
return { status: 401, body: { error: "admin_required" } };
}
const bodyParsed = createAlbumBodySchema.safeParse(input.body ?? {});
if (!bodyParsed.success) {
return {
status: 400,
body: { error: "invalid_body", issues: bodyParsed.error.issues },
};
}
const body = bodyParsed.data;
const db = (input.db ?? getDb()) as DbLike;
const rows = await db<
{
id: string;
name: string;
created_at: string;
}[]
>`
insert into albums (name)
values (${body.name})
returning id, name, created_at
`;
const created = rows[0];
if (!created) {
return { status: 500, body: { error: "insert_failed" } };
}
const payload = JSON.stringify({ name: created.name });
await db`
insert into audit_log (actor, action, entity_type, entity_id, payload)
values ('admin', 'create', 'album', ${created.id}, ${payload}::jsonb)
`;
return { status: 200, body: created };
}
export async function handleAddAlbumAsset(input: {
adminOk: boolean;
params: { id: string };
body: unknown;
db?: DbLike;
}): Promise<{ status: number; body: unknown }> {
if (!input.adminOk) {
return { status: 401, body: { error: "admin_required" } };
}
const paramsParsed = albumParamsSchema.safeParse(input.params);
if (!paramsParsed.success) {
return {
status: 400,
body: { error: "invalid_params", issues: paramsParsed.error.issues },
};
}
const body = albumAssetBodySchema.parse(input.body ?? {});
const db = (input.db ?? getDb()) as DbLike;
const rows = await db<
{
album_id: string;
asset_id: string;
ord: number | null;
}[]
>`
insert into album_assets (album_id, asset_id, ord)
values (${paramsParsed.data.id}, ${body.assetId}, ${body.ord ?? null})
on conflict (album_id, asset_id)
do update set ord = excluded.ord
returning album_id, asset_id, ord
`;
const created = rows[0];
if (!created) {
return { status: 500, body: { error: "insert_failed" } };
}
const payload = JSON.stringify({
asset_id: created.asset_id,
ord: created.ord,
});
await db`
insert into audit_log (actor, action, entity_type, entity_id, payload)
values ('admin', 'add_asset', 'album', ${created.album_id}, ${payload}::jsonb)
`;
return { status: 200, body: created };
}
export async function handleRemoveAlbumAsset(input: {
adminOk: boolean;
params: { id: string };
body: unknown;
db?: DbLike;
}): Promise<{ status: number; body: unknown }> {
if (!input.adminOk) {
return { status: 401, body: { error: "admin_required" } };
}
const paramsParsed = albumParamsSchema.safeParse(input.params);
if (!paramsParsed.success) {
return {
status: 400,
body: { error: "invalid_params", issues: paramsParsed.error.issues },
};
}
const body = albumAssetBodySchema.parse(input.body ?? {});
const db = (input.db ?? getDb()) as DbLike;
await db`
delete from album_assets
where album_id = ${paramsParsed.data.id}
and asset_id = ${body.assetId}
`;
const payload = JSON.stringify({ asset_id: body.assetId });
await db`
insert into audit_log (actor, action, entity_type, entity_id, payload)
values ('admin', 'remove_asset', 'album', ${paramsParsed.data.id}, ${payload}::jsonb)
`;
return { status: 200, body: { ok: true } };
}