feat: init inkl. docker configs
This commit is contained in:
245
src/lib/db.ts
Normal file
245
src/lib/db.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user