Dieser Commit ist enthalten in:
Claude Project Manager
2025-09-20 21:31:04 +02:00
Commit 6b9b6d4f20
1821 geänderte Dateien mit 348527 neuen und 0 gelöschten Zeilen

444
backend/src/config/database.ts Normale Datei
Datei anzeigen

@ -0,0 +1,444 @@
import Database from 'better-sqlite3'
import path from 'path'
import { Employee, User, SkillDefinition } from '@skillmate/shared'
import bcrypt from 'bcryptjs'
import { v4 as uuidv4 } from 'uuid'
import { db as secureDb, encryptedDb, initializeSecureDatabase } from './secureDatabase'
export { initializeSecureDatabase } from './secureDatabase'
// Export the secure database instance
export const db = secureDb
export function initializeDatabase() {
// Users table
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
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
)
`)
// Profiles table (erweitert für Yellow Pages)
db.exec(`
CREATE TABLE IF NOT EXISTS profiles (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
department TEXT,
location TEXT,
role TEXT,
email TEXT,
phone TEXT,
teams_link TEXT,
job_category TEXT CHECK(job_category IN ('Technik', 'IT & Digitalisierung', 'Verwaltung', 'F&E', 'Kommunikation & HR', 'Produktion', 'Sonstiges')),
job_title TEXT,
job_desc TEXT,
consent_public_profile INTEGER DEFAULT 0,
consent_searchable INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
updated_by TEXT NOT NULL,
review_due_at TEXT,
search_vector TEXT
)
`)
// Volltext-Index für Suche
db.exec(`
CREATE INDEX IF NOT EXISTS idx_profiles_search
ON profiles(search_vector);
CREATE INDEX IF NOT EXISTS idx_profiles_department
ON profiles(department);
CREATE INDEX IF NOT EXISTS idx_profiles_location
ON profiles(location);
CREATE INDEX IF NOT EXISTS idx_profiles_job_category
ON profiles(job_category);
CREATE INDEX IF NOT EXISTS idx_profiles_review_due
ON profiles(review_due_at);
`)
// Employees table (für Kompatibilität beibehalten)
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,
phone TEXT NOT NULL,
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
)
`)
// 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
)
`)
// Profile Kompetenzen (Arrays als separate Tabellen)
db.exec(`
CREATE TABLE IF NOT EXISTS profile_domains (
profile_id TEXT NOT NULL,
domain TEXT NOT NULL,
PRIMARY KEY (profile_id, domain),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_tools (
profile_id TEXT NOT NULL,
tool TEXT NOT NULL,
PRIMARY KEY (profile_id, tool),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_methods (
profile_id TEXT NOT NULL,
method TEXT NOT NULL,
PRIMARY KEY (profile_id, method),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_industry_knowledge (
profile_id TEXT NOT NULL,
knowledge TEXT NOT NULL,
PRIMARY KEY (profile_id, knowledge),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_regulatory (
profile_id TEXT NOT NULL,
regulation TEXT NOT NULL,
PRIMARY KEY (profile_id, regulation),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_networks (
profile_id TEXT NOT NULL,
network TEXT NOT NULL,
PRIMARY KEY (profile_id, network),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_digital_skills (
profile_id TEXT NOT NULL,
skill TEXT NOT NULL,
PRIMARY KEY (profile_id, skill),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS profile_social_skills (
profile_id TEXT NOT NULL,
skill TEXT NOT NULL,
PRIMARY KEY (profile_id, skill),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
`)
// Profile Sprachen
db.exec(`
CREATE TABLE IF NOT EXISTS profile_languages (
profile_id TEXT NOT NULL,
code TEXT NOT NULL,
level TEXT NOT NULL CHECK(level IN ('basic', 'fluent', 'native', 'business')),
PRIMARY KEY (profile_id, code),
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
`)
// Profile Projekte
db.exec(`
CREATE TABLE IF NOT EXISTS profile_projects (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL,
title TEXT NOT NULL,
role TEXT,
summary TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS project_links (
project_id TEXT NOT NULL,
link TEXT NOT NULL,
PRIMARY KEY (project_id, link),
FOREIGN KEY (project_id) REFERENCES profile_projects(id) ON DELETE CASCADE
);
`)
// Language skills table (für Kompatibilität)
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
)
`)
// Sync log table
db.exec(`
CREATE TABLE IF NOT EXISTS sync_log (
id TEXT PRIMARY KEY,
sync_time TEXT NOT NULL,
success INTEGER NOT NULL,
items_synced INTEGER,
error_message TEXT,
duration INTEGER
)
`)
// 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', 10)
const now = new Date().toISOString()
db.prepare(`
INSERT INTO users (id, username, email, password, role, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
uuidv4(),
'admin',
'admin@skillmate.local',
hashedPassword,
'admin',
1,
now,
now
)
}
// Workspace Management Tables
// Workspaces (Desks, Meeting Rooms, etc.)
db.exec(`
CREATE TABLE IF NOT EXISTS workspaces (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('desk', 'meeting_room', 'phone_booth', 'parking', 'locker')),
floor TEXT NOT NULL,
building TEXT,
capacity INTEGER DEFAULT 1,
equipment TEXT, -- JSON array of equipment
position_x INTEGER, -- For floor plan visualization
position_y INTEGER,
is_active INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`)
// Bookings table
db.exec(`
CREATE TABLE IF NOT EXISTS bookings (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL,
user_id TEXT NOT NULL,
employee_id TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('confirmed', 'cancelled', 'completed', 'no_show')),
check_in_time TEXT,
check_out_time TEXT,
notes TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (workspace_id) REFERENCES workspaces(id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (employee_id) REFERENCES employees(id)
)
`)
// Recurring bookings
db.exec(`
CREATE TABLE IF NOT EXISTS recurring_bookings (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL,
user_id TEXT NOT NULL,
employee_id TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
time_start TEXT NOT NULL,
time_end TEXT NOT NULL,
days_of_week TEXT NOT NULL, -- JSON array of days [1,2,3,4,5] for Mon-Fri
created_at TEXT NOT NULL,
FOREIGN KEY (workspace_id) REFERENCES workspaces(id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (employee_id) REFERENCES employees(id)
)
`)
// Booking rules and restrictions
db.exec(`
CREATE TABLE IF NOT EXISTS booking_rules (
id TEXT PRIMARY KEY,
workspace_type TEXT,
max_duration_hours INTEGER,
max_advance_days INTEGER,
min_advance_hours INTEGER,
max_bookings_per_user_per_day INTEGER,
max_bookings_per_user_per_week INTEGER,
auto_release_minutes INTEGER, -- Auto-release if not checked in
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`)
// Analytics data
db.exec(`
CREATE TABLE IF NOT EXISTS workspace_analytics (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL,
date TEXT NOT NULL,
total_bookings INTEGER DEFAULT 0,
total_hours_booked REAL DEFAULT 0,
utilization_rate REAL DEFAULT 0,
no_show_count INTEGER DEFAULT 0,
unique_users INTEGER DEFAULT 0,
peak_hour INTEGER,
FOREIGN KEY (workspace_id) REFERENCES workspaces(id),
UNIQUE(workspace_id, date)
)
`)
// Floor plans
db.exec(`
CREATE TABLE IF NOT EXISTS floor_plans (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
floor TEXT NOT NULL,
building TEXT,
image_url TEXT,
svg_data TEXT, -- SVG data for interactive floor plan
width INTEGER,
height INTEGER,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`)
// Audit Log für Änderungsverfolgung
db.exec(`
CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY,
entity_type TEXT NOT NULL,
entity_id TEXT NOT NULL,
action TEXT NOT NULL CHECK(action IN ('create', 'update', 'delete')),
user_id TEXT NOT NULL,
changes TEXT, -- JSON mit Änderungen
timestamp TEXT NOT NULL,
ip_address TEXT,
user_agent TEXT
);
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log(entity_type, entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp);
`)
// Reminder-System
db.exec(`
CREATE TABLE IF NOT EXISTS reminders (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('annual_update', 'overdue', 'custom')),
message TEXT,
sent_at TEXT,
acknowledged_at TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_reminders_profile ON reminders(profile_id);
CREATE INDEX IF NOT EXISTS idx_reminders_sent ON reminders(sent_at);
`)
// Kontrollierte Vokabulare/Tags
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 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);
CREATE INDEX IF NOT EXISTS idx_bookings_workspace ON bookings(workspace_id);
CREATE INDEX IF NOT EXISTS idx_bookings_user ON bookings(user_id);
CREATE INDEX IF NOT EXISTS idx_bookings_start_time ON bookings(start_time);
CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status);
CREATE INDEX IF NOT EXISTS idx_workspace_analytics_date ON workspace_analytics(date);
`)
}

Datei anzeigen

@ -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)