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), }; }