feat: scaffold klarbild foundation (astro+postgres+s3, auth, migrations, health)

- Astro 5 SSR (node standalone) + React, OKLCH tokens (no tailwind)
- migrations/001_init.sql: full schema per 03-datenmodell-api
- lib: db+migrations, crypto (AES-256-GCM), auth (argon2+signed session, ratelimit),
  storage (S3/MinIO, presigned URLs), openrouter (POST /v1/images, cost)
- middleware: init-once + session guard + admin gate; /api/health (db+storage)
- login + studio placeholder; seeds (till/lea, default recipes); Dockerfile
- verified: astro build passes
This commit is contained in:
2026-07-23 11:11:08 +00:00
commit b46dbbe889
26 changed files with 9651 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
import { one, query } from './db';
import { hashPassword } from './auth';
/** Legt beim Erststart Nutzer, Standard-Rezepte, Einstellungen und Modelle an. */
export async function seed(): Promise<void> {
// Einstellungen (eine Zeile)
await query(`INSERT INTO settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING`);
// Nutzer
const till = await one(`SELECT id FROM users WHERE username='till'`);
if (!till) {
await query(
`INSERT INTO users (username, role, display_name, password_hash) VALUES ($1,'admin','Till',$2)`,
['till', await hashPassword(process.env.SEED_TILL_PASSWORD || 'klarbild-till')]);
}
const lea = await one(`SELECT id FROM users WHERE username='lea'`);
if (!lea) {
await query(
`INSERT INTO users (username, role, display_name, password_hash) VALUES ($1,'user','Lea',$2)`,
['lea', await hashPassword(process.env.SEED_LEA_PASSWORD || 'klarbild-lea')]);
}
// Standard-Rezepte
const count = await one<{ n: string }>(`SELECT count(*)::text AS n FROM recipes`);
if (count && Number(count.n) === 0) {
const admin = await one<{ id: string }>(`SELECT id FROM users WHERE username='till'`);
const by = admin?.id ?? null;
const R = (name: string, tasks: string[], fmt: string, orient: string, extra: Record<string, unknown> = {}) =>
query(
`INSERT INTO recipes (name, tasks, output_format, orientation, crop_mode, dpi, delivery, is_default, created_by, contour_mm)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
[name, JSON.stringify(tasks), fmt, orient, (extra.crop_mode as string) || 'crop', 300,
(extra.delivery as string) || 'library', !!extra.is_default, by, (extra.contour_mm as number) ?? null]);
await R('Nur bereinigen', ['clean'], 'keep', 'landscape', { is_default: true });
await R('Poster 30×40', ['clean', 'format'], '30x40', 'portrait');
await R('The Frame', ['clean', 'format'], 'theframe', 'landscape');
await R('Sticker 5 cm', ['clean', 'cutout', 'format', 'contour'], 'sticker5', 'landscape',
{ contour_mm: 3 });
}
}