feat: init inkl. docker configs

This commit is contained in:
betalabor.de
2026-04-24 18:43:42 +02:00
commit c9ef44423c
37 changed files with 10538 additions and 0 deletions

245
src/lib/db.ts Normal file
View File

@@ -0,0 +1,245 @@
import Database from 'better-sqlite3';
import { join, dirname } from 'path';
import { existsSync, mkdirSync } from 'fs';
import { fileURLToPath } from 'url';
import type { CV, CVData, CVSettings } from '../types';
import { DEFAULT_DATA, DEFAULT_SETTINGS } from '../types';
const DB_PATH = import.meta.env.DB_PATH || join(process.cwd(), 'data', 'lebenslauf.db');
const dir = dirname(DB_PATH);
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
let _db: Database.Database | null = null;
export function getDb(): Database.Database {
if (!_db) {
_db = new Database(DB_PATH);
_db.pragma('journal_mode = WAL');
_db.pragma('foreign_keys = ON');
initSchema(_db);
}
return _db;
}
function initSchema(db: Database.Database) {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
otp TEXT,
otp_expires INTEGER,
created_at INTEGER DEFAULT (unixepoch())
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profiles (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
title TEXT NOT NULL DEFAULT 'Mein Profil',
language TEXT NOT NULL DEFAULT 'de',
data TEXT NOT NULL DEFAULT '{}',
created_at INTEGER DEFAULT (unixepoch()),
updated_at INTEGER DEFAULT (unixepoch()),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS cvs (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
profile_id TEXT NOT NULL,
hash TEXT UNIQUE NOT NULL,
title TEXT NOT NULL DEFAULT 'Mein Lebenslauf',
template INTEGER NOT NULL DEFAULT 1,
settings TEXT NOT NULL DEFAULT '{}',
public INTEGER NOT NULL DEFAULT 0,
created_at INTEGER DEFAULT (unixepoch()),
updated_at INTEGER DEFAULT (unixepoch()),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
`);
// Migration for cvs: add profile_id if not exists
try {
const tableInfo = db.prepare('PRAGMA table_info(cvs)').all() as any[];
const hasProfileId = tableInfo.some(col => col.name === 'profile_id');
if (!hasProfileId) {
db.prepare('ALTER TABLE cvs ADD COLUMN profile_id TEXT').run();
// Migrate embedded data into profiles
const allOldCVs = db.prepare('SELECT * FROM cvs WHERE profile_id IS NULL').all() as any[];
for (const old of allOldCVs) {
const profId = Math.random().toString(36).slice(2, 10);
db.prepare('INSERT INTO profiles (id, user_id, title, language, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
.run(profId, old.user_id, old.title + ' (Data)', 'de', old.data || '{}', old.created_at, old.updated_at);
db.prepare('UPDATE cvs SET profile_id = ? WHERE id = ?').run(profId, old.id);
}
}
} catch(e) { console.error("Migration error:", e); }
}
// ── Users ─────────────────────────────────────────────────────────────
export function getUser(email: string) {
const db = getDb();
return db.prepare('SELECT * FROM users WHERE email = ?').get(email) as any;
}
export function upsertUser(email: string) {
const db = getDb();
db.prepare('INSERT OR IGNORE INTO users (email) VALUES (?)').run(email);
return getUser(email);
}
export function setOTP(email: string, otp: string, expiresAt: number) {
const db = getDb();
db.prepare('UPDATE users SET otp = ?, otp_expires = ? WHERE email = ?').run(otp, expiresAt, email);
}
export function consumeOTP(email: string, otp: string): boolean {
const db = getDb();
const now = Math.floor(Date.now() / 1000);
const user = db.prepare(
'SELECT id FROM users WHERE email = ? AND otp = ? AND otp_expires > ?'
).get(email, otp, now) as any;
if (user) {
db.prepare('UPDATE users SET otp = NULL, otp_expires = NULL WHERE email = ?').run(email);
return true;
}
return false;
}
// ── Sessions ──────────────────────────────────────────────────────────
export function createSession(userId: number, sessionId: string, expiresAt: number) {
const db = getDb();
db.prepare('INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)').run(sessionId, userId, expiresAt);
}
export function getSession(sessionId: string) {
const db = getDb();
const now = Math.floor(Date.now() / 1000);
return db.prepare(`
SELECT s.*, u.email, u.id as user_id FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE s.id = ? AND s.expires_at > ?
`).get(sessionId, now) as any;
}
export function deleteSession(sessionId: string) {
const db = getDb();
db.prepare('DELETE FROM sessions WHERE id = ?').run(sessionId);
}
// ── Profiles ──────────────────────────────────────────────────────────
export function getProfilesByUser(userId: number): any[] {
const db = getDb();
const rows = db.prepare('SELECT * FROM profiles WHERE user_id = ? ORDER BY updated_at DESC').all(userId) as any[];
return rows.map(r => ({ ...r, data: JSON.parse(r.data || '{}') }));
}
export function getProfileById(id: string, userId?: number): any | undefined {
const db = getDb();
const row = userId
? db.prepare('SELECT * FROM profiles WHERE id = ? AND user_id = ?').get(id, userId) as any
: db.prepare('SELECT * FROM profiles WHERE id = ?').get(id) as any;
return row ? { ...row, data: JSON.parse(row.data || '{}') } : undefined;
}
export function createProfile(userId: number, id: string, title: string, language: string): any {
const db = getDb();
db.prepare(`
INSERT INTO profiles (id, user_id, title, language, data)
VALUES (?, ?, ?, ?, ?)
`).run(id, userId, title, language, JSON.stringify(DEFAULT_DATA));
return getProfileById(id)!;
}
export function updateProfile(id: string, userId: number, updates: Partial<{ title: string; language: string; data: any; }>) {
const db = getDb();
const fields: string[] = [];
const values: any[] = [];
if (updates.title !== undefined) { fields.push('title = ?'); values.push(updates.title); }
if (updates.language !== undefined) { fields.push('language = ?'); values.push(updates.language); }
if (updates.data !== undefined) { fields.push('data = ?'); values.push(JSON.stringify(updates.data)); }
if (fields.length === 0) return;
fields.push('updated_at = unixepoch()');
values.push(id, userId);
db.prepare(`UPDATE profiles SET ${fields.join(', ')} WHERE id = ? AND user_id = ?`).run(...values);
}
export function deleteProfile(id: string, userId: number) {
const db = getDb();
db.prepare('DELETE FROM profiles WHERE id = ? AND user_id = ?').run(id, userId);
}
// ── CVs ───────────────────────────────────────────────────────────────
export function getCVsByUser(userId: number): CV[] {
const db = getDb();
const rows = db.prepare('SELECT * FROM cvs WHERE user_id = ? ORDER BY updated_at DESC').all(userId) as any[];
return rows.map(parseCV);
}
export function getCVById(id: string, userId?: number): CV | undefined {
const db = getDb();
const row = userId
? db.prepare('SELECT * FROM cvs WHERE id = ? AND user_id = ?').get(id, userId)
: db.prepare('SELECT * FROM cvs WHERE id = ?').get(id);
return row ? parseCV(row as any) : undefined;
}
export function getCVByHash(hash: string): CV | undefined {
const db = getDb();
const row = db.prepare('SELECT * FROM cvs WHERE hash = ? AND public = 1').get(hash);
return row ? parseCV(row as any) : undefined;
}
export function createCV(userId: number, id: string, profile_id: string, hash: string, title: string): CV {
const db = getDb();
db.prepare(`
INSERT INTO cvs (id, user_id, profile_id, hash, title, template, settings)
VALUES (?, ?, ?, ?, ?, 1, ?)
`).run(id, userId, profile_id, hash, title,
JSON.stringify(DEFAULT_SETTINGS)
);
return getCVById(id)!;
}
export function updateCV(id: string, userId: number, updates: Partial<{
title: string; template: number; settings: CVSettings; public: boolean;
}>) {
const db = getDb();
const fields: string[] = [];
const values: any[] = [];
if (updates.title !== undefined) { fields.push('title = ?'); values.push(updates.title); }
if (updates.template !== undefined) { fields.push('template = ?'); values.push(updates.template); }
if (updates.settings !== undefined) { fields.push('settings = ?'); values.push(JSON.stringify(updates.settings)); }
if (updates.public !== undefined) { fields.push('public = ?'); values.push(updates.public ? 1 : 0); }
if (fields.length === 0) return;
fields.push('updated_at = unixepoch()');
values.push(id, userId);
db.prepare(`UPDATE cvs SET ${fields.join(', ')} WHERE id = ? AND user_id = ?`).run(...values);
}
export function deleteCV(id: string, userId: number) {
const db = getDb();
db.prepare('DELETE FROM cvs WHERE id = ? AND user_id = ?').run(id, userId);
}
function parseCV(row: any): CV {
return {
...row,
settings: JSON.parse(row.settings || '{}'),
public: Boolean(row.public),
};
}