Files
klarbild-oss/src/pages/api/folders/index.ts
T
tillandClaude Opus 5 e2481139a1
CI / check (push) Has been cancelled
Klarbild 1.0.0 — first public release
A self-hosted print studio: images at exact physical sizes, sheet layout with
crop marks, tiling, 1:1 PDF. Everything else — image generation, delivery,
share links, mail, Telegram — is an optional module that is off by default.

This is the neutral public twin of a private tool. Compared to that original:

- optional modules behind KLARBILD_MODULES; the default install is print-only
- English interface with German as a full second language (1,038 strings);
  the English sentence is the translation key
- no built-in accounts — first boot creates one admin from ADMIN_USERNAME /
  ADMIN_PASSWORD, or generates a password and logs it once
- delivery and backup targets are generic SFTP/FTPS endpoints, off by default
- English routes, English identifiers, English comments throughout
- two compose files: print-only by default, everything in the full one
- AGPL-3.0, CI (build, tests, type check, secret scan), CONTRIBUTING, SECURITY

124 tests green, type check clean, verified end to end against a live instance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nh4A79bbqT9dcd2inzYUdg
2026-09-15 14:52:41 +00:00

47 lines
2.3 KiB
TypeScript

import type { APIRoute } from 'astro';
import { one, query } from '../../../lib/db';
export const prerender = false;
const json = (b: unknown, s = 200) =>
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
export const GET: APIRoute = async ({ locals }) => {
if (!locals.user) return new Response('Unauthorized', { status: 401 });
const rows = await query(`SELECT f.*, (SELECT count(*) FROM items WHERE folder_id=f.id)::int AS count
FROM folders f ORDER BY f.name`);
return json({ folders: rows });
};
export const POST: APIRoute = async ({ request, locals }) => {
if (!locals.user) return new Response('Unauthorized', { status: 401 });
const b = await request.json();
if (!b.name?.trim()) return json({ error: 'Name is missing.' }, 400);
const row = await one(`INSERT INTO folders (name, delivery_folder) VALUES ($1,$2) RETURNING *`,
[b.name.trim(), b.delivery_folder || null]);
return json({ folder: row });
};
// Rename a folder / change the folder on the target. Body { id, name?, delivery_folder? }
export const PATCH: APIRoute = async ({ request, locals }) => {
if (!locals.user) return new Response('Unauthorized', { status: 401 });
const b = await request.json();
if (!b.id) return json({ error: 'No id.' }, 400);
const sets: string[] = []; const args: any[] = [];
if (typeof b.name === 'string' && b.name.trim()) { args.push(b.name.trim()); sets.push(`name=$${args.length}`); }
if ('delivery_folder' in b) { args.push(b.delivery_folder?.trim() || null); sets.push(`delivery_folder=$${args.length}`); }
if (!sets.length) return json({ error: 'Nothing to change.' }, 400);
args.push(b.id);
const row = await one(`UPDATE folders SET ${sets.join(',')} WHERE id=$${args.length} RETURNING *`, args);
return json({ folder: row });
};
// Delete a folder (the images stay, they only lose the assignment). ?id=…
export const DELETE: APIRoute = async ({ request, url, locals }) => {
if (!locals.user) return new Response('Unauthorized', { status: 401 });
let id = url.searchParams.get('id');
if (!id) { try { id = (await request.json())?.id; } catch { /* no matter */ } }
if (!id) return json({ error: 'No id.' }, 400);
await query('DELETE FROM folders WHERE id=$1', [id]);
return json({ ok: true });
};