Files
SkillMate/backend/src/config/secureDatabase.ts
Claude Project Manager 6b9b6d4f20 Initial commit
2025-09-20 21:31:04 +02:00

361 Zeilen
12 KiB
TypeScript

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)