Initial commit
Dieser Commit ist enthalten in:
360
backend/src/config/secureDatabase.ts
Normale Datei
360
backend/src/config/secureDatabase.ts
Normale Datei
@ -0,0 +1,360 @@
|
||||
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)
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren