const Database = require('better-sqlite3') const fs = require('fs') const path = require('path') const { ensureDecrypted } = require('./weekly_crypto') let db = null const getDB = () => { if (!db) throw new Error('Database not initialized') return db } const tryOpen = (dbPath) => { try { const d = new Database(dbPath) try { const row = d.prepare(`PRAGMA integrity_check`).get() const ok = row && (row.integrity_check === 'ok' || row['integrity_check'] === 'ok' || Object.values(row)[0] === 'ok') if (!ok) throw new Error('integrity_check_failed') } catch (e) { throw e } return d } catch (e) { return null } } const initDB = (dbPath, sealPath) => { if (db) return db // Robust Open db = tryOpen(dbPath) if (!db) { // Attempt recovery if (fs.existsSync(dbPath)) { const badName = `${dbPath}.bad-${Date.now()}` try { fs.renameSync(dbPath, badName) } catch {} console.warn(`Database corrupt, renamed to ${badName}`) } // Try decrypt from seal if (sealPath) ensureDecrypted(dbPath, sealPath) db = tryOpen(dbPath) if (!db) { // If still fails, create fresh (previous was renamed or didn't exist) db = new Database(dbPath) } } // Pragma db.pragma('journal_mode = WAL'); db.pragma('synchronous = NORMAL'); db.pragma('busy_timeout = 4000'); // Users db.exec(`CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, pass_salt TEXT, pass_hash TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) )`) // Reports db.exec(`CREATE TABLE IF NOT EXISTS reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, week_id TEXT UNIQUE, title_enc TEXT, title_iv TEXT, title_tag TEXT, summary_enc TEXT, summary_iv TEXT, summary_tag TEXT, content_enc TEXT, content_iv TEXT, content_tag TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) )`) // Events db.exec(`CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, report_id INTEGER, parent_event_id INTEGER, title_enc TEXT, title_iv TEXT, title_tag TEXT, content_enc TEXT, content_iv TEXT, content_tag TEXT, status_code TEXT, status_enc TEXT, status_iv TEXT, status_tag TEXT, deadline TEXT, push_required INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), completed_at TEXT, FOREIGN KEY(report_id) REFERENCES reports(id) )`) // Event Reviews db.exec(`CREATE TABLE IF NOT EXISTS event_reviews ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_id INTEGER, review_enc TEXT, review_iv TEXT, review_tag TEXT, rating INTEGER, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY(event_id) REFERENCES events(id) )`) // Attachments db.exec(`CREATE TABLE IF NOT EXISTS attachments ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_id INTEGER, filename_enc TEXT, filename_iv TEXT, filename_tag TEXT, mime_enc TEXT, mime_iv TEXT, mime_tag TEXT, size INTEGER, chunks INTEGER DEFAULT 1, store_enc TEXT, store_iv TEXT, store_tag TEXT, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY(event_id) REFERENCES events(id) )`) // Versions db.exec(`CREATE TABLE IF NOT EXISTS versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, subject_type TEXT, subject_id INTEGER, snapshot_enc TEXT, snapshot_iv TEXT, snapshot_tag TEXT, created_at TEXT DEFAULT (datetime('now')) )`) // Logs db.exec(`CREATE TABLE IF NOT EXISTS logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, action TEXT, details TEXT, source TEXT, created_at TEXT DEFAULT (datetime('now')) )`) // Email Sends db.exec(`CREATE TABLE IF NOT EXISTS email_sends ( id INTEGER PRIMARY KEY AUTOINCREMENT, day TEXT, report_id INTEGER, event_id INTEGER, category TEXT, created_at TEXT DEFAULT (datetime('now')), UNIQUE(day, report_id, event_id, category) )`) // Event External (Ensure table exists for integrations) db.exec(`CREATE TABLE IF NOT EXISTS event_external ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, ext_id TEXT NOT NULL, event_id INTEGER NOT NULL, created_at TEXT DEFAULT (datetime('now')), UNIQUE(source, ext_id), FOREIGN KEY(event_id) REFERENCES events(id) );`) // Event Relations db.exec(`CREATE TABLE IF NOT EXISTS event_relations ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_id INTEGER NOT NULL, target_event_id INTEGER NOT NULL, relation_type TEXT, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY(event_id) REFERENCES events(id), FOREIGN KEY(target_event_id) REFERENCES events(id) );`) db.exec(`CREATE INDEX IF NOT EXISTS idx_reports_week_id ON reports(week_id)`) db.exec(`CREATE INDEX IF NOT EXISTS idx_events_report_id ON events(report_id)`) db.exec(`CREATE INDEX IF NOT EXISTS idx_events_parent_event_id ON events(parent_event_id)`) db.exec(`CREATE INDEX IF NOT EXISTS idx_event_reviews_event_id ON event_reviews(event_id)`) db.exec(`CREATE INDEX IF NOT EXISTS idx_attachments_event_id ON attachments(event_id)`) db.exec(`CREATE INDEX IF NOT EXISTS idx_versions_subject ON versions(subject_type, subject_id)`) db.exec(`CREATE INDEX IF NOT EXISTS idx_event_external_event_id ON event_external(event_id)`) db.exec(`CREATE INDEX IF NOT EXISTS idx_event_relations_event_id ON event_relations(event_id)`) db.exec(`CREATE INDEX IF NOT EXISTS idx_event_relations_target_event_id ON event_relations(target_event_id)`) return db } module.exports = { initDB, getDB }