Zwiscshenstand - laufende Version
Dieser Commit ist enthalten in:
35
backend/package-lock.json
generiert
35
backend/package-lock.json
generiert
@ -26,6 +26,7 @@
|
||||
"multer": "^2.0.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemailer": "^7.0.6",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.11.0"
|
||||
@ -3862,6 +3863,12 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-ensure": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
|
||||
"integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-gyp": {
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
|
||||
@ -4096,6 +4103,34 @@
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pdf-parse": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz",
|
||||
"integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^3.1.0",
|
||||
"node-ensure": "^0.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-parse/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
|
||||
@ -9,7 +9,8 @@
|
||||
"start": "node dist/index.js",
|
||||
"reset-admin": "node scripts/reset-admin.js",
|
||||
"migrate-users": "node scripts/migrate-users.js",
|
||||
"seed-skills": "node scripts/seed-skills-from-frontend.js",
|
||||
"seed-skills": "node scripts/seed-skills-from-shared.js",
|
||||
"seed-skills-shared": "node scripts/seed-skills-from-shared.js",
|
||||
"purge-users": "node scripts/purge-users.js"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
134
backend/scripts/seed-skills-from-shared.js
Normale Datei
134
backend/scripts/seed-skills-from-shared.js
Normale Datei
@ -0,0 +1,134 @@
|
||||
// Seed controlled vocabulary (categories/subcategories) and skills
|
||||
// from shared/skills.js (SKILL_HIERARCHY) into the local SQLite DB.
|
||||
//
|
||||
// Usage:
|
||||
// cd backend
|
||||
// node scripts/seed-skills-from-shared.js
|
||||
//
|
||||
// Optionally set DATABASE_PATH to seed a custom DB file.
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const crypto = require('crypto')
|
||||
const Database = require('better-sqlite3')
|
||||
|
||||
function getDbPath() {
|
||||
if (process.env.DATABASE_PATH && process.env.DATABASE_PATH.length > 0) {
|
||||
return process.env.DATABASE_PATH
|
||||
}
|
||||
// default dev DB in backend folder
|
||||
return path.join(__dirname, '..', 'skillmate.dev.encrypted.db')
|
||||
}
|
||||
|
||||
function ensureTables(db) {
|
||||
// controlled_vocabulary (canonical definition from secureDatabase)
|
||||
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);
|
||||
`)
|
||||
|
||||
// skills (canonical definition from secureDatabase)
|
||||
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
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
function loadHierarchy() {
|
||||
// Load from shared/skills.js
|
||||
const skillsPath = path.join(__dirname, '..', '..', 'shared', 'skills.js')
|
||||
if (!fs.existsSync(skillsPath)) {
|
||||
throw new Error('shared/skills.js not found')
|
||||
}
|
||||
const mod = require(skillsPath)
|
||||
if (!mod || !Array.isArray(mod.SKILL_HIERARCHY)) {
|
||||
throw new Error('SKILL_HIERARCHY missing or invalid in shared/skills.js')
|
||||
}
|
||||
return mod.SKILL_HIERARCHY
|
||||
}
|
||||
|
||||
function seed(db, hierarchy) {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const insertVocab = db.prepare(`
|
||||
INSERT OR IGNORE INTO controlled_vocabulary (id, category, value, description, is_active, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
const insertSkill = db.prepare(`
|
||||
INSERT OR IGNORE INTO skills (id, name, category, description, requires_certification, expires_after)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
let catCount = 0
|
||||
let subCount = 0
|
||||
let skillCount = 0
|
||||
|
||||
for (const cat of hierarchy) {
|
||||
const catId = String(cat.id)
|
||||
const catName = String(cat.name || cat.id)
|
||||
|
||||
// category
|
||||
const catPk = crypto.randomUUID()
|
||||
const catRes = insertVocab.run(catPk, 'skill_category', catId, catName, 1, now)
|
||||
if (catRes.changes > 0) catCount++
|
||||
|
||||
for (const sub of (cat.subcategories || [])) {
|
||||
const subId = String(sub.id)
|
||||
const subName = String(sub.name || sub.id)
|
||||
const key = `${catId}.${subId}`
|
||||
|
||||
// subcategory
|
||||
const subPk = crypto.randomUUID()
|
||||
const subRes = insertVocab.run(subPk, 'skill_subcategory', key, subName, 1, now)
|
||||
if (subRes.changes > 0) subCount++
|
||||
|
||||
for (const sk of (sub.skills || [])) {
|
||||
const sId = `${key}.${sk.id}`
|
||||
const sName = String(sk.name || sk.id)
|
||||
const requires = (catId === 'certifications' || subId === 'weapons') ? 1 : 0
|
||||
const expires = (catId === 'certifications') ? 36 : null
|
||||
const sRes = insertSkill.run(sId, sName, key, null, requires, expires)
|
||||
if (sRes.changes > 0) skillCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { catCount, subCount, skillCount }
|
||||
}
|
||||
|
||||
function main() {
|
||||
const dbPath = getDbPath()
|
||||
console.log('➡️ Seeding skills into DB:', dbPath)
|
||||
const db = new Database(dbPath)
|
||||
try {
|
||||
ensureTables(db)
|
||||
const hierarchy = loadHierarchy()
|
||||
const { catCount, subCount, skillCount } = seed(db, hierarchy)
|
||||
console.log(`✅ Seeding completed. Inserted: ${catCount} categories, ${subCount} subcategories, ${skillCount} skills.`)
|
||||
console.log('ℹ️ Re-open the Admin Panel to see Skills in hierarchy view.')
|
||||
} catch (err) {
|
||||
console.error('❌ Seeding failed:', err.message)
|
||||
process.exitCode = 1
|
||||
} finally {
|
||||
try { db.close() } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@ -9,22 +9,30 @@ export { initializeSecureDatabase } from './secureDatabase'
|
||||
// Export the secure database instance
|
||||
export const db = secureDb
|
||||
|
||||
// IMPORTANT: secureDatabase.ts owns all security‑sensitive base tables
|
||||
// (users, employees, skills and their junctions, language_skills, specializations,
|
||||
// controlled_vocabulary, system_settings, security_audit_log) including
|
||||
// encrypted fields and indexes. This module defines only extended, non‑sensitive
|
||||
// schemas (profiles, workspaces/bookings, organizational structure, reminders, analytics).
|
||||
|
||||
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
|
||||
)
|
||||
`)
|
||||
// Lightweight schema guard (no mutations): warn if legacy/plain schemas are present
|
||||
try {
|
||||
const usersCols: any[] = db.prepare(`PRAGMA table_info(users)`).all() as any
|
||||
const hasEmailHash = usersCols.some(c => c.name === 'email_hash')
|
||||
if (!hasEmailHash) {
|
||||
console.warn('[DB-Guard] users.email_hash missing. Ensure initializeSecureDatabase() runs first and legacy data is migrated.')
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const empCols: any[] = db.prepare(`PRAGMA table_info(employees)`).all() as any
|
||||
const hasEmpEmailHash = empCols.some(c => c.name === 'email_hash')
|
||||
const hasEmpPhoneHash = empCols.some(c => c.name === 'phone_hash')
|
||||
if (!hasEmpEmailHash || !hasEmpPhoneHash) {
|
||||
console.warn('[DB-Guard] employees hash columns missing. Ensure secure schema initialized and data migrated.')
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Profiles table (erweitert für Yellow Pages)
|
||||
db.exec(`
|
||||
@ -68,57 +76,7 @@ export function initializeDatabase() {
|
||||
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
|
||||
)
|
||||
`)
|
||||
// Employees/Skills tables are defined in secureDatabase (encrypted & indexed)
|
||||
|
||||
// Profile Kompetenzen (Arrays als separate Tabellen)
|
||||
db.exec(`
|
||||
@ -210,62 +168,9 @@ export function initializeDatabase() {
|
||||
);
|
||||
`)
|
||||
|
||||
// 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
|
||||
)
|
||||
`)
|
||||
// Language skills / Specializations are defined in secureDatabase
|
||||
|
||||
// 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
|
||||
)
|
||||
}
|
||||
// Sync tables are owned by SyncService initializer (avoid duplicate legacy schema)
|
||||
|
||||
// Workspace Management Tables
|
||||
|
||||
@ -432,9 +337,12 @@ export function initializeDatabase() {
|
||||
FOREIGN KEY (unit_id) REFERENCES organizational_units(id) ON DELETE CASCADE,
|
||||
UNIQUE(employee_id, unit_id, role)
|
||||
);
|
||||
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_emp_units_employee ON employee_unit_assignments(employee_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_emp_units_unit ON employee_unit_assignments(unit_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_emp_units_primary_unique
|
||||
ON employee_unit_assignments(employee_id)
|
||||
WHERE is_primary = 1;
|
||||
`)
|
||||
|
||||
// Special Positions (Personalrat, Beauftragte, etc.)
|
||||
@ -538,29 +446,8 @@ export function initializeDatabase() {
|
||||
CREATE INDEX IF NOT EXISTS idx_reminders_sent ON reminders(sent_at);
|
||||
`)
|
||||
|
||||
// Kontrollierte Vokabulare/Tags
|
||||
// Create indexes for better performance (only for tables defined here)
|
||||
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);
|
||||
|
||||
@ -80,17 +80,19 @@ export const encryptedDb = {
|
||||
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,
|
||||
primary_unit_id,
|
||||
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,
|
||||
@primary_unit_id,
|
||||
@created_at, @updated_at, @created_by
|
||||
)
|
||||
`).run(encrypted)
|
||||
@ -173,12 +175,24 @@ export function initializeSecureDatabase() {
|
||||
clearance_level TEXT,
|
||||
clearance_valid_until TEXT,
|
||||
clearance_issued_date TEXT,
|
||||
primary_unit_id TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL,
|
||||
updated_by TEXT
|
||||
)
|
||||
`)
|
||||
|
||||
// Add primary_unit_id column if missing (legacy DBs)
|
||||
try {
|
||||
const cols: any[] = db.prepare(`PRAGMA table_info(employees)`).all() as any
|
||||
const hasPrimaryUnitId = cols.some(c => c.name === 'primary_unit_id')
|
||||
if (!hasPrimaryUnitId) {
|
||||
db.exec(`ALTER TABLE employees ADD COLUMN primary_unit_id TEXT`)
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Add indexes for hash fields
|
||||
db.exec(`
|
||||
@ -309,6 +323,7 @@ export function initializeSecureDatabase() {
|
||||
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_employees_primary_unit ON employees(primary_unit_id);
|
||||
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);
|
||||
|
||||
@ -24,6 +24,7 @@ import employeeOrganizationRoutes from './routes/employeeOrganization'
|
||||
import { errorHandler } from './middleware/errorHandler'
|
||||
import { logger } from './utils/logger'
|
||||
import { syncScheduler } from './services/syncScheduler'
|
||||
import { ensureSkillsSeeded } from './services/skillSeeder'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
@ -33,6 +34,8 @@ const PORT = process.env.PORT || 3004
|
||||
// Initialize secure database (core tables) and extended schema (organization, deputies, etc.)
|
||||
initializeSecureDatabase()
|
||||
initializeDatabase()
|
||||
// Ensure skills/categories exist for Admin Skill Management (idempotent)
|
||||
ensureSkillsSeeded()
|
||||
|
||||
// Initialize sync scheduler
|
||||
syncScheduler
|
||||
|
||||
@ -66,7 +66,32 @@ function logSecurityAudit(
|
||||
// Get all employees
|
||||
router.get('/', authenticate, requirePermission('employees:read'), async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const employees = encryptedDb.getAllEmployees()
|
||||
const { unitId, includeDescendants } = (req.query || {}) as any
|
||||
|
||||
// Optional filter by unit (including descendants)
|
||||
let allowedIds: Set<string> | null = null
|
||||
if (unitId) {
|
||||
const collectDescendants = (rootId: string): string[] => {
|
||||
const ids: string[] = [rootId]
|
||||
const stack: string[] = [rootId]
|
||||
while (stack.length) {
|
||||
const curr = stack.pop()!
|
||||
const children = db.prepare('SELECT id FROM organizational_units WHERE parent_id = ?').all(curr) as any[]
|
||||
for (const ch of children) {
|
||||
ids.push(ch.id)
|
||||
stack.push(ch.id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
const unitIds = includeDescendants === '1' || includeDescendants === 'true'
|
||||
? collectDescendants(String(unitId))
|
||||
: [String(unitId)]
|
||||
const rows = db.prepare(`SELECT id FROM employees WHERE primary_unit_id IN (${unitIds.map(() => '?').join(',')})`).all(...unitIds) as any[]
|
||||
allowedIds = new Set(rows.map(r => r.id))
|
||||
}
|
||||
|
||||
const employees = encryptedDb.getAllEmployees().filter((e: any) => !allowedIds || allowedIds.has(e.id))
|
||||
|
||||
const employeesWithDetails = employees.map((emp: any) => {
|
||||
// Get skills
|
||||
@ -331,7 +356,8 @@ router.post('/',
|
||||
const {
|
||||
firstName, lastName, employeeNumber, photo, position = 'Mitarbeiter',
|
||||
department, email, phone = 'Nicht angegeben', mobile, office, availability = 'available',
|
||||
clearance, skills = [], languages = [], specializations = [], userRole, createUser
|
||||
clearance, skills = [], languages = [], specializations = [], userRole, createUser,
|
||||
primaryUnitId, assignmentRole
|
||||
} = req.body
|
||||
|
||||
// Generate employee number if not provided
|
||||
@ -363,11 +389,28 @@ router.post('/',
|
||||
clearance_level: clearance?.level || null,
|
||||
clearance_valid_until: clearance?.validUntil || null,
|
||||
clearance_issued_date: clearance?.issuedDate || null,
|
||||
primary_unit_id: primaryUnitId || null,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by: req.user!.id
|
||||
})
|
||||
|
||||
// Create primary assignment if provided
|
||||
if (primaryUnitId) {
|
||||
const unit = db.prepare('SELECT id, type FROM organizational_units WHERE id = ?').get(primaryUnitId)
|
||||
if (!unit) {
|
||||
return res.status(400).json({ success: false, error: { message: 'Invalid primary unit' } })
|
||||
}
|
||||
db.prepare(`
|
||||
INSERT INTO employee_unit_assignments (
|
||||
id, employee_id, unit_id, role, start_date, end_date, is_primary, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
uuidv4(), employeeId, primaryUnitId, assignmentRole || 'mitarbeiter',
|
||||
now, null, 1, now, now
|
||||
)
|
||||
}
|
||||
|
||||
// Insert skills (only if they exist in skills table)
|
||||
if (skills && skills.length > 0) {
|
||||
const insertSkill = db.prepare(`
|
||||
@ -477,6 +520,7 @@ router.post('/',
|
||||
office: office || null,
|
||||
availability,
|
||||
clearance,
|
||||
primaryUnitId: primaryUnitId || null,
|
||||
skills,
|
||||
languages,
|
||||
specializations,
|
||||
|
||||
112
backend/src/services/skillSeeder.ts
Normale Datei
112
backend/src/services/skillSeeder.ts
Normale Datei
@ -0,0 +1,112 @@
|
||||
import path from 'path'
|
||||
import { db } from '../config/secureDatabase'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
type SkillHierarchy = Array<{
|
||||
id: string
|
||||
name: string
|
||||
subcategories?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
skills?: Array<{ id: string; name: string }>
|
||||
}>
|
||||
}>
|
||||
|
||||
function loadSharedHierarchy(): SkillHierarchy | null {
|
||||
try {
|
||||
const sharedPath = path.join(__dirname, '../../../shared/skills.js')
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const mod = require(sharedPath)
|
||||
if (mod && Array.isArray(mod.SKILL_HIERARCHY)) {
|
||||
return mod.SKILL_HIERARCHY as SkillHierarchy
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('SkillSeeder: Could not load shared/skills.js. Skipping seeding.')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function ensureSkillsSeeded() {
|
||||
try {
|
||||
// Always ensure vocabulary tables exist (idempotent)
|
||||
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);
|
||||
`)
|
||||
|
||||
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
|
||||
)
|
||||
`)
|
||||
|
||||
const hierarchy = loadSharedHierarchy()
|
||||
if (!hierarchy) return
|
||||
|
||||
const now = new Date().toISOString()
|
||||
const insertVocab = db.prepare(`
|
||||
INSERT OR IGNORE INTO controlled_vocabulary (id, category, value, description, is_active, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
const insertSkill = db.prepare(`
|
||||
INSERT OR IGNORE INTO skills (id, name, category, description, requires_certification, expires_after)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
let cats = 0, subs = 0, skills = 0
|
||||
|
||||
for (const cat of hierarchy) {
|
||||
const catId = String(cat.id)
|
||||
const catName = String(cat.name || cat.id)
|
||||
|
||||
// category
|
||||
insertVocab.run(cryptoRandomUUID(), 'skill_category', catId, catName, 1, now)
|
||||
cats++
|
||||
|
||||
for (const sub of (cat.subcategories || [])) {
|
||||
const subId = String(sub.id)
|
||||
const subName = String(sub.name || sub.id)
|
||||
const key = `${catId}.${subId}`
|
||||
insertVocab.run(cryptoRandomUUID(), 'skill_subcategory', key, subName, 1, now)
|
||||
subs++
|
||||
|
||||
for (const sk of (sub.skills || [])) {
|
||||
const sId = `${key}.${sk.id}`
|
||||
const sName = String(sk.name || sk.id)
|
||||
const requires = (catId === 'certifications' || subId === 'weapons') ? 1 : 0
|
||||
const expires = (catId === 'certifications') ? 36 : null
|
||||
insertSkill.run(sId, sName, key, null, requires, expires)
|
||||
skills++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`SkillSeeder: ensured ${cats} categories, ${subs} subcategories, ${skills} skills (idempotent).`)
|
||||
} catch (err) {
|
||||
logger.warn('SkillSeeder failed (non-fatal): ' + (err as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
function cryptoRandomUUID() {
|
||||
// Node 18 has crypto.randomUUID, but keep a tiny fallback
|
||||
try { return (require('crypto').randomUUID() as string) } catch {}
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren