feat(core): add secure recipe APIs, precise scaling and versioned imports
This commit is contained in:
commit
5d71d32e20
31 files changed
+6293
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import 'dotenv/config';
|
||||
import { hash } from '@node-rs/argon2';
|
||||
import postgres from 'postgres';
|
||||
import { createInterface } from 'node:readline/promises';
|
||||
const username = process.argv[2]?.trim().toLowerCase();
|
||||
if (!username || !/^[a-z0-9._-]{3,80}$/.test(username))
|
||||
throw new Error('Aufruf: npm run admin:create -- BENUTZERNAME (3–80 Zeichen)');
|
||||
async function passwordInput(): Promise<string> {
|
||||
if (!process.stdin.isTTY) {
|
||||
let input = '';
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
return input.trimEnd();
|
||||
}
|
||||
process.stdout.write('Neues Passwort (mind. 12 Zeichen, Eingabe verborgen): ');
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
// Disable terminal echo while readline receives characters.
|
||||
const output = rl as unknown as { _writeToOutput: (s: string) => void };
|
||||
output._writeToOutput = () => {};
|
||||
const value = await rl.question('');
|
||||
rl.close();
|
||||
process.stdout.write('\n');
|
||||
return value;
|
||||
}
|
||||
const password = await passwordInput();
|
||||
if (password.length < 12 || password.length > 200)
|
||||
throw new Error('Passwort muss 12–200 Zeichen enthalten.');
|
||||
const sql = postgres(process.env.DATABASE_URL!, { max: 1 });
|
||||
try {
|
||||
const hashed = await hash(password, {
|
||||
algorithm: 2,
|
||||
memoryCost: 65536,
|
||||
timeCost: 3,
|
||||
parallelism: 1,
|
||||
});
|
||||
await sql`INSERT INTO admins (username, password) VALUES (${username}, ${hashed})`;
|
||||
console.log(`Administrator ${username} erstellt.`);
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'dotenv/config';
|
||||
import { readdir, readFile } from 'node:fs/promises';
|
||||
import postgres from 'postgres';
|
||||
const sql = postgres(process.env.DATABASE_URL!, { max: 1 });
|
||||
try {
|
||||
await sql.begin(async (tx) => {
|
||||
await tx`SELECT pg_advisory_xact_lock(7218194)`;
|
||||
await tx`CREATE TABLE IF NOT EXISTS migrations (name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`;
|
||||
for (const name of (await readdir('migrations')).filter((f) => f.endsWith('.sql')).sort()) {
|
||||
if ((await tx`SELECT name FROM migrations WHERE name = ${name}`).length) continue;
|
||||
await tx.unsafe(await readFile(`migrations/${name}`, 'utf8'));
|
||||
await tx`INSERT INTO migrations (name) VALUES (${name})`;
|
||||
console.log(`Migration angewendet: ${name}`);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { z } from 'zod';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { collectionSchema, recipeSchema } from '../src/lib/recipe';
|
||||
import { exportRecipes } from '../src/lib/import';
|
||||
import { examples } from '../tests/fixtures';
|
||||
await mkdir('public/examples', { recursive: true });
|
||||
await writeFile(
|
||||
'public/examples/hamham.schema.json',
|
||||
JSON.stringify(z.toJSONSchema(collectionSchema, { target: 'draft-2020-12' }), null, 2),
|
||||
);
|
||||
const r = recipeSchema.parse({
|
||||
title: 'Serbische Pljeskavica',
|
||||
subtitle: 'Saftig gegrillt, mit Ajvar und frischen Zwiebeln',
|
||||
description:
|
||||
'Ein Klassiker vom Balkan: würzige Faschierte-Laibchen, außen kräftig geröstet und innen saftig. Dazu passen Lepinja, Ajvar und ein knackiger Salat.',
|
||||
cuisine: 'Serbien',
|
||||
categories: ['Für Gäste'],
|
||||
tags: ['Grillen', 'Familienliebling'],
|
||||
difficulty: 'Einfach',
|
||||
prepMinutes: 20,
|
||||
cookMinutes: 12,
|
||||
restMinutes: 60,
|
||||
servings: 4,
|
||||
published: false,
|
||||
ingredients: [
|
||||
{ id: 'fleisch', name: 'Rinderfaschiertes', amount: 800, unit: 'g', section: 'Pljeskavica' },
|
||||
{
|
||||
id: 'zwiebel',
|
||||
name: 'Zwiebel',
|
||||
amount: 1,
|
||||
unit: 'Stück',
|
||||
note: 'sehr fein gewürfelt',
|
||||
section: 'Pljeskavica',
|
||||
},
|
||||
{ id: 'paprika', name: 'Paprikapulver edelsüß', amount: 2, unit: 'TL', section: 'Pljeskavica' },
|
||||
{ id: 'salz', name: 'Salz', amount: 10, unit: 'g', section: 'Pljeskavica' },
|
||||
{
|
||||
id: 'pfeffer',
|
||||
name: 'Schwarzer Pfeffer',
|
||||
amount: null,
|
||||
unit: '',
|
||||
note: 'nach Geschmack',
|
||||
section: 'Pljeskavica',
|
||||
},
|
||||
{ id: 'wasser', name: 'Mineralwasser', amount: 60, unit: 'ml', section: 'Pljeskavica' },
|
||||
{
|
||||
id: 'ajvar',
|
||||
name: 'Ajvar',
|
||||
amount: 120,
|
||||
unit: 'g',
|
||||
section: 'Zum Servieren',
|
||||
optional: true,
|
||||
},
|
||||
{ id: 'brot', name: 'Lepinja', amount: 4, unit: 'Stück', section: 'Zum Servieren' },
|
||||
],
|
||||
steps: [
|
||||
{
|
||||
id: 'mischen',
|
||||
title: 'Die Mischung vorbereiten',
|
||||
text: '{{fleisch}}, {{zwiebel}}, {{paprika}}, {{salz}} und {{wasser}} gründlich verkneten. Mit {{pfeffer}} abschmecken.',
|
||||
ingredients: ['fleisch', 'zwiebel', 'paprika', 'salz', 'wasser', 'pfeffer'],
|
||||
},
|
||||
{
|
||||
id: 'ruhen',
|
||||
title: 'Geschmack entwickeln lassen',
|
||||
text: 'Die Masse zugedeckt mindestens eine Stunde im Kühlschrank ruhen lassen.',
|
||||
timers: [{ id: 'ruhezeit', label: 'Im Kühlschrank ruhen', seconds: 3600 }],
|
||||
},
|
||||
{
|
||||
id: 'formen',
|
||||
title: 'Flache Laibchen formen',
|
||||
text: 'Mit angefeuchteten Händen je Portion ein etwa 1,5 cm dickes Laibchen formen. In die Mitte eine leichte Mulde drücken.',
|
||||
},
|
||||
{
|
||||
id: 'grillen',
|
||||
title: 'Kräftig grillen',
|
||||
text: 'Grill oder Grillpfanne vorheizen. Laibchen von beiden Seiten je etwa fünf bis sechs Minuten braten. Faschiertes vollständig durchgaren: im Kern mindestens 70 °C für zwei Minuten erreichen.',
|
||||
timers: [
|
||||
{ id: 'erste-seite', label: 'Erste Seite', seconds: 360 },
|
||||
{ id: 'zweite-seite', label: 'Zweite Seite', seconds: 360 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'servieren',
|
||||
title: 'Ab an den Tisch',
|
||||
text: 'In {{brot}} mit {{ajvar}} servieren. Nach Wunsch frische Zwiebelringe dazu reichen.',
|
||||
ingredients: ['brot', 'ajvar'],
|
||||
},
|
||||
],
|
||||
tips: 'Nicht während des Bratens flachdrücken – so bleibt die Pljeskavica saftig. Für Schärfe etwas Chiliflocken zur Masse geben.',
|
||||
storage:
|
||||
'Reste rasch abkühlen, abgedeckt im Kühlschrank aufbewahren und innerhalb von zwei Tagen verbrauchen. Vor dem Essen gründlich erhitzen.',
|
||||
});
|
||||
await writeFile('public/examples/pljeskavica.json', exportRecipes([r], 'json'));
|
||||
await writeFile('public/examples/pljeskavica.xml', exportRecipes([r], 'xml'));
|
||||
await writeFile('public/examples/kochbuch.json', exportRecipes(examples, 'json'));
|
||||
console.log('JSON Schema und vollständige JSON/XML-Beispiele erzeugt.');
|
||||
Reference in new issue
Block a user