Files

556 lines
19 KiB
JavaScript

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: '{}',
showcase_url: 'http://192.168.1.247:3010'
};
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 };