185 lines
4.6 KiB
TypeScript
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 } };
|
|
}
|