feat(db): drizzle-schema, db-client und initiale migration
Vollstaendiges Datenmodell: Katalog (manufacturer, theme, set, tag, instruction), Besitz (collection_item, location, missing_part, photo), Wunschlisten (wishlist, wishlist_item) und die Auth-Tabellen von better-auth inklusive passkey und invite. Kernregel: ein set ist ein Katalogeintrag, ein collection_item die Verknuepfung mit dem tatsaechlichen Besitz. Preise liegen als Cent in INTEGER-Spalten, damit Summen exakt bleiben. SQLite laeuft mit WAL, foreign_keys=ON und busy_timeout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+72
@@ -0,0 +1,72 @@
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Central, validated access to the runtime configuration.
|
||||
*
|
||||
* Everything the app needs to know about its environment is derived here so
|
||||
* that a misconfigured container fails fast and loudly instead of misbehaving
|
||||
* at request time. Only imported from server-side code and scripts.
|
||||
*/
|
||||
const schema = z.object({
|
||||
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
||||
|
||||
/**
|
||||
* Public origin the app is reached under, e.g. https://bricktrack.example.de.
|
||||
* TLS is terminated by the reverse proxy; this value is what the browser sees
|
||||
* and therefore defines the WebAuthn relying party ID.
|
||||
*/
|
||||
BASE_URL: z.url().default('http://localhost:3000'),
|
||||
|
||||
/** Signing secret for sessions and tokens. */
|
||||
BETTER_AUTH_SECRET: z.string().min(32, 'BETTER_AUTH_SECRET braucht mindestens 32 Zeichen'),
|
||||
|
||||
/** Single directory that holds everything worth backing up. */
|
||||
DATA_DIR: z.string().min(1).default('./data'),
|
||||
|
||||
/** Upload size limit per file, in megabytes. */
|
||||
MAX_UPLOAD_MB: z.coerce.number().int().positive().max(200).default(25),
|
||||
|
||||
/** Optional, phase 2: Rebrickable set import. Import stays disabled without it. */
|
||||
REBRICKABLE_API_KEY: z.string().min(1).optional(),
|
||||
});
|
||||
|
||||
function parseEnv() {
|
||||
const parsed = schema.safeParse(process.env);
|
||||
if (!parsed.success) {
|
||||
const issues = parsed.error.issues
|
||||
.map((issue) => ` - ${issue.path.join('.')}: ${issue.message}`)
|
||||
.join('\n');
|
||||
throw new Error(`Ungueltige Umgebungsvariablen:\n${issues}`);
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
const raw = parseEnv();
|
||||
|
||||
const dataDir = path.resolve(raw.DATA_DIR);
|
||||
const baseUrl = new URL(raw.BASE_URL);
|
||||
|
||||
export const env = {
|
||||
...raw,
|
||||
BASE_URL: baseUrl.origin,
|
||||
|
||||
/** Absolute paths derived from the single data directory. */
|
||||
DATA_DIR: dataDir,
|
||||
DB_DIR: path.join(dataDir, 'db'),
|
||||
DB_PATH: path.join(dataDir, 'db', 'bricktrack.db'),
|
||||
FILES_DIR: path.join(dataDir, 'files'),
|
||||
|
||||
/**
|
||||
* WebAuthn relying party ID: the registrable domain of BASE_URL. Passkeys are
|
||||
* bound to it, so changing the domain invalidates existing credentials.
|
||||
*/
|
||||
RP_ID: baseUrl.hostname,
|
||||
RP_NAME: 'Bricktrack',
|
||||
|
||||
MAX_UPLOAD_BYTES: raw.MAX_UPLOAD_MB * 1024 * 1024,
|
||||
|
||||
isProduction: raw.NODE_ENV === 'production',
|
||||
} as const;
|
||||
|
||||
export type Env = typeof env;
|
||||
Reference in New Issue
Block a user