const Database = require('better-sqlite3'); const path = require('path'); const bcrypt = require('bcryptjs'); const DB_PATH = process.env.DB_PATH || path.join(__dirname, 'data', 'consulting.db'); let db; function getDb() { if (!db) { const fs = require('fs'); const dir = path.dirname(DB_PATH); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); db = new Database(DB_PATH); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); initSchema(); migrateSchema(); seedDefaults(); } return db; } function migrateSchema() { const existingColumns = db.prepare("PRAGMA table_info('users')").all().map(r => r.name); const newCols = { email: "TEXT DEFAULT ''", role: "TEXT DEFAULT 'admin'", reset_token: "TEXT DEFAULT ''", reset_expires: "TEXT DEFAULT ''" }; for (const [col, def] of Object.entries(newCols)) { if (!existingColumns.includes(col)) { db.exec(`ALTER TABLE users ADD COLUMN ${col} ${def}`); } } db.exec(` CREATE TABLE IF NOT EXISTS ai_providers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, provider_type TEXT NOT NULL DEFAULT 'openrouter', api_key TEXT DEFAULT '', base_url TEXT DEFAULT '', default_model TEXT DEFAULT '', is_active INTEGER DEFAULT 1, is_default INTEGER DEFAULT 0, extra_headers TEXT DEFAULT '{}', created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS integrations ( id TEXT PRIMARY KEY, name TEXT DEFAULT '', api_key TEXT DEFAULT '', webhook_url TEXT DEFAULT '', config TEXT DEFAULT '{}', status TEXT DEFAULT 'inactive', created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, report_type TEXT DEFAULT 'client_summary', client_id INTEGER REFERENCES clients(id) ON DELETE SET NULL, project_id INTEGER, config TEXT DEFAULT '{}', created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, client_id INTEGER REFERENCES clients(id) ON DELETE SET NULL, start_date TEXT, end_date TEXT, status TEXT DEFAULT 'planning', priority TEXT DEFAULT 'medium', budget REAL, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); `); const diagramCols = db.prepare("PRAGMA table_info('data_diagrams')").all().map(r => r.name); if (!diagramCols.includes('engagement_id')) { db.exec('ALTER TABLE data_diagrams ADD COLUMN engagement_id INTEGER REFERENCES engagements(id) ON DELETE SET NULL'); } if (!diagramCols.includes('rack_id')) { db.exec('ALTER TABLE data_diagrams ADD COLUMN rack_id INTEGER REFERENCES racks(id) ON DELETE SET NULL'); } const tblRow = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='data_diagrams'").get(); const tblSql = tblRow ? tblRow.sql : ''; if (tblSql && tblSql.includes('CHECK') && !tblSql.includes('application_flow')) { const cols = db.prepare("PRAGMA table_info('data_diagrams')").all(); const colNames = cols.map(c => c.name); const selectCols = colNames.join(', '); db.exec(` CREATE TABLE data_diagrams_v2 ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT, diagram_type TEXT DEFAULT 'data_architecture', nodes TEXT NOT NULL DEFAULT '[]', edges TEXT NOT NULL DEFAULT '[]', engagement_id INTEGER REFERENCES engagements(id) ON DELETE SET NULL, rack_id INTEGER REFERENCES racks(id) ON DELETE SET NULL, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); INSERT INTO data_diagrams_v2 (${selectCols}) SELECT ${selectCols} FROM data_diagrams; DROP TABLE data_diagrams; ALTER TABLE data_diagrams_v2 RENAME TO data_diagrams; `); } } function initSchema() { db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, email TEXT DEFAULT '', role TEXT DEFAULT 'admin' CHECK(role IN ('admin','consultant','viewer')), reset_token TEXT DEFAULT '', reset_expires TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS clients ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, industry TEXT, website TEXT, contact_name TEXT, contact_email TEXT, contact_phone TEXT, status TEXT DEFAULT 'lead' CHECK(status IN ('lead','active','paused','archived')), source TEXT, notes TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS engagements ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE, type TEXT NOT NULL CHECK(type IN ('assessment','architecture','implementation','monitoring','consulting','training')), status TEXT DEFAULT 'planned' CHECK(status IN ('planned','in_progress','completed','on_hold')), title TEXT NOT NULL, description TEXT, start_date TEXT, end_date TEXT, outcome TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS assessments ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE, engagement_id INTEGER REFERENCES engagements(id) ON DELETE SET NULL, answers TEXT NOT NULL, report TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, engagement_id INTEGER NOT NULL REFERENCES engagements(id) ON DELETE CASCADE, title TEXT NOT NULL, description TEXT, status TEXT DEFAULT 'todo' CHECK(status IN ('todo','in_progress','done','cancelled')), priority TEXT DEFAULT 'medium' CHECK(priority IN ('low','medium','high','critical')), due_date TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS advice_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE, advice_type TEXT NOT NULL, advice_text TEXT NOT NULL, priority TEXT DEFAULT 'medium', dismissed INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS racks ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE, name TEXT NOT NULL, location TEXT, datacenter TEXT, total_units INTEGER DEFAULT 42, notes TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS rack_devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, rack_id INTEGER NOT NULL REFERENCES racks(id) ON DELETE CASCADE, name TEXT NOT NULL, device_type TEXT DEFAULT 'server', model TEXT, manufacturer TEXT, position_u INTEGER NOT NULL, height_u INTEGER DEFAULT 1, specs TEXT, mgmt_ip TEXT, notes TEXT, serial TEXT DEFAULT '', asset_tag TEXT DEFAULT '', status TEXT DEFAULT 'active', created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS network_devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE, name TEXT NOT NULL, device_type TEXT NOT NULL CHECK(device_type IN ('switch','router','firewall','loadbalancer','ap','other')), model TEXT, manufacturer TEXT, ip_address TEXT, mgmt_ip TEXT, ports_count INTEGER DEFAULT 24, os_version TEXT, notes TEXT, status TEXT DEFAULT 'active' CHECK(status IN ('active','standby','decommissioned')), created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS network_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE, name TEXT, from_type TEXT NOT NULL CHECK(from_type IN ('rack_device','network_device','external')), from_id INTEGER, from_port TEXT, to_type TEXT NOT NULL CHECK(to_type IN ('rack_device','network_device','external')), to_id INTEGER, to_port TEXT, media_type TEXT DEFAULT 'copper' CHECK(media_type IN ('copper','sfp','sfp+','qsfp','dac','fiber')), speed TEXT DEFAULT '1GbE', vlan TEXT, status TEXT DEFAULT 'active' CHECK(status IN ('active','inactive','planned')), notes TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS data_diagrams ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT, diagram_type TEXT DEFAULT 'data_architecture' CHECK(diagram_type IN ('data_architecture','network_topology','system_architecture')), nodes TEXT NOT NULL DEFAULT '[]', edges TEXT NOT NULL DEFAULT '[]', created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS client_notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE, content TEXT NOT NULL, created_by TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS app_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS client_files ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE, original_name TEXT NOT NULL, stored_name TEXT NOT NULL, mime_type TEXT DEFAULT '', file_size INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, username TEXT DEFAULT '', action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id INTEGER, details TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS time_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, client_id INTEGER REFERENCES clients(id) ON DELETE SET NULL, engagement_id INTEGER REFERENCES engagements(id) ON DELETE SET NULL, date TEXT NOT NULL, hours REAL NOT NULL, description TEXT DEFAULT '', billable INTEGER DEFAULT 1, hourly_rate REAL DEFAULT 150.0, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS invoices ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE, number TEXT UNIQUE NOT NULL, status TEXT DEFAULT 'draft' CHECK(status IN ('draft','sent','paid','overdue','cancelled')), date TEXT NOT NULL, due_date TEXT NOT NULL, subtotal REAL DEFAULT 0, tax REAL DEFAULT 0, total REAL DEFAULT 0, notes TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS invoice_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, invoice_id INTEGER NOT NULL REFERENCES invoices(id) ON DELETE CASCADE, description TEXT NOT NULL, quantity REAL DEFAULT 1, unit_price REAL DEFAULT 0, total REAL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, type TEXT NOT NULL, title TEXT NOT NULL, message TEXT DEFAULT '', channel TEXT DEFAULT 'in-app' CHECK(channel IN ('in-app','email','slack','webhook')), read INTEGER DEFAULT 0, sent INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS calendar_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, client_id INTEGER REFERENCES clients(id) ON DELETE SET NULL, engagement_id INTEGER REFERENCES engagements(id) ON DELETE SET NULL, title TEXT NOT NULL, description TEXT DEFAULT '', event_type TEXT DEFAULT 'meeting' CHECK(event_type IN ('meeting','call','deadline','workshop','review','other')), start_time TEXT NOT NULL, end_time TEXT NOT NULL, all_day INTEGER DEFAULT 0, color TEXT DEFAULT '#58a6ff', created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS user_settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL UNIQUE, notify_email INTEGER DEFAULT 1, notify_slack INTEGER DEFAULT 0, notify_webhook INTEGER DEFAULT 0, webhook_url TEXT DEFAULT '', slack_webhook TEXT DEFAULT '', language TEXT DEFAULT 'nl' ); CREATE TABLE IF NOT EXISTS email_accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, email TEXT NOT NULL, name TEXT DEFAULT '', imap_host TEXT NOT NULL, imap_port INTEGER DEFAULT 993, imap_secure INTEGER DEFAULT 1, imap_user TEXT NOT NULL, imap_pass TEXT NOT NULL, smtp_host TEXT NOT NULL, smtp_port INTEGER DEFAULT 587, smtp_secure INTEGER DEFAULT 0, smtp_user TEXT DEFAULT '', smtp_pass TEXT DEFAULT '', active INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS email_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER NOT NULL REFERENCES email_accounts(id) ON DELETE CASCADE, uid INTEGER NOT NULL, folder TEXT DEFAULT 'INBOX', subject TEXT DEFAULT '', from_name TEXT DEFAULT '', from_addr TEXT DEFAULT '', to_addr TEXT DEFAULT '', cc TEXT DEFAULT '', bcc TEXT DEFAULT '', date TEXT DEFAULT '', body_text TEXT DEFAULT '', body_html TEXT DEFAULT '', attachments INTEGER DEFAULT 0, seen INTEGER DEFAULT 0, flagged INTEGER DEFAULT 0, replied INTEGER DEFAULT 0, size INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS datacenters ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, address TEXT DEFAULT '', city TEXT DEFAULT '', region TEXT DEFAULT '', postal_code TEXT DEFAULT '', phone TEXT DEFAULT '', website TEXT DEFAULT '', type TEXT DEFAULT 'colo', operator TEXT DEFAULT '', tier TEXT DEFAULT 'III', power_mw REAL DEFAULT 0, size_sqm INTEGER DEFAULT 0, network_vendors TEXT DEFAULT '', server_vendors TEXT DEFAULT '', storage_vendors TEXT DEFAULT '', certifications TEXT DEFAULT '', notes TEXT DEFAULT '' ); CREATE TABLE IF NOT EXISTS tech_companies ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, industry TEXT DEFAULT '', city TEXT DEFAULT '', address TEXT DEFAULT '', postal_code TEXT DEFAULT '', phone TEXT DEFAULT '', email TEXT DEFAULT '', website TEXT DEFAULT '', linkedin TEXT DEFAULT '', employees INTEGER DEFAULT 0, revenue_range TEXT DEFAULT '', data_warehouse TEXT DEFAULT '', bi_tools TEXT DEFAULT '', cloud_provider TEXT DEFAULT '', data_lake TEXT DEFAULT '', ai_ml TEXT DEFAULT '', database_vendor TEXT DEFAULT '', lead_score INTEGER DEFAULT 0, lead_status TEXT DEFAULT 'new', notes TEXT DEFAULT '', source TEXT DEFAULT '' ); `); } function seedDefaults() { const existing = db.prepare('SELECT COUNT(*) as cnt FROM users').get(); if (existing.cnt === 0) { const pw = process.env.ADMIN_PASSWORD || 'admin'; const hash = bcrypt.hashSync(pw, 10); const result = db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)').run('admin', hash, 'admin'); db.prepare('INSERT INTO user_settings (user_id, language) VALUES (?, ?)').run(result.lastInsertRowid, 'nl'); console.log(`Default user created: admin / ${pw}`); } } const DEFAULTS = { brand_name: 'Mek-Tech Consulting', language: 'nl', theme: 'mek-neon-blue', default_page: '/', auto_refresh: '0', items_per_page: '50', debug_mode: '0', icon_colors: JSON.stringify({ 'layout-dashboard': '#58a6ff', users: '#3fb950', briefcase: '#d29922', server: '#bc8cff', 'check-circle': '#3fb950', lightbulb: '#d29922', clock: '#79c0ff', eye: '#f778ba', phone: '#3fb950', 'file-text': '#f778ba', 'edit-3': '#79c0ff', folder: '#d29922', 'clipboard-list': '#bc8cff' }), theme_colors: '{}' }; function getSettings() { const d = getDb(); const rows = d.prepare('SELECT key, value FROM app_settings').all(); const s = { ...DEFAULTS }; // Ensure defaults exist in DB const upsert = d.prepare(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, datetime('now')) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`); for (const [k, v] of Object.entries(DEFAULTS)) { const row = rows.find(r => r.key === k); if (row) { s[k] = row.value; } else { upsert.run(k, v); } } // Also store icon_colors as parsed object if (typeof s.icon_colors === 'string') { try { s.icon_colors = JSON.parse(s.icon_colors); } catch (e) { s.icon_colors = JSON.parse(DEFAULTS.icon_colors); } } if (typeof s.theme_colors === 'string') { try { s.theme_colors = JSON.parse(s.theme_colors || '{}'); } catch (e) { s.theme_colors = {}; } } if (!s.theme_colors || typeof s.theme_colors !== 'object') s.theme_colors = {}; return s; } function updateSetting(key, value) { const d = getDb(); if (typeof value === 'object') value = JSON.stringify(value); d.prepare(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, datetime('now')) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`).run(key, value); } function logAudit(userId, username, action, entityType, entityId, details) { const d = getDb(); d.prepare('INSERT INTO audit_log (user_id, username, action, entity_type, entity_id, details) VALUES (?, ?, ?, ?, ?, ?)') .run(userId || null, username || '', action, entityType, entityId || null, details || ''); } function getUserSettings(userId) { const d = getDb(); let s = d.prepare('SELECT * FROM user_settings WHERE user_id = ?').get(userId); if (!s) { d.prepare('INSERT INTO user_settings (user_id) VALUES (?)').run(userId); s = d.prepare('SELECT * FROM user_settings WHERE user_id = ?').get(userId); } return s; } module.exports = { getDb, getSettings, updateSetting, logAudit, getUserSettings, DEFAULTS };