import Database from 'better-sqlite3' import path from 'path' import { randomBytes } from 'crypto' import fs from 'fs' import { Employee, User, SkillDefinition } from '@skillmate/shared' import bcrypt from 'bcryptjs' import { v4 as uuidv4 } from 'uuid' import { FieldEncryption } from '../services/encryption' // Get or generate database encryption key const getDatabaseKey = (): string => { let key = process.env.DATABASE_ENCRYPTION_KEY if (!key) { console.warn('⚠️ No DATABASE_ENCRYPTION_KEY found in environment variables.') console.warn('⚠️ Generating a temporary key for development.') console.warn('⚠️ For production, set DATABASE_ENCRYPTION_KEY in your .env file!') // Generate and save a development key const keyPath = path.join(process.cwd(), '.database.key') if (fs.existsSync(keyPath)) { key = fs.readFileSync(keyPath, 'utf8') } else { key = randomBytes(32).toString('hex') fs.writeFileSync(keyPath, key, { mode: 0o600 }) // Restrictive permissions console.log('💾 Development key saved to .database.key (add to .gitignore!)') } } return key } // Database path configuration const getDbPath = (): string => { const dbPath = process.env.DATABASE_PATH || ( process.env.NODE_ENV === 'production' ? path.join(process.cwd(), 'data', 'skillmate.encrypted.db') : path.join(process.cwd(), 'skillmate.dev.encrypted.db') ) // Ensure data directory exists for production const dbDir = path.dirname(dbPath) if (!fs.existsSync(dbDir)) { fs.mkdirSync(dbDir, { recursive: true }) } return dbPath } const dbPath = getDbPath() const dbKey = getDatabaseKey() // Create database connection with encryption support export const db = new Database(dbPath) // Enable better performance and data integrity db.pragma('journal_mode = WAL') db.pragma('foreign_keys = ON') db.pragma('busy_timeout = 5000') // Add encryption helper functions to database export const encryptedDb = { ...db, // Prepare statement with automatic encryption/decryption prepareEncrypted(sql: string) { return db.prepare(sql) }, // Insert employee with encrypted fields insertEmployee(employee: any) { const encrypted = { ...employee, email: FieldEncryption.encrypt(employee.email), phone: FieldEncryption.encrypt(employee.phone), mobile: FieldEncryption.encrypt(employee.mobile), clearance_level: FieldEncryption.encrypt(employee.clearance_level), clearance_valid_until: FieldEncryption.encrypt(employee.clearance_valid_until), // Add search hashes for encrypted fields email_hash: employee.email ? FieldEncryption.hash(employee.email) : null, phone_hash: employee.phone ? FieldEncryption.hash(employee.phone) : null } return db.prepare(` INSERT INTO employees ( id, first_name, last_name, employee_number, photo, position, department, email, email_hash, phone, phone_hash, mobile, office, availability, clearance_level, clearance_valid_until, clearance_issued_date, created_at, updated_at, created_by ) VALUES ( @id, @first_name, @last_name, @employee_number, @photo, @position, @department, @email, @email_hash, @phone, @phone_hash, @mobile, @office, @availability, @clearance_level, @clearance_valid_until, @clearance_issued_date, @created_at, @updated_at, @created_by ) `).run(encrypted) }, // Get employee with decrypted fields getEmployee(id: string) { const employee = db.prepare(` SELECT * FROM employees WHERE id = ? `).get(id) as any if (!employee) return null // Decrypt sensitive fields return { ...employee, email: FieldEncryption.decrypt(employee.email), phone: FieldEncryption.decrypt(employee.phone), mobile: FieldEncryption.decrypt(employee.mobile), clearance_level: FieldEncryption.decrypt(employee.clearance_level), clearance_valid_until: FieldEncryption.decrypt(employee.clearance_valid_until) } }, // Get all employees with decrypted fields (handle decryption failures) getAllEmployees() { const employees = db.prepare(` SELECT * FROM employees ORDER BY last_name, first_name `).all() as any[] return employees.map(emp => { const safeDecrypt = (field: any) => { if (!field) return field try { return FieldEncryption.decrypt(field) || field } catch (error) { // For compatibility with old unencrypted data or different encryption keys return field } } return { ...emp, email: safeDecrypt(emp.email), phone: safeDecrypt(emp.phone), mobile: safeDecrypt(emp.mobile), clearance_level: safeDecrypt(emp.clearance_level), clearance_valid_until: safeDecrypt(emp.clearance_valid_until) } }) }, // Search by encrypted field using hash findByEmail(email: string) { const emailHash = FieldEncryption.hash(email) return db.prepare(` SELECT * FROM employees WHERE email_hash = ? `).get(emailHash) } } export function initializeSecureDatabase() { // Create updated employees table with hash fields for searching db.exec(` CREATE TABLE IF NOT EXISTS employees ( id TEXT PRIMARY KEY, first_name TEXT NOT NULL, last_name TEXT NOT NULL, employee_number TEXT UNIQUE NOT NULL, photo TEXT, position TEXT NOT NULL, department TEXT NOT NULL, email TEXT NOT NULL, email_hash TEXT, phone TEXT NOT NULL, phone_hash TEXT, mobile TEXT, office TEXT, availability TEXT NOT NULL, clearance_level TEXT, clearance_valid_until TEXT, clearance_issued_date TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, created_by TEXT NOT NULL, updated_by TEXT ) `) // Add indexes for hash fields db.exec(` CREATE INDEX IF NOT EXISTS idx_employees_email_hash ON employees(email_hash); CREATE INDEX IF NOT EXISTS idx_employees_phone_hash ON employees(phone_hash); `) // Users table with encrypted email db.exec(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, email TEXT NOT NULL, email_hash TEXT, password TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('admin', 'superuser', 'user')), employee_id TEXT, last_login TEXT, is_active INTEGER DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(email_hash) ) `) // Create index for email hash db.exec(` CREATE INDEX IF NOT EXISTS idx_users_email_hash ON users(email_hash); `) // Skills table db.exec(` CREATE TABLE IF NOT EXISTS skills ( id TEXT PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL, description TEXT, requires_certification INTEGER DEFAULT 0, expires_after INTEGER ) `) // Employee skills junction table db.exec(` CREATE TABLE IF NOT EXISTS employee_skills ( employee_id TEXT NOT NULL, skill_id TEXT NOT NULL, level TEXT, verified INTEGER DEFAULT 0, verified_by TEXT, verified_date TEXT, PRIMARY KEY (employee_id, skill_id), FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE, FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE ) `) // Language skills table db.exec(` CREATE TABLE IF NOT EXISTS language_skills ( id TEXT PRIMARY KEY, employee_id TEXT NOT NULL, language TEXT NOT NULL, proficiency TEXT NOT NULL, certified INTEGER DEFAULT 0, certificate_type TEXT, is_native INTEGER DEFAULT 0, can_interpret INTEGER DEFAULT 0, FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE ) `) // Specializations table db.exec(` CREATE TABLE IF NOT EXISTS specializations ( id TEXT PRIMARY KEY, employee_id TEXT NOT NULL, name TEXT NOT NULL, FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE ) `) // Audit Log for security tracking db.exec(` CREATE TABLE IF NOT EXISTS security_audit_log ( id TEXT PRIMARY KEY, entity_type TEXT NOT NULL, entity_id TEXT NOT NULL, action TEXT NOT NULL CHECK(action IN ('create', 'read', 'update', 'delete', 'login', 'logout', 'failed_login')), user_id TEXT, changes TEXT, timestamp TEXT NOT NULL, ip_address TEXT, user_agent TEXT, risk_level TEXT CHECK(risk_level IN ('low', 'medium', 'high', 'critical')) ); CREATE INDEX IF NOT EXISTS idx_security_audit_entity ON security_audit_log(entity_type, entity_id); CREATE INDEX IF NOT EXISTS idx_security_audit_user ON security_audit_log(user_id); CREATE INDEX IF NOT EXISTS idx_security_audit_timestamp ON security_audit_log(timestamp); CREATE INDEX IF NOT EXISTS idx_security_audit_risk ON security_audit_log(risk_level); `) // System settings table db.exec(` CREATE TABLE IF NOT EXISTS system_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, description TEXT, updated_at TEXT NOT NULL, updated_by TEXT ) `) // Insert default system settings const settingsExist = db.prepare('SELECT key FROM system_settings WHERE key = ?').get('email_notifications_enabled') if (!settingsExist) { const now = new Date().toISOString() db.prepare(` INSERT INTO system_settings (key, value, description, updated_at, updated_by) VALUES (?, ?, ?, ?, ?) `).run('email_notifications_enabled', 'false', 'Enable/disable email notifications for new user passwords', now, 'system') } // Create indexes for better performance db.exec(` CREATE INDEX IF NOT EXISTS idx_employees_availability ON employees(availability); CREATE INDEX IF NOT EXISTS idx_employees_department ON employees(department); CREATE INDEX IF NOT EXISTS idx_employee_skills_employee ON employee_skills(employee_id); CREATE INDEX IF NOT EXISTS idx_employee_skills_skill ON employee_skills(skill_id); CREATE INDEX IF NOT EXISTS idx_language_skills_employee ON language_skills(employee_id); CREATE INDEX IF NOT EXISTS idx_specializations_employee ON specializations(employee_id); `) // Controlled vocabulary (used to store skill categories/subcategories names) db.exec(` CREATE TABLE IF NOT EXISTS controlled_vocabulary ( id TEXT PRIMARY KEY, category TEXT NOT NULL, value TEXT NOT NULL, description TEXT, is_active INTEGER DEFAULT 1, created_at TEXT NOT NULL, UNIQUE(category, value) ); CREATE INDEX IF NOT EXISTS idx_vocab_category ON controlled_vocabulary(category); CREATE INDEX IF NOT EXISTS idx_vocab_value ON controlled_vocabulary(value); `) // Create default admin user if not exists const adminExists = db.prepare('SELECT id FROM users WHERE username = ?').get('admin') if (!adminExists) { const hashedPassword = bcrypt.hashSync('admin123', 12) const now = new Date().toISOString() const adminEmail = 'admin@skillmate.local' db.prepare(` INSERT INTO users (id, username, email, email_hash, password, role, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( uuidv4(), 'admin', FieldEncryption.encrypt(adminEmail), FieldEncryption.hash(adminEmail), hashedPassword, 'admin', 1, now, now ) console.log('🔐 Default admin user created with password: admin123') console.log('⚠️ Please change this password immediately!') } } // db is already exported above via export const db = new Database(dbPath)