const Database = require('better-sqlite3'); const bcrypt = require('bcryptjs'); const path = require('path'); const db = new Database(process.env.DB_PATH || '/data/dickendock.db'); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Dashboard-Widgets CREATE TABLE IF NOT EXISTS quick_links ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, title TEXT NOT NULL, url TEXT NOT NULL, icon TEXT NOT NULL DEFAULT 'πŸ”—', sort_order INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, text TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0, sort_order INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, content TEXT NOT NULL DEFAULT '', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Tool: 3D-Kalkulator CREATE TABLE IF NOT EXISTS calendar_feeds ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, url TEXT NOT NULL, color TEXT NOT NULL DEFAULT '#4ecdc4', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS folders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, parent_id INTEGER REFERENCES folders(id) ON DELETE CASCADE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS folder_shares ( id INTEGER PRIMARY KEY AUTOINCREMENT, folder_id INTEGER NOT NULL REFERENCES folders(id) ON DELETE CASCADE, shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, shared_with INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(folder_id, shared_with) ); CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, filename TEXT NOT NULL, originalname TEXT NOT NULL, mimetype TEXT NOT NULL DEFAULT 'application/octet-stream', size INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS file_shares ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, shared_with INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(file_id, shared_with) ); CREATE TABLE IF NOT EXISTS admin_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, bemerkung TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'warteliste', custom_price REAL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS order_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE, calculation_id INTEGER REFERENCES calculations(id) ON DELETE SET NULL, calc_name TEXT NOT NULL, preis_freundschaft REAL NOT NULL, preis_normal REAL NOT NULL, preis_auftrag REAL NOT NULL, stueckzahl INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS calculations ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, gramm REAL NOT NULL, stunden REAL NOT NULL, farben INTEGER NOT NULL DEFAULT 1, materialpreis_pro_gramm REAL NOT NULL DEFAULT 0.01, stromverbrauch_kw REAL NOT NULL DEFAULT 0.15, strompreis_pro_kwh REAL NOT NULL DEFAULT 0.38, druckerpreis REAL NOT NULL DEFAULT 550, gesamtdruckstunden REAL NOT NULL DEFAULT 5000, verschleiss_pro_stunde REAL NOT NULL DEFAULT 0.06, preis_freundschaft REAL NOT NULL, preis_normal REAL NOT NULL, preis_auftrag REAL NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); // Standard-Admin beim ersten Start anlegen // Default admin settings const settingDefaults = [ ['file_max_size_mb', '50'], ['file_max_count', '20'], ['file_allowed_ext', '.pdf,.jpg,.jpeg,.png,.gif,.zip,.txt,.docx,.xlsx,.mp4,.stl,.3mf'], ]; for (const [key, value] of settingDefaults) { if (!db.prepare('SELECT key FROM admin_settings WHERE key=?').get(key)) db.prepare('INSERT INTO admin_settings (key,value) VALUES (?,?)').run(key, value); } if (db.prepare('SELECT COUNT(*) c FROM users').get().c === 0) { db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)') .run('admin', bcrypt.hashSync('admin123', 12), 'admin'); console.log('βœ… Erster Start: Admin angelegt: admin / admin123 β†’ Bitte Passwort sofort Γ€ndern!'); } // ── Migrationen (werden bei jedem Start geprΓΌft) ───────────────────────────── const cols = db.prepare("PRAGMA table_info(calculations)").all().map(r => r.name); if (!cols.includes('image')) db.exec("ALTER TABLE calculations ADD COLUMN image TEXT"); if (!cols.includes('bemerkung')) db.exec("ALTER TABLE calculations ADD COLUMN bemerkung TEXT NOT NULL DEFAULT ''"); // Migrationen orders const ordCols = db.prepare("PRAGMA table_info(orders)").all().map(r => r.name); if (!ordCols.includes('bezahlt')) db.exec("ALTER TABLE orders ADD COLUMN bezahlt INTEGER NOT NULL DEFAULT 0"); if (!ordCols.includes('bezahlt_am')) db.exec("ALTER TABLE orders ADD COLUMN bezahlt_am DATETIME"); // Migrationen order_items // files migrations const fileCols = db.prepare("PRAGMA table_info(files)").all().map(r => r.name); if (!fileCols.includes('folder_id')) db.exec("ALTER TABLE files ADD COLUMN folder_id INTEGER REFERENCES folders(id) ON DELETE SET NULL"); // folder_shares migrations (only if table exists) const fsTableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='folder_shares'").get(); if (fsTableExists) { const fsCols = db.prepare("PRAGMA table_info(folder_shares)").all().map(r => r.name); if (!fsCols.includes('created_at')) db.exec("ALTER TABLE folder_shares ADD COLUMN created_at DATETIME DEFAULT NULL"); } const oiCols = db.prepare("PRAGMA table_info(order_items)").all().map(r => r.name); if (!oiCols.includes('stunden')) db.exec("ALTER TABLE order_items ADD COLUMN stunden REAL NOT NULL DEFAULT 0"); if (!oiCols.includes('custom_price')) db.exec("ALTER TABLE order_items ADD COLUMN custom_price REAL"); if (!oiCols.includes('status')) db.exec("ALTER TABLE order_items ADD COLUMN status TEXT NOT NULL DEFAULT 'warteliste'"); if (!oiCols.includes('qty_warteliste')) db.exec("ALTER TABLE order_items ADD COLUMN qty_warteliste INTEGER NOT NULL DEFAULT 0"); if (!oiCols.includes('qty_in_arbeit')) db.exec("ALTER TABLE order_items ADD COLUMN qty_in_arbeit INTEGER NOT NULL DEFAULT 0"); if (!oiCols.includes('qty_fertig')) db.exec("ALTER TABLE order_items ADD COLUMN qty_fertig INTEGER NOT NULL DEFAULT 0"); // Bestehende EintrΓ€ge: alle StΓΌcke auf Warteliste setzen falls noch keine Qtys gesetzt db.exec("UPDATE order_items SET qty_warteliste=stueckzahl WHERE qty_warteliste=0 AND qty_in_arbeit=0 AND qty_fertig=0"); // Migration: file_shares – Passwort und Zugriffszeit const fShareCols = db.prepare("PRAGMA table_info(file_shares)").all().map(r => r.name); if (fShareCols.length && !fShareCols.includes('password_hash')) db.exec("ALTER TABLE file_shares ADD COLUMN password_hash TEXT DEFAULT NULL"); if (fShareCols.length && !fShareCols.includes('accessed_at')) db.exec("ALTER TABLE file_shares ADD COLUMN accessed_at DATETIME DEFAULT NULL"); // ── Nachrichten-Migrationen ─────────────────────────────────────────────────── if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages'").get()) { db.exec(` CREATE TABLE messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, recipient_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, encrypted_content TEXT NOT NULL, iv TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, read_by_recipient INTEGER NOT NULL DEFAULT 0 ) `); } if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='user_keys'").get()) { db.exec(` CREATE TABLE user_keys ( user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, public_key TEXT NOT NULL, updated_at DATETIME DEFAULT NULL ) `); } if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='pushover_settings'").get()) { db.exec(` CREATE TABLE pushover_settings ( user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, user_key TEXT NOT NULL, app_token TEXT NOT NULL ) `); } // Migration: Soft-Delete-Spalten entfernen (nur Nachrichten ΓΌbernehmen die noch sichtbar waren) const msgCols = db.prepare("PRAGMA table_info(messages)").all().map(r => r.name); if (msgCols.includes('deleted_by_sender')) { db.exec(` CREATE TABLE messages_v2 ( id INTEGER PRIMARY KEY AUTOINCREMENT, sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, recipient_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, encrypted_content TEXT NOT NULL, iv TEXT NOT NULL, created_at DATETIME DEFAULT NULL, read_by_recipient INTEGER NOT NULL DEFAULT 0 ); INSERT INTO messages_v2 (id, sender_id, recipient_id, encrypted_content, iv, created_at, read_by_recipient) SELECT id, sender_id, recipient_id, encrypted_content, iv, created_at, COALESCE(read_by_recipient, 0) FROM messages WHERE deleted_by_sender = 0 AND deleted_by_recipient = 0; DROP TABLE messages; ALTER TABLE messages_v2 RENAME TO messages; `); console.log('βœ… Migration: messages β†’ Hard-Delete (Soft-Delete-Spalten entfernt)'); } // Falls read_by_recipient noch fehlt (sehr alte Installation) const msgCols2 = db.prepare("PRAGMA table_info(messages)").all().map(r => r.name); if (msgCols2.length && !msgCols2.includes('read_by_recipient')) { db.exec("ALTER TABLE messages ADD COLUMN read_by_recipient INTEGER NOT NULL DEFAULT 0"); } module.exports = db;