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

Datei anzeigen

@ -0,0 +1,235 @@
import { Router } from 'express'
import { db } from '../config/database'
import { authenticateToken, AuthRequest } from '../middleware/auth'
const router = Router()
// Get workspace utilization analytics
router.get('/workspace-utilization', authenticateToken, (req: AuthRequest, res) => {
if (!['admin', 'superuser'].includes(req.user!.role)) {
return res.status(403).json({ error: 'Insufficient permissions' })
}
try {
const { from_date, to_date, workspace_id, workspace_type } = req.query
let query = `
SELECT
wa.*,
w.name as workspace_name,
w.type as workspace_type,
w.floor,
w.building
FROM workspace_analytics wa
JOIN workspaces w ON wa.workspace_id = w.id
WHERE 1=1
`
const params: any[] = []
if (from_date) {
query += ' AND wa.date >= ?'
params.push(from_date)
}
if (to_date) {
query += ' AND wa.date <= ?'
params.push(to_date)
}
if (workspace_id) {
query += ' AND wa.workspace_id = ?'
params.push(workspace_id)
}
if (workspace_type) {
query += ' AND w.type = ?'
params.push(workspace_type)
}
query += ' ORDER BY wa.date DESC'
const analytics = db.prepare(query).all(...params)
res.json(analytics)
} catch (error) {
console.error('Error fetching analytics:', error)
res.status(500).json({ error: 'Failed to fetch analytics' })
}
})
// Get overall statistics
router.get('/overview', authenticateToken, (req: AuthRequest, res) => {
if (!['admin', 'superuser'].includes(req.user!.role)) {
return res.status(403).json({ error: 'Insufficient permissions' })
}
try {
const { from_date = new Date().toISOString().split('T')[0], to_date } = req.query
// Total workspaces by type
const workspaceStats = db.prepare(`
SELECT type, COUNT(*) as count
FROM workspaces
WHERE is_active = 1
GROUP BY type
`).all()
// Booking statistics
const bookingStats = db.prepare(`
SELECT
COUNT(*) as total_bookings,
COUNT(DISTINCT user_id) as unique_users,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_bookings,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_bookings,
SUM(CASE WHEN status = 'no_show' THEN 1 ELSE 0 END) as no_shows
FROM bookings
WHERE start_time >= ?
${to_date ? 'AND end_time <= ?' : ''}
`).get(from_date, ...(to_date ? [to_date] : []))
// Average utilization by workspace type
const utilizationByType = db.prepare(`
SELECT
w.type,
AVG(wa.utilization_rate) as avg_utilization,
AVG(wa.total_hours_booked) as avg_hours_booked
FROM workspace_analytics wa
JOIN workspaces w ON wa.workspace_id = w.id
WHERE wa.date >= ?
${to_date ? 'AND wa.date <= ?' : ''}
GROUP BY w.type
`).all(from_date, ...(to_date ? [to_date] : []))
// Popular workspaces
const popularWorkspaces = db.prepare(`
SELECT
w.id,
w.name,
w.type,
w.floor,
COUNT(b.id) as booking_count,
AVG(
CAST((julianday(b.end_time) - julianday(b.start_time)) * 24 AS REAL)
) as avg_duration_hours
FROM workspaces w
JOIN bookings b ON w.id = b.workspace_id
WHERE b.start_time >= ?
${to_date ? 'AND b.end_time <= ?' : ''}
AND b.status != 'cancelled'
GROUP BY w.id, w.name, w.type, w.floor
ORDER BY booking_count DESC
LIMIT 10
`).all(from_date, ...(to_date ? [to_date] : []))
// Peak hours analysis
const peakHours = db.prepare(`
SELECT
CAST(strftime('%H', start_time) AS INTEGER) as hour,
COUNT(*) as booking_count
FROM bookings
WHERE start_time >= ?
${to_date ? 'AND end_time <= ?' : ''}
AND status != 'cancelled'
GROUP BY hour
ORDER BY hour
`).all(from_date, ...(to_date ? [to_date] : []))
res.json({
workspace_stats: workspaceStats,
booking_stats: bookingStats,
utilization_by_type: utilizationByType,
popular_workspaces: popularWorkspaces,
peak_hours: peakHours,
date_range: {
from: from_date,
to: to_date || new Date().toISOString().split('T')[0]
}
})
} catch (error) {
console.error('Error fetching overview:', error)
res.status(500).json({ error: 'Failed to fetch overview' })
}
})
// Update analytics data (should be run by a scheduled job)
router.post('/update', authenticateToken, (req: AuthRequest, res) => {
if (req.user!.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' })
}
try {
const date = req.body.date || new Date().toISOString().split('T')[0]
// Calculate analytics for each workspace
const workspaces = db.prepare('SELECT id FROM workspaces WHERE is_active = 1').all()
for (const workspace of workspaces) {
const dayStart = `${date}T00:00:00.000Z`
const dayEnd = `${date}T23:59:59.999Z`
// Get booking statistics for the day
const stats = db.prepare(`
SELECT
COUNT(*) as total_bookings,
COUNT(DISTINCT user_id) as unique_users,
SUM(
CASE
WHEN status = 'no_show' OR (status = 'confirmed' AND check_in_time IS NULL AND end_time < ?)
THEN 1
ELSE 0
END
) as no_show_count,
SUM(
CAST((julianday(end_time) - julianday(start_time)) * 24 AS REAL)
) as total_hours_booked
FROM bookings
WHERE workspace_id = ?
AND start_time >= ? AND start_time <= ?
AND status != 'cancelled'
`).get(new Date().toISOString(), (workspace as any).id, dayStart, dayEnd)
// Calculate peak hour
const peakHour = db.prepare(`
SELECT
CAST(strftime('%H', start_time) AS INTEGER) as hour,
COUNT(*) as count
FROM bookings
WHERE workspace_id = ?
AND start_time >= ? AND start_time <= ?
AND status != 'cancelled'
GROUP BY hour
ORDER BY count DESC
LIMIT 1
`).get((workspace as any).id, dayStart, dayEnd)
// Calculate utilization rate (assuming 10 hour work day)
const workHoursPerDay = 10
const utilizationRate = (stats as any).total_hours_booked / workHoursPerDay
// Insert or update analytics record
db.prepare(`
INSERT OR REPLACE INTO workspace_analytics (
id, workspace_id, date, total_bookings, total_hours_booked,
utilization_rate, no_show_count, unique_users, peak_hour
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
`${(workspace as any).id}-${date}`,
(workspace as any).id,
date,
(stats as any).total_bookings || 0,
(stats as any).total_hours_booked || 0,
utilizationRate || 0,
(stats as any).no_show_count || 0,
(stats as any).unique_users || 0,
(peakHour as any)?.hour || null
)
}
res.json({ message: 'Analytics updated successfully' })
} catch (error) {
console.error('Error updating analytics:', error)
res.status(500).json({ error: 'Failed to update analytics' })
}
})
export default router

121
backend/src/routes/auth.ts Normale Datei
Datei anzeigen

@ -0,0 +1,121 @@
import { Router, Request, Response, NextFunction } from 'express'
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import { body, validationResult } from 'express-validator'
import { db } from '../config/secureDatabase'
import { User, LoginRequest, LoginResponse } from '@skillmate/shared'
import { FieldEncryption } from '../services/encryption'
import { logger } from '../utils/logger'
const router = Router()
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'
router.post('/login',
[
body('username').optional().notEmpty().trim(),
body('email').optional().isEmail().normalizeEmail(),
body('password').notEmpty()
],
async (req: Request, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const { username, email, password } = req.body
// Determine login identifier (email takes precedence)
const loginIdentifier = email || username
if (!loginIdentifier) {
return res.status(400).json({
success: false,
error: { message: 'Either username or email is required' }
})
}
let userRow: any
// Try to find by email first (if looks like email), then by username
if (loginIdentifier.includes('@')) {
// Login with email
const emailHash = FieldEncryption.hash(loginIdentifier)
userRow = db.prepare(`
SELECT id, username, email, password, role, employee_id, last_login, is_active, created_at, updated_at
FROM users
WHERE email_hash = ? AND is_active = 1
`).get(emailHash) as any
} else {
// Login with username
userRow = db.prepare(`
SELECT id, username, email, password, role, employee_id, last_login, is_active, created_at, updated_at
FROM users
WHERE username = ? AND is_active = 1
`).get(loginIdentifier) as any
}
if (!userRow) {
return res.status(401).json({
success: false,
error: { message: 'Invalid credentials' }
})
}
// Check password
const isValidPassword = await bcrypt.compare(password, userRow.password)
if (!isValidPassword) {
return res.status(401).json({
success: false,
error: { message: 'Invalid credentials' }
})
}
// Update last login
const now = new Date().toISOString()
db.prepare('UPDATE users SET last_login = ? WHERE id = ?').run(now, userRow.id)
// Create user object without password (decrypt email)
const user: User = {
id: userRow.id,
username: userRow.username,
email: FieldEncryption.decrypt(userRow.email) || '',
role: userRow.role,
employeeId: userRow.employee_id,
lastLogin: new Date(now),
isActive: Boolean(userRow.is_active),
createdAt: new Date(userRow.created_at),
updatedAt: new Date(userRow.updated_at)
}
// Generate token
const token = jwt.sign(
{ user },
JWT_SECRET,
{ expiresIn: '24h' }
)
const response: LoginResponse = {
user,
token: {
accessToken: token,
expiresIn: 86400,
tokenType: 'Bearer'
}
}
logger.info(`User ${loginIdentifier} logged in successfully`)
res.json({ success: true, data: response })
} catch (error) {
next(error)
}
}
)
router.post('/logout', (req, res) => {
res.json({ success: true, message: 'Logged out successfully' })
})
export default router

330
backend/src/routes/bookings.ts Normale Datei
Datei anzeigen

@ -0,0 +1,330 @@
import { Router, Response } from 'express'
import { db } from '../config/database'
import { authenticateToken, AuthRequest } from '../middleware/auth'
import { v4 as uuidv4 } from 'uuid'
import { Booking, BookingRequest } from '@skillmate/shared'
const router = Router()
// Get user's bookings
router.get('/my-bookings', authenticateToken, (req: AuthRequest, res: Response) => {
try {
const { status, from_date, to_date } = req.query
let query = `
SELECT b.*, w.name as workspace_name, w.type as workspace_type,
w.floor, w.building, e.first_name, e.last_name, e.photo
FROM bookings b
JOIN workspaces w ON b.workspace_id = w.id
JOIN employees e ON b.employee_id = e.id
WHERE b.user_id = ?
`
const params: any[] = [req.user!.id]
if (status) {
query += ' AND b.status = ?'
params.push(status)
}
if (from_date) {
query += ' AND b.start_time >= ?'
params.push(from_date)
}
if (to_date) {
query += ' AND b.end_time <= ?'
params.push(to_date)
}
query += ' ORDER BY b.start_time DESC'
const bookings = db.prepare(query).all(...params)
res.json(bookings)
} catch (error) {
console.error('Error fetching bookings:', error)
res.status(500).json({ error: 'Failed to fetch bookings' })
}
})
// Get all bookings (admin/superuser)
router.get('/', authenticateToken, (req: AuthRequest, res: Response) => {
if (!['admin', 'superuser'].includes(req.user!.role)) {
return res.status(403).json({ error: 'Insufficient permissions' })
}
try {
const { workspace_id, user_id, status, from_date, to_date } = req.query
let query = `
SELECT b.*, w.name as workspace_name, w.type as workspace_type,
w.floor, w.building, e.first_name, e.last_name, e.photo,
u.username
FROM bookings b
JOIN workspaces w ON b.workspace_id = w.id
JOIN employees e ON b.employee_id = e.id
JOIN users u ON b.user_id = u.id
WHERE 1=1
`
const params: any[] = []
if (workspace_id) {
query += ' AND b.workspace_id = ?'
params.push(workspace_id)
}
if (user_id) {
query += ' AND b.user_id = ?'
params.push(user_id)
}
if (status) {
query += ' AND b.status = ?'
params.push(status)
}
if (from_date) {
query += ' AND b.start_time >= ?'
params.push(from_date)
}
if (to_date) {
query += ' AND b.end_time <= ?'
params.push(to_date)
}
query += ' ORDER BY b.start_time DESC'
const bookings = db.prepare(query).all(...params)
res.json(bookings)
} catch (error) {
console.error('Error fetching bookings:', error)
res.status(500).json({ error: 'Failed to fetch bookings' })
}
})
// Create booking
router.post('/', authenticateToken, (req: AuthRequest, res: Response) => {
try {
const bookingRequest: BookingRequest = req.body
const { workspace_id, start_time, end_time, notes, recurring } = bookingRequest
if (!workspace_id || !start_time || !end_time) {
return res.status(400).json({ error: 'Missing required fields' })
}
// Check if workspace exists and is active
const workspace = db.prepare('SELECT * FROM workspaces WHERE id = ? AND is_active = 1').get(workspace_id)
if (!workspace) {
return res.status(404).json({ error: 'Workspace not found or inactive' })
}
// Check for conflicts
const conflict = db.prepare(`
SELECT COUNT(*) as count FROM bookings
WHERE workspace_id = ? AND status = 'confirmed'
AND (
(start_time <= ? AND end_time > ?)
OR (start_time < ? AND end_time >= ?)
OR (start_time >= ? AND end_time <= ?)
)
`).get(workspace_id, start_time, start_time, end_time, end_time, start_time, end_time)
if ((conflict as any).count > 0) {
return res.status(409).json({ error: 'Time slot already booked' })
}
// Check booking rules
const rules = db.prepare('SELECT * FROM booking_rules WHERE workspace_type = ?').get((workspace as any).type)
if (rules) {
// Validate against rules
const startDate = new Date(start_time)
const endDate = new Date(end_time)
const now = new Date()
// Check max duration
if ((rules as any).max_duration_hours) {
const durationHours = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60)
if (durationHours > (rules as any).max_duration_hours) {
return res.status(400).json({
error: `Maximum booking duration is ${(rules as any).max_duration_hours} hours`
})
}
}
// Check advance booking limits
if ((rules as any).max_advance_days) {
const daysInAdvance = (startDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
if (daysInAdvance > (rules as any).max_advance_days) {
return res.status(400).json({
error: `Cannot book more than ${(rules as any).max_advance_days} days in advance`
})
}
}
if ((rules as any).min_advance_hours) {
const hoursInAdvance = (startDate.getTime() - now.getTime()) / (1000 * 60 * 60)
if (hoursInAdvance < (rules as any).min_advance_hours) {
return res.status(400).json({
error: `Must book at least ${(rules as any).min_advance_hours} hours in advance`
})
}
}
}
const bookingId = uuidv4()
const now = new Date().toISOString()
db.prepare(`
INSERT INTO bookings (
id, workspace_id, user_id, employee_id, start_time, end_time,
status, notes, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
bookingId, workspace_id, req.user!.id, req.user!.employeeId,
start_time, end_time, 'confirmed', notes, now, now
)
// Handle recurring bookings
if (recurring) {
const recurringId = uuidv4()
const startDate = new Date(start_time)
const startTimeOnly = startDate.toTimeString().substring(0, 8)
const endTimeOnly = new Date(end_time).toTimeString().substring(0, 8)
db.prepare(`
INSERT INTO recurring_bookings (
id, workspace_id, user_id, employee_id, start_date, end_date,
time_start, time_end, days_of_week, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
recurringId, workspace_id, req.user!.id, req.user!.employeeId,
start_time, recurring.end_date, startTimeOnly, endTimeOnly,
JSON.stringify(recurring.days_of_week), now
)
// Create individual bookings for the recurring series
// This is a simplified version - in production you'd want a background job
createRecurringBookings(recurringId, bookingRequest, req.user)
}
const newBooking = db.prepare(`
SELECT b.*, w.name as workspace_name, w.type as workspace_type
FROM bookings b
JOIN workspaces w ON b.workspace_id = w.id
WHERE b.id = ?
`).get(bookingId)
res.status(201).json(newBooking)
} catch (error) {
console.error('Error creating booking:', error)
res.status(500).json({ error: 'Failed to create booking' })
}
})
// Check in to booking
router.post('/:id/check-in', authenticateToken, (req: AuthRequest, res: Response) => {
try {
const booking = db.prepare('SELECT * FROM bookings WHERE id = ? AND user_id = ?')
.get(req.params.id, req.user!.id)
if (!booking) {
return res.status(404).json({ error: 'Booking not found' })
}
if ((booking as any).status !== 'confirmed') {
return res.status(400).json({ error: 'Booking is not confirmed' })
}
const now = new Date()
const startTime = new Date((booking as any).start_time)
// Allow check-in 15 minutes before start time
const earliestCheckIn = new Date(startTime.getTime() - 15 * 60 * 1000)
if (now < earliestCheckIn) {
return res.status(400).json({ error: 'Too early to check in' })
}
db.prepare(`
UPDATE bookings
SET check_in_time = ?, updated_at = ?
WHERE id = ?
`).run(now.toISOString(), now.toISOString(), req.params.id)
res.json({ message: 'Checked in successfully' })
} catch (error) {
console.error('Error checking in:', error)
res.status(500).json({ error: 'Failed to check in' })
}
})
// Check out from booking
router.post('/:id/check-out', authenticateToken, (req: AuthRequest, res: Response) => {
try {
const booking = db.prepare('SELECT * FROM bookings WHERE id = ? AND user_id = ?')
.get(req.params.id, req.user!.id)
if (!booking) {
return res.status(404).json({ error: 'Booking not found' })
}
if (!(booking as any).check_in_time) {
return res.status(400).json({ error: 'Not checked in' })
}
const now = new Date().toISOString()
db.prepare(`
UPDATE bookings
SET check_out_time = ?, status = 'completed', updated_at = ?
WHERE id = ?
`).run(now, now, req.params.id)
res.json({ message: 'Checked out successfully' })
} catch (error) {
console.error('Error checking out:', error)
res.status(500).json({ error: 'Failed to check out' })
}
})
// Cancel booking
router.post('/:id/cancel', authenticateToken, (req: AuthRequest, res: Response) => {
try {
const booking = db.prepare('SELECT * FROM bookings WHERE id = ?').get(req.params.id)
if (!booking) {
return res.status(404).json({ error: 'Booking not found' })
}
// Users can only cancel their own bookings, admins can cancel any
if ((booking as any).user_id !== req.user!.id && req.user!.role !== 'admin') {
return res.status(403).json({ error: 'Not authorized to cancel this booking' })
}
if ((booking as any).status !== 'confirmed') {
return res.status(400).json({ error: 'Booking is not confirmed' })
}
const now = new Date().toISOString()
db.prepare(`
UPDATE bookings
SET status = 'cancelled', updated_at = ?
WHERE id = ?
`).run(now, req.params.id)
res.json({ message: 'Booking cancelled successfully' })
} catch (error) {
console.error('Error cancelling booking:', error)
res.status(500).json({ error: 'Failed to cancel booking' })
}
})
// Helper function to create recurring bookings
function createRecurringBookings(recurringId: string, request: BookingRequest, user: any) {
// This is a simplified implementation
// In production, this should be handled by a background job
// to avoid timeout issues with many bookings
}
export default router

Datei anzeigen

@ -0,0 +1,561 @@
import { Router, Response, NextFunction } from 'express'
import { v4 as uuidv4 } from 'uuid'
import { body, validationResult } from 'express-validator'
import bcrypt from 'bcrypt'
import { db } from '../config/database'
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
import { requirePermission, requireEditPermission } from '../middleware/roleAuth'
import { Employee, LanguageSkill, Skill, UserRole } from '@skillmate/shared'
import { syncService } from '../services/syncService'
import { FieldEncryption } from '../services/encryption'
const router = Router()
// Helper function to map old proficiency to new level
function mapProficiencyToLevel(proficiency: string): 'basic' | 'fluent' | 'native' | 'business' {
const mapping: Record<string, 'basic' | 'fluent' | 'native' | 'business'> = {
'A1': 'basic',
'A2': 'basic',
'B1': 'business',
'B2': 'business',
'C1': 'fluent',
'C2': 'fluent',
'Muttersprache': 'native',
'native': 'native',
'fluent': 'fluent',
'advanced': 'business',
'intermediate': 'business',
'basic': 'basic'
}
return mapping[proficiency] || 'basic'
}
// Get all employees
router.get('/', authenticate, requirePermission('employees:read'), async (req: AuthRequest, res, next) => {
try {
const employees = db.prepare(`
SELECT id, first_name, last_name, employee_number, photo, position,
department, email, phone, mobile, office, availability,
clearance_level, clearance_valid_until, clearance_issued_date,
created_at, updated_at, created_by, updated_by
FROM employees
ORDER BY last_name, first_name
`).all()
const employeesWithDetails = employees.map((emp: any) => {
// Get skills
const skills = db.prepare(`
SELECT s.id, s.name, s.category, es.level, es.verified, es.verified_by, es.verified_date
FROM employee_skills es
JOIN skills s ON es.skill_id = s.id
WHERE es.employee_id = ?
`).all(emp.id)
// Get languages
const languages = db.prepare(`
SELECT language, proficiency, certified, certificate_type, is_native, can_interpret
FROM language_skills
WHERE employee_id = ?
`).all(emp.id)
// Get specializations
const specializations = db.prepare(`
SELECT name FROM specializations WHERE employee_id = ?
`).all(emp.id).map((s: any) => s.name)
const employee: Employee = {
id: emp.id,
firstName: emp.first_name,
lastName: emp.last_name,
employeeNumber: emp.employee_number,
photo: emp.photo,
position: emp.position,
department: emp.department,
email: emp.email,
phone: emp.phone,
mobile: emp.mobile,
office: emp.office,
availability: emp.availability,
skills: skills.map((s: any) => ({
id: s.id,
name: s.name,
category: s.category,
level: s.level,
verified: Boolean(s.verified),
verifiedBy: s.verified_by,
verifiedDate: s.verified_date ? new Date(s.verified_date) : undefined
})),
languages: languages.map((l: any) => ({
code: l.language, // Map language to code for new interface
level: mapProficiencyToLevel(l.proficiency) // Map old proficiency to new level
})),
clearance: emp.clearance_level ? {
level: emp.clearance_level,
validUntil: new Date(emp.clearance_valid_until),
issuedDate: new Date(emp.clearance_issued_date)
} : undefined,
specializations,
createdAt: new Date(emp.created_at),
updatedAt: new Date(emp.updated_at),
createdBy: emp.created_by,
updatedBy: emp.updated_by
}
return employee
})
res.json({ success: true, data: employeesWithDetails })
} catch (error) {
next(error)
}
})
// Get employee by ID
router.get('/:id', authenticate, requirePermission('employees:read'), async (req: AuthRequest, res, next) => {
try {
const { id } = req.params
const emp = db.prepare(`
SELECT id, first_name, last_name, employee_number, photo, position,
department, email, phone, mobile, office, availability,
clearance_level, clearance_valid_until, clearance_issued_date,
created_at, updated_at, created_by, updated_by
FROM employees
WHERE id = ?
`).get(id) as any
if (!emp) {
return res.status(404).json({
success: false,
error: { message: 'Employee not found' }
})
}
// Get skills
const skills = db.prepare(`
SELECT s.id, s.name, s.category, es.level, es.verified, es.verified_by, es.verified_date
FROM employee_skills es
JOIN skills s ON es.skill_id = s.id
WHERE es.employee_id = ?
`).all(emp.id)
// Get languages
const languages = db.prepare(`
SELECT language, proficiency, certified, certificate_type, is_native, can_interpret
FROM language_skills
WHERE employee_id = ?
`).all(emp.id)
// Get specializations
const specializations = db.prepare(`
SELECT name FROM specializations WHERE employee_id = ?
`).all(emp.id).map((s: any) => s.name)
const employee: Employee = {
id: emp.id,
firstName: emp.first_name,
lastName: emp.last_name,
employeeNumber: emp.employee_number,
photo: emp.photo,
position: emp.position,
department: emp.department,
email: emp.email,
phone: emp.phone,
mobile: emp.mobile,
office: emp.office,
availability: emp.availability,
skills: skills.map((s: any) => ({
id: s.id,
name: s.name,
category: s.category,
level: s.level,
verified: Boolean(s.verified),
verifiedBy: s.verified_by,
verifiedDate: s.verified_date ? new Date(s.verified_date) : undefined
})),
languages: languages.map((l: any) => ({
code: l.language, // Map language to code for new interface
level: mapProficiencyToLevel(l.proficiency) // Map old proficiency to new level
})),
clearance: emp.clearance_level ? {
level: emp.clearance_level,
validUntil: new Date(emp.clearance_valid_until),
issuedDate: new Date(emp.clearance_issued_date)
} : undefined,
specializations,
createdAt: new Date(emp.created_at),
updatedAt: new Date(emp.updated_at),
createdBy: emp.created_by,
updatedBy: emp.updated_by
}
res.json({ success: true, data: employee })
} catch (error) {
next(error)
}
})
// Create employee (admin/poweruser only)
router.post('/',
authenticate,
requirePermission('employees:create'),
[
body('firstName').notEmpty().trim(),
body('lastName').notEmpty().trim(),
body('email').isEmail(),
body('department').notEmpty().trim()
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const employeeId = uuidv4()
const now = new Date().toISOString()
const {
firstName, lastName, employeeNumber, photo, position,
department, email, phone, mobile, office, availability,
clearance, skills, languages, specializations, userRole, createUser
} = req.body
// Insert employee with default values for missing fields
db.prepare(`
INSERT INTO employees (
id, first_name, last_name, employee_number, photo, position,
department, email, phone, mobile, office, availability,
clearance_level, clearance_valid_until, clearance_issued_date,
created_at, updated_at, created_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
employeeId,
firstName,
lastName,
employeeNumber || null,
photo || null,
position || 'Mitarbeiter', // Default position
department,
email,
phone || 'Nicht angegeben', // Default phone
mobile || null,
office || null,
availability || 'available', // Default availability
clearance?.level || null,
clearance?.validUntil || null,
clearance?.issuedDate || null,
now, now, req.user!.id
)
// Insert skills
if (skills && skills.length > 0) {
const insertSkill = db.prepare(`
INSERT INTO employee_skills (employee_id, skill_id, level, verified, verified_by, verified_date)
VALUES (?, ?, ?, ?, ?, ?)
`)
for (const skill of skills) {
insertSkill.run(
employeeId,
skill.id,
skill.level || null,
skill.verified ? 1 : 0,
skill.verifiedBy || null,
skill.verifiedDate || null
)
}
}
// Insert languages
if (languages && languages.length > 0) {
const insertLang = db.prepare(`
INSERT INTO language_skills (
id, employee_id, language, proficiency, certified,
certificate_type, is_native, can_interpret
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`)
for (const lang of languages) {
insertLang.run(
uuidv4(),
employeeId,
lang.language,
lang.proficiency,
lang.certified ? 1 : 0,
lang.certificateType || null,
lang.isNative ? 1 : 0,
lang.canInterpret ? 1 : 0
)
}
}
// Insert specializations
if (specializations && specializations.length > 0) {
const insertSpec = db.prepare(`
INSERT INTO specializations (id, employee_id, name)
VALUES (?, ?, ?)
`)
for (const spec of specializations) {
insertSpec.run(uuidv4(), employeeId, spec)
}
}
// Queue sync for new employee
const newEmployee = {
id: employeeId,
firstName,
lastName,
employeeNumber: employeeNumber || null,
photo: photo || null,
position: position || 'Mitarbeiter',
department,
email,
phone: phone || 'Nicht angegeben',
mobile: mobile || null,
office: office || null,
availability: availability || 'available',
clearance,
skills: skills || [],
languages: languages || [],
specializations: specializations || [],
createdAt: now,
updatedAt: now,
createdBy: req.user!.id
}
// Create user account if requested
let userId = null
let temporaryPassword = null
if (createUser && userRole) {
try {
userId = uuidv4()
// Generate a secure temporary password
temporaryPassword = `TempPass${Math.random().toString(36).slice(-8)}!@#`
const hashedPassword = await bcrypt.hash(temporaryPassword, 10)
// Encrypt email for user table storage
const encryptedEmail = FieldEncryption.encrypt(email)
db.prepare(`
INSERT INTO users (id, username, email, password, role, employee_id, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
userId, email, encryptedEmail, hashedPassword, userRole, employeeId, 1, now, now
)
console.log(`User created for employee ${firstName} ${lastName} with role ${userRole}`)
} catch (userError) {
console.error('Error creating user account:', userError)
// Continue without failing the employee creation
temporaryPassword = null
}
}
await syncService.queueSync('employees', 'create', newEmployee)
res.status(201).json({
success: true,
data: {
id: employeeId,
userId: userId,
temporaryPassword: temporaryPassword
},
message: `Employee created successfully${createUser ? ' with user account' : ''}`
})
} catch (error) {
next(error)
}
}
)
// Update employee (admin/poweruser only)
router.put('/:id',
authenticate,
requireEditPermission(req => req.params.id),
[
body('firstName').notEmpty().trim(),
body('lastName').notEmpty().trim(),
body('position').notEmpty().trim(),
body('department').notEmpty().trim(),
body('email').isEmail(),
body('phone').notEmpty().trim(),
body('availability').isIn(['available', 'parttime', 'unavailable', 'busy', 'away', 'vacation', 'sick', 'training', 'operation'])
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const { id } = req.params
const now = new Date().toISOString()
const {
firstName, lastName, position, department, email, phone,
mobile, office, availability, clearance, skills, languages, specializations
} = req.body
// Check if employee exists
const existing = db.prepare('SELECT id FROM employees WHERE id = ?').get(id)
if (!existing) {
return res.status(404).json({
success: false,
error: { message: 'Employee not found' }
})
}
// Update employee
db.prepare(`
UPDATE employees SET
first_name = ?, last_name = ?, position = ?, department = ?,
email = ?, phone = ?, mobile = ?, office = ?, availability = ?,
clearance_level = ?, clearance_valid_until = ?, clearance_issued_date = ?,
updated_at = ?, updated_by = ?
WHERE id = ?
`).run(
firstName, lastName, position, department,
email, phone, mobile || null, office || null, availability,
clearance?.level || null, clearance?.validUntil || null, clearance?.issuedDate || null,
now, req.user!.id, id
)
// Update skills
if (skills !== undefined) {
// Delete existing skills
db.prepare('DELETE FROM employee_skills WHERE employee_id = ?').run(id)
// Insert new skills
if (skills.length > 0) {
const insertSkill = db.prepare(`
INSERT INTO employee_skills (employee_id, skill_id, level, verified, verified_by, verified_date)
VALUES (?, ?, ?, ?, ?, ?)
`)
for (const skill of skills) {
insertSkill.run(
id,
skill.id,
skill.level || null,
skill.verified ? 1 : 0,
skill.verifiedBy || null,
skill.verifiedDate || null
)
}
}
}
// Queue sync for updated employee
const updatedEmployee = {
id,
firstName,
lastName,
position,
department,
email,
phone,
mobile: mobile || null,
office: office || null,
availability,
clearance,
skills,
languages,
specializations,
updatedAt: now,
updatedBy: req.user!.id
}
await syncService.queueSync('employees', 'update', updatedEmployee)
res.json({
success: true,
message: 'Employee updated successfully'
})
} catch (error) {
next(error)
}
}
)
// Delete employee (admin only)
router.delete('/:id',
authenticate,
requirePermission('employees:delete'),
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
// Check if employee exists
const existing = db.prepare('SELECT id FROM employees WHERE id = ?').get(id)
if (!existing) {
return res.status(404).json({
success: false,
error: { message: 'Employee not found' }
})
}
// Delete employee and related data
db.prepare('DELETE FROM employee_skills WHERE employee_id = ?').run(id)
db.prepare('DELETE FROM language_skills WHERE employee_id = ?').run(id)
db.prepare('DELETE FROM specializations WHERE employee_id = ?').run(id)
db.prepare('DELETE FROM employees WHERE id = ?').run(id)
// Queue sync for deleted employee
await syncService.queueSync('employees', 'delete', { id })
res.json({
success: true,
message: 'Employee deleted successfully'
})
} catch (error) {
next(error)
}
}
)
// Search employees by skills
router.post('/search', authenticate, requirePermission('employees:read'), async (req: AuthRequest, res, next) => {
try {
const { skills, category } = req.body
let query = `
SELECT DISTINCT e.id, e.first_name, e.last_name, e.employee_number,
e.position, e.department, e.availability
FROM employees e
JOIN employee_skills es ON e.id = es.employee_id
JOIN skills s ON es.skill_id = s.id
WHERE 1=1
`
const params: any[] = []
if (skills && skills.length > 0) {
const placeholders = skills.map(() => '?').join(',')
query += ` AND s.name IN (${placeholders})`
params.push(...skills)
}
if (category) {
query += ` AND s.category = ?`
params.push(category)
}
query += ` ORDER BY e.last_name, e.first_name`
const results = db.prepare(query).all(...params)
res.json({ success: true, data: results })
} catch (error) {
next(error)
}
})
export default router

Datei anzeigen

@ -0,0 +1,813 @@
import { Router, Response, NextFunction } from 'express'
import { v4 as uuidv4 } from 'uuid'
import { body, validationResult } from 'express-validator'
import bcrypt from 'bcrypt'
import { db, encryptedDb } from '../config/secureDatabase'
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
import { requirePermission, requireEditPermission } from '../middleware/roleAuth'
import { Employee, LanguageSkill, Skill, UserRole } from '@skillmate/shared'
import { syncService } from '../services/syncService'
import { FieldEncryption } from '../services/encryption'
import { emailService } from '../services/emailService'
import { logger } from '../utils/logger'
const router = Router()
// Helper function to map old proficiency to new level
function mapProficiencyToLevel(proficiency: string): 'basic' | 'fluent' | 'native' | 'business' {
const mapping: Record<string, 'basic' | 'fluent' | 'native' | 'business'> = {
'A1': 'basic',
'A2': 'basic',
'B1': 'business',
'B2': 'business',
'C1': 'fluent',
'C2': 'fluent',
'Muttersprache': 'native',
'native': 'native',
'fluent': 'fluent',
'advanced': 'business',
'intermediate': 'business',
'basic': 'basic'
}
return mapping[proficiency] || 'basic'
}
// Log security audit events
function logSecurityAudit(
action: string,
entityType: string,
entityId: string,
userId: string,
req: AuthRequest,
riskLevel: 'low' | 'medium' | 'high' | 'critical' = 'low'
) {
try {
db.prepare(`
INSERT INTO security_audit_log (
id, entity_type, entity_id, action, user_id,
timestamp, ip_address, user_agent, risk_level
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
uuidv4(),
entityType,
entityId,
action,
userId,
new Date().toISOString(),
req.ip || req.connection.remoteAddress,
req.get('user-agent'),
riskLevel
)
} catch (error) {
logger.error('Failed to log security audit:', error)
}
}
// Get all employees
router.get('/', authenticate, requirePermission('employees:read'), async (req: AuthRequest, res, next) => {
try {
const employees = encryptedDb.getAllEmployees()
const employeesWithDetails = employees.map((emp: any) => {
// Get skills
const skills = db.prepare(`
SELECT s.id, s.name, s.category, es.level, es.verified, es.verified_by, es.verified_date
FROM employee_skills es
JOIN skills s ON es.skill_id = s.id
WHERE es.employee_id = ?
`).all(emp.id)
// Get languages
const languages = db.prepare(`
SELECT language, proficiency, certified, certificate_type, is_native, can_interpret
FROM language_skills
WHERE employee_id = ?
`).all(emp.id)
// Get specializations
const specializations = db.prepare(`
SELECT name FROM specializations WHERE employee_id = ?
`).all(emp.id).map((s: any) => s.name)
const employee: Employee = {
id: emp.id,
firstName: emp.first_name,
lastName: emp.last_name,
employeeNumber: (emp.employee_number && !String(emp.employee_number).startsWith('EMP')) ? emp.employee_number : undefined,
photo: emp.photo,
position: emp.position,
department: emp.department,
email: emp.email,
phone: emp.phone,
mobile: emp.mobile,
office: emp.office,
availability: emp.availability,
skills: skills.map((s: any) => ({
id: s.id,
name: s.name,
category: s.category,
level: s.level,
verified: Boolean(s.verified),
verifiedBy: s.verified_by,
verifiedDate: s.verified_date ? new Date(s.verified_date) : undefined
})),
languages: languages.map((l: any) => ({
code: l.language,
level: mapProficiencyToLevel(l.proficiency)
})),
clearance: emp.clearance_level && emp.clearance_valid_until && emp.clearance_issued_date ? {
level: emp.clearance_level,
validUntil: new Date(emp.clearance_valid_until),
issuedDate: new Date(emp.clearance_issued_date)
} : undefined,
specializations,
createdAt: new Date(emp.created_at),
updatedAt: new Date(emp.updated_at),
createdBy: emp.created_by,
updatedBy: emp.updated_by
}
return employee
})
// Log read access
logSecurityAudit('read', 'employees', 'all', req.user!.id, req, 'low')
res.json({ success: true, data: employeesWithDetails })
} catch (error) {
logger.error('Error fetching employees:', error)
next(error)
}
})
// Public employees for frontend: only those with a linked user (excluding admin)
router.get('/public', authenticate, async (req: AuthRequest, res, next) => {
try {
// Get allowed employee IDs from users table (exclude admin, only active accounts)
const allowedUserLinks = db.prepare(`
SELECT employee_id FROM users
WHERE employee_id IS NOT NULL AND username <> 'admin' AND is_active = 1
`).all() as any[]
const allowedIds = new Set((allowedUserLinks || []).map((u: any) => u.employee_id))
const employees = encryptedDb.getAllEmployees()
.filter((emp: any) => allowedIds.has(emp.id))
const employeesWithDetails = employees.map((emp: any) => {
const skills = db.prepare(`
SELECT s.id, s.name, s.category, es.level, es.verified, es.verified_by, es.verified_date
FROM employee_skills es
JOIN skills s ON es.skill_id = s.id
WHERE es.employee_id = ?
`).all(emp.id)
const languages = db.prepare(`
SELECT language, proficiency, certified, certificate_type, is_native, can_interpret
FROM language_skills
WHERE employee_id = ?
`).all(emp.id)
const specializations = db.prepare(`
SELECT name FROM specializations WHERE employee_id = ?
`).all(emp.id).map((s: any) => s.name)
const employee: Employee = {
id: emp.id,
firstName: emp.first_name,
lastName: emp.last_name,
employeeNumber: (emp.employee_number && !String(emp.employee_number).startsWith('EMP')) ? emp.employee_number : undefined,
photo: emp.photo,
position: emp.position,
department: emp.department,
email: emp.email,
phone: emp.phone,
mobile: emp.mobile,
office: emp.office,
availability: emp.availability,
skills: skills.map((s: any) => ({
id: s.id,
name: s.name,
category: s.category,
level: s.level,
verified: Boolean(s.verified),
verifiedBy: s.verified_by,
verifiedDate: s.verified_date ? new Date(s.verified_date) : undefined
})),
languages: languages.map((l: any) => ({
code: l.language,
level: mapProficiencyToLevel(l.proficiency)
})),
clearance: emp.clearance_level && emp.clearance_valid_until && emp.clearance_issued_date ? {
level: emp.clearance_level,
validUntil: new Date(emp.clearance_valid_until),
issuedDate: new Date(emp.clearance_issued_date)
} : undefined,
specializations,
createdAt: new Date(emp.created_at),
updatedAt: new Date(emp.updated_at),
createdBy: emp.created_by,
updatedBy: emp.updated_by
}
return employee
})
res.json({ success: true, data: employeesWithDetails })
} catch (error) {
logger.error('Error fetching public employees:', error)
next(error)
}
})
// Get employee by ID
router.get('/:id', authenticate, requirePermission('employees:read'), async (req: AuthRequest, res, next) => {
try {
const { id } = req.params
const emp = encryptedDb.getEmployee(id)
if (!emp) {
return res.status(404).json({
success: false,
error: { message: 'Employee not found' }
})
}
// Get skills
const skills = db.prepare(`
SELECT s.id, s.name, s.category, es.level, es.verified, es.verified_by, es.verified_date
FROM employee_skills es
JOIN skills s ON es.skill_id = s.id
WHERE es.employee_id = ?
`).all(emp.id)
// Get languages
const languages = db.prepare(`
SELECT language, proficiency, certified, certificate_type, is_native, can_interpret
FROM language_skills
WHERE employee_id = ?
`).all(emp.id)
// Get specializations
const specializations = db.prepare(`
SELECT name FROM specializations WHERE employee_id = ?
`).all(emp.id).map((s: any) => s.name)
const employee: Employee = {
id: emp.id,
firstName: emp.first_name,
lastName: emp.last_name,
employeeNumber: (emp.employee_number && !String(emp.employee_number).startsWith('EMP')) ? emp.employee_number : undefined,
photo: emp.photo,
position: emp.position,
department: emp.department,
email: emp.email,
phone: emp.phone,
mobile: emp.mobile,
office: emp.office,
availability: emp.availability,
skills: skills.map((s: any) => ({
id: s.id,
name: s.name,
category: s.category,
level: s.level,
verified: Boolean(s.verified),
verifiedBy: s.verified_by,
verifiedDate: s.verified_date ? new Date(s.verified_date) : undefined
})),
languages: languages.map((l: any) => ({
code: l.language,
level: mapProficiencyToLevel(l.proficiency)
})),
clearance: emp.clearance_level && emp.clearance_valid_until && emp.clearance_issued_date ? {
level: emp.clearance_level,
validUntil: new Date(emp.clearance_valid_until),
issuedDate: new Date(emp.clearance_issued_date)
} : undefined,
specializations,
createdAt: new Date(emp.created_at),
updatedAt: new Date(emp.updated_at),
createdBy: emp.created_by,
updatedBy: emp.updated_by
}
// Log read access
logSecurityAudit('read', 'employees', id, req.user!.id, req, 'low')
res.json({ success: true, data: employee })
} catch (error) {
logger.error('Error fetching employee:', error)
next(error)
}
})
// Create employee (admin/poweruser only)
router.post('/',
authenticate,
requirePermission('employees:create'),
[
body('firstName').notEmpty().trim().escape(),
body('lastName').notEmpty().trim().escape(),
body('email').isEmail().normalizeEmail(),
body('department').notEmpty().trim().escape(),
body('position').optional().trim().escape(), // Optional
body('phone').optional().trim(), // Optional - kann später ergänzt werden
body('employeeNumber').optional().trim() // Optional - wird automatisch generiert wenn leer
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
const transaction = db.transaction(() => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const employeeId = uuidv4()
const now = new Date().toISOString()
const {
firstName, lastName, employeeNumber, photo, position = 'Mitarbeiter',
department, email, phone = 'Nicht angegeben', mobile, office, availability = 'available',
clearance, skills = [], languages = [], specializations = [], userRole, createUser
} = req.body
// Generate employee number if not provided
const finalEmployeeNumber = employeeNumber || `EMP${Date.now()}`
// Check if employee number already exists
const existingEmployee = db.prepare('SELECT id FROM employees WHERE employee_number = ?').get(finalEmployeeNumber)
if (existingEmployee) {
return res.status(409).json({
success: false,
error: { message: 'Employee number already exists' }
})
}
// Insert employee with encrypted fields
encryptedDb.insertEmployee({
id: employeeId,
first_name: firstName,
last_name: lastName,
employee_number: finalEmployeeNumber,
photo: photo || null,
position,
department,
email,
phone,
mobile: mobile || null,
office: office || null,
availability,
clearance_level: clearance?.level || null,
clearance_valid_until: clearance?.validUntil || null,
clearance_issued_date: clearance?.issuedDate || null,
created_at: now,
updated_at: now,
created_by: req.user!.id
})
// Insert skills (only if they exist in skills table)
if (skills && skills.length > 0) {
const insertSkill = db.prepare(`
INSERT INTO employee_skills (employee_id, skill_id, level, verified, verified_by, verified_date)
VALUES (?, ?, ?, ?, ?, ?)
`)
const checkSkillExists = db.prepare('SELECT id FROM skills WHERE id = ?')
for (const skill of skills) {
// Check if skill exists before inserting
const skillExists = checkSkillExists.get(skill.id)
if (skillExists) {
insertSkill.run(
employeeId,
skill.id,
skill.level || null,
skill.verified ? 1 : 0,
skill.verifiedBy || null,
skill.verifiedDate || null
)
} else {
logger.warn(`Skill with ID ${skill.id} does not exist in skills table, skipping`)
}
}
}
// Insert languages
if (languages && languages.length > 0) {
const insertLang = db.prepare(`
INSERT INTO language_skills (
id, employee_id, language, proficiency, certified,
certificate_type, is_native, can_interpret
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`)
for (const lang of languages) {
insertLang.run(
uuidv4(),
employeeId,
lang.code || lang.language,
lang.level || lang.proficiency || 'basic',
lang.certified ? 1 : 0,
lang.certificateType || null,
lang.isNative ? 1 : 0,
lang.canInterpret ? 1 : 0
)
}
}
// Insert specializations
if (specializations && specializations.length > 0) {
const insertSpec = db.prepare(`
INSERT INTO specializations (id, employee_id, name)
VALUES (?, ?, ?)
`)
for (const spec of specializations) {
insertSpec.run(uuidv4(), employeeId, spec)
}
}
// Create user account if requested
let userId = null
let tempPassword = null
if (createUser) {
userId = uuidv4()
tempPassword = `Temp${Math.random().toString(36).slice(-8)}!@#`
const hashedPassword = bcrypt.hashSync(tempPassword, 12)
// Enforce role policy: only admins may assign roles; others default to 'user'
const assignedRole = req.user?.role === 'admin' && userRole ? userRole : 'user'
db.prepare(`
INSERT INTO users (id, username, email, email_hash, password, role, employee_id, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
userId,
email,
FieldEncryption.encrypt(email),
FieldEncryption.hash(email),
hashedPassword,
assignedRole,
employeeId,
1,
now,
now
)
logger.info(`User created for employee ${firstName} ${lastName} with role ${userRole}`)
}
// Log creation
logSecurityAudit('create', 'employees', employeeId, req.user!.id, req, 'medium')
// Queue sync for new employee
const newEmployee = {
id: employeeId,
firstName,
lastName,
employeeNumber: finalEmployeeNumber,
photo: photo || null,
position,
department,
email,
phone,
mobile: mobile || null,
office: office || null,
availability,
clearance,
skills,
languages,
specializations,
createdAt: now,
updatedAt: now,
createdBy: req.user!.id
}
syncService.queueSync('employees', 'create', newEmployee).catch(err => {
logger.error('Failed to queue sync:', err)
})
return res.status(201).json({
success: true,
data: {
id: employeeId,
userId: userId,
temporaryPassword: tempPassword
},
message: `Employee created successfully${createUser ? ' with user account' : ''}`
})
} catch (error) {
logger.error('Error creating employee:', error)
throw error
}
})
try {
const result = transaction()
// Send email after successful transaction (if user was created)
const { createUser, userRole, email, firstName } = req.body
if (createUser && userRole && result && (result as any).data?.userId && (result as any).data?.temporaryPassword) {
// Check if email notifications are enabled
const emailNotificationsSetting = db.prepare('SELECT value FROM system_settings WHERE key = ?').get('email_notifications_enabled') as any
const emailNotificationsEnabled = emailNotificationsSetting?.value === 'true'
// Send initial password via email if notifications are enabled
if (emailNotificationsEnabled && emailService.isServiceEnabled()) {
const emailSent = await emailService.sendInitialPassword(email, (result as any).data.temporaryPassword, firstName)
if (emailSent) {
logger.info(`Initial password email sent to ${email}`)
} else {
logger.warn(`Failed to send initial password email to ${email}`)
}
}
}
return result
} catch (error: any) {
logger.error('Transaction failed:', error)
return res.status(500).json({
success: false,
error: {
message: 'Failed to create employee',
details: process.env.NODE_ENV === 'development' ? error.message : undefined
}
})
}
}
)
// Update employee
router.put('/:id',
authenticate,
requireEditPermission(req => req.params.id),
[
body('firstName').notEmpty().trim().escape(),
body('lastName').notEmpty().trim().escape(),
body('position').optional().trim().escape(),
body('department').notEmpty().trim().escape(),
body('email').isEmail().normalizeEmail(),
body('phone').optional().trim(),
body('employeeNumber').optional().trim(),
body('availability').optional().isIn(['available', 'parttime', 'unavailable', 'busy', 'away', 'vacation', 'sick', 'training', 'operation'])
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
const transaction = db.transaction(() => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const { id } = req.params
const now = new Date().toISOString()
const {
firstName, lastName, position = 'Mitarbeiter', department, email, phone = 'Nicht angegeben',
mobile, office, availability = 'available', clearance, skills, languages, specializations,
employeeNumber
} = req.body
// Check if employee exists
const existing = db.prepare('SELECT id FROM employees WHERE id = ?').get(id)
if (!existing) {
return res.status(404).json({
success: false,
error: { message: 'Employee not found' }
})
}
// Update employee with encrypted fields
db.prepare(`
UPDATE employees SET
first_name = ?, last_name = ?, position = ?, department = ?,
email = ?, email_hash = ?, phone = ?, phone_hash = ?,
mobile = ?, office = ?, availability = ?,
clearance_level = ?, clearance_valid_until = ?, clearance_issued_date = ?,
updated_at = ?, updated_by = ?
WHERE id = ?
`).run(
firstName, lastName, position, department,
FieldEncryption.encrypt(email),
FieldEncryption.hash(email),
FieldEncryption.encrypt(phone || ''),
FieldEncryption.hash(phone || ''),
mobile ? FieldEncryption.encrypt(mobile) : null,
office || null,
availability,
clearance?.level || null,
clearance?.validUntil || null,
clearance?.issuedDate || null,
now,
req.user!.id,
id
)
// Optionally update employee number (NW-Kennung) with uniqueness check
if (employeeNumber && typeof employeeNumber === 'string') {
const existsNumber = db.prepare('SELECT id FROM employees WHERE employee_number = ? AND id <> ?').get(employeeNumber, id)
if (existsNumber) {
return res.status(409).json({ success: false, error: { message: 'Employee number already exists' } })
}
db.prepare('UPDATE employees SET employee_number = ?, updated_at = ?, updated_by = ? WHERE id = ?')
.run(employeeNumber, now, req.user!.id, id)
}
// Update skills
if (skills !== undefined) {
// Delete existing skills
db.prepare('DELETE FROM employee_skills WHERE employee_id = ?').run(id)
// Insert new skills
if (skills.length > 0) {
const insertSkill = db.prepare(`
INSERT INTO employee_skills (employee_id, skill_id, level, verified, verified_by, verified_date)
VALUES (?, ?, ?, ?, ?, ?)
`)
const checkSkillExists = db.prepare('SELECT id FROM skills WHERE id = ?')
for (const skill of skills) {
const exists = checkSkillExists.get(skill.id)
if (!exists) {
logger.warn(`Skill with ID ${skill.id} does not exist in skills table, skipping`)
continue
}
insertSkill.run(
id,
skill.id,
typeof skill.level === 'number' ? skill.level : (parseInt(skill.level) || null),
skill.verified ? 1 : 0,
skill.verifiedBy || null,
skill.verifiedDate || null
)
}
}
}
// Log update
logSecurityAudit('update', 'employees', id, req.user!.id, req, 'medium')
// Queue sync for updated employee
const updatedEmployee = {
id,
firstName,
lastName,
position,
department,
email,
phone,
mobile: mobile || null,
office: office || null,
availability,
clearance,
skills,
languages,
specializations,
updatedAt: now,
updatedBy: req.user!.id
}
syncService.queueSync('employees', 'update', updatedEmployee).catch(err => {
logger.error('Failed to queue sync:', err)
})
return res.json({
success: true,
message: 'Employee updated successfully'
})
} catch (error) {
logger.error('Error updating employee:', error)
throw error
}
})
try {
return transaction()
} catch (error: any) {
logger.error('Transaction failed:', error)
return res.status(500).json({
success: false,
error: {
message: 'Failed to update employee',
details: process.env.NODE_ENV === 'development' ? error.message : undefined
}
})
}
}
)
// Delete employee (admin only)
router.delete('/:id',
authenticate,
requirePermission('employees:delete'),
async (req: AuthRequest, res: Response, next: NextFunction) => {
const transaction = db.transaction(() => {
try {
const { id } = req.params
// Check if employee exists
const existing = db.prepare('SELECT id FROM employees WHERE id = ?').get(id)
if (!existing) {
return res.status(404).json({
success: false,
error: { message: 'Employee not found' }
})
}
// Delete employee and related data
db.prepare('DELETE FROM employee_skills WHERE employee_id = ?').run(id)
db.prepare('DELETE FROM language_skills WHERE employee_id = ?').run(id)
db.prepare('DELETE FROM specializations WHERE employee_id = ?').run(id)
db.prepare('DELETE FROM employees WHERE id = ?').run(id)
// Log deletion
logSecurityAudit('delete', 'employees', id, req.user!.id, req, 'high')
// Queue sync for deleted employee
syncService.queueSync('employees', 'delete', { id }).catch(err => {
logger.error('Failed to queue sync:', err)
})
return res.json({
success: true,
message: 'Employee deleted successfully'
})
} catch (error) {
logger.error('Error deleting employee:', error)
throw error
}
})
try {
return transaction()
} catch (error: any) {
logger.error('Transaction failed:', error)
return res.status(500).json({
success: false,
error: {
message: 'Failed to delete employee',
details: process.env.NODE_ENV === 'development' ? error.message : undefined
}
})
}
}
)
// Search employees by skills
router.post('/search', authenticate, requirePermission('employees:read'), async (req: AuthRequest, res, next) => {
try {
const { skills, category } = req.body
let query = `
SELECT DISTINCT e.id, e.first_name, e.last_name, e.employee_number,
e.position, e.department, e.availability,
e.email, e.phone, e.mobile
FROM employees e
JOIN employee_skills es ON e.id = es.employee_id
JOIN skills s ON es.skill_id = s.id
WHERE 1=1
`
const params: any[] = []
if (skills && skills.length > 0) {
const placeholders = skills.map(() => '?').join(',')
query += ` AND s.name IN (${placeholders})`
params.push(...skills)
}
if (category) {
query += ` AND s.category = ?`
params.push(category)
}
query += ` ORDER BY e.last_name, e.first_name`
const results = db.prepare(query).all(...params)
// Decrypt sensitive fields in results safely
const decryptedResults = results.map((emp: any) => {
const safeDecrypt = (v: any) => {
try { return v ? FieldEncryption.decrypt(v) : null } catch { return null }
}
return {
...emp,
email: safeDecrypt(emp.email),
phone: safeDecrypt(emp.phone),
mobile: safeDecrypt(emp.mobile),
}
})
res.json({ success: true, data: decryptedResults })
} catch (error) {
logger.error('Error searching employees:', error)
next(error)
}
})
export default router

338
backend/src/routes/network.ts Normale Datei
Datei anzeigen

@ -0,0 +1,338 @@
import { Router } from 'express'
import { v4 as uuidv4 } from 'uuid'
import { db } from '../config/database'
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
import crypto from 'crypto'
import { syncScheduler } from '../services/syncScheduler'
const router = Router()
// Initialize network nodes table if not exists
db.exec(`
CREATE TABLE IF NOT EXISTS network_nodes (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
location TEXT,
ip_address TEXT NOT NULL,
port INTEGER NOT NULL,
api_key TEXT NOT NULL UNIQUE,
type TEXT CHECK(type IN ('admin', 'local')) NOT NULL,
is_online INTEGER DEFAULT 0,
last_sync TEXT,
last_ping TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
created_by TEXT NOT NULL
)
`)
// Initialize sync settings table
db.exec(`
CREATE TABLE IF NOT EXISTS sync_settings (
id TEXT PRIMARY KEY,
auto_sync_interval TEXT,
conflict_resolution TEXT CHECK(conflict_resolution IN ('admin', 'newest', 'manual')),
sync_employees INTEGER DEFAULT 1,
sync_skills INTEGER DEFAULT 1,
sync_users INTEGER DEFAULT 1,
sync_settings INTEGER DEFAULT 0,
bandwidth_limit INTEGER,
updated_at TEXT NOT NULL,
updated_by TEXT NOT NULL
)
`)
// Get all network nodes
router.get('/nodes', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const nodes = db.prepare(`
SELECT * FROM network_nodes ORDER BY type DESC, name ASC
`).all()
res.json({
success: true,
data: nodes.map((node: any) => ({
id: node.id,
name: node.name,
location: node.location,
ipAddress: node.ip_address,
port: node.port,
apiKey: node.api_key,
type: node.type,
isOnline: Boolean(node.is_online),
lastSync: node.last_sync ? new Date(node.last_sync) : null,
lastPing: node.last_ping ? new Date(node.last_ping) : null,
createdAt: new Date(node.created_at),
updatedAt: new Date(node.updated_at)
}))
})
} catch (error) {
next(error)
}
})
// Create new network node
router.post('/nodes', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { name, location, ipAddress, port, type } = req.body
const nodeId = uuidv4()
const apiKey = generateApiKey()
const now = new Date().toISOString()
db.prepare(`
INSERT INTO network_nodes (
id, name, location, ip_address, port, api_key, type,
is_online, created_at, updated_at, created_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
nodeId, name, location || null, ipAddress, port, apiKey, type,
0, now, now, req.user!.id
)
res.status(201).json({
success: true,
data: {
id: nodeId,
apiKey: apiKey
},
message: 'Network node created successfully'
})
} catch (error) {
next(error)
}
})
// Update network node
router.put('/nodes/:id', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { id } = req.params
const { name, location, ipAddress, port } = req.body
const now = new Date().toISOString()
const result = db.prepare(`
UPDATE network_nodes
SET name = ?, location = ?, ip_address = ?, port = ?,
updated_at = ?, updated_by = ?
WHERE id = ?
`).run(
name, location || null, ipAddress, port,
now, req.user!.id, id
)
if (result.changes === 0) {
return res.status(404).json({
success: false,
error: { message: 'Network node not found' }
})
}
res.json({
success: true,
message: 'Network node updated successfully'
})
} catch (error) {
next(error)
}
})
// Delete network node
router.delete('/nodes/:id', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { id } = req.params
const result = db.prepare('DELETE FROM network_nodes WHERE id = ?').run(id)
if (result.changes === 0) {
return res.status(404).json({
success: false,
error: { message: 'Network node not found' }
})
}
res.json({
success: true,
message: 'Network node deleted successfully'
})
} catch (error) {
next(error)
}
})
// Ping network node
router.post('/nodes/:id/ping', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { id } = req.params
const node = db.prepare('SELECT * FROM network_nodes WHERE id = ?').get(id) as any
if (!node) {
return res.status(404).json({
success: false,
error: { message: 'Network node not found' }
})
}
// TODO: Implement actual ping logic
// For now, simulate ping
const isOnline = Math.random() > 0.2 // 80% chance of being online
const now = new Date().toISOString()
db.prepare(`
UPDATE network_nodes
SET is_online = ?, last_ping = ?
WHERE id = ?
`).run(isOnline ? 1 : 0, now, id)
res.json({
success: true,
data: {
isOnline,
lastPing: now,
responseTime: isOnline ? Math.floor(Math.random() * 100) + 10 : null
}
})
} catch (error) {
next(error)
}
})
// Get sync settings
router.get('/sync-settings', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
let settings = db.prepare('SELECT * FROM sync_settings WHERE id = ?').get('default') as any
if (!settings) {
// Create default settings
const now = new Date().toISOString()
db.prepare(`
INSERT INTO sync_settings (
id, auto_sync_interval, conflict_resolution,
sync_employees, sync_skills, sync_users, sync_settings,
bandwidth_limit, updated_at, updated_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
'default', 'disabled', 'admin',
1, 1, 1, 0,
null, now, 'system'
)
settings = db.prepare('SELECT * FROM sync_settings WHERE id = ?').get('default') as any
}
res.json({
success: true,
data: {
autoSyncInterval: settings.auto_sync_interval,
conflictResolution: settings.conflict_resolution,
syncEmployees: Boolean(settings.sync_employees),
syncSkills: Boolean(settings.sync_skills),
syncUsers: Boolean(settings.sync_users),
syncSettings: Boolean(settings.sync_settings),
bandwidthLimit: settings.bandwidth_limit
}
})
} catch (error) {
next(error)
}
})
// Update sync settings
router.put('/sync-settings', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const {
autoSyncInterval,
conflictResolution,
syncEmployees,
syncSkills,
syncUsers,
syncSettings,
bandwidthLimit
} = req.body
const now = new Date().toISOString()
db.prepare(`
UPDATE sync_settings
SET auto_sync_interval = ?, conflict_resolution = ?,
sync_employees = ?, sync_skills = ?, sync_users = ?, sync_settings = ?,
bandwidth_limit = ?, updated_at = ?, updated_by = ?
WHERE id = ?
`).run(
autoSyncInterval,
conflictResolution,
syncEmployees ? 1 : 0,
syncSkills ? 1 : 0,
syncUsers ? 1 : 0,
syncSettings ? 1 : 0,
bandwidthLimit || null,
now,
req.user!.id,
'default'
)
res.json({
success: true,
message: 'Sync settings updated successfully'
})
} catch (error) {
next(error)
}
})
// Trigger manual sync
router.post('/sync/trigger', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { nodeIds } = req.body
// TODO: Implement actual sync logic
// For now, simulate sync process
const now = new Date().toISOString()
if (nodeIds && nodeIds.length > 0) {
// Update specific nodes
const placeholders = nodeIds.map(() => '?').join(',')
db.prepare(`
UPDATE network_nodes
SET last_sync = ?
WHERE id IN (${placeholders})
`).run(now, ...nodeIds)
} else {
// Update all nodes
db.prepare(`
UPDATE network_nodes
SET last_sync = ?
`).run(now)
}
res.json({
success: true,
message: 'Sync triggered successfully',
data: {
syncedAt: now,
nodeCount: nodeIds?.length || 'all'
}
})
} catch (error) {
next(error)
}
})
// Get sync scheduler status
router.get('/sync-status', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const status = syncScheduler.getStatus()
res.json({
success: true,
data: status
})
} catch (error) {
next(error)
}
})
function generateApiKey(): string {
return crypto.randomBytes(32).toString('hex')
}
export default router

700
backend/src/routes/profiles.ts Normale Datei
Datei anzeigen

@ -0,0 +1,700 @@
import { Router, Response, NextFunction } from 'express'
import { v4 as uuidv4 } from 'uuid'
import { body, validationResult, query } from 'express-validator'
import { db } from '../config/database'
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
// Profile Interface direkt definiert, da shared module noch nicht aktualisiert
interface Profile {
id: string
name: string
department?: string
location?: string
role?: string
contacts?: {
email?: string
phone?: string
teams?: string
}
domains?: string[]
tools?: string[]
methods?: string[]
industryKnowledge?: string[]
regulatory?: string[]
languages?: { code: string; level: string }[]
projects?: { title: string; role?: string; summary?: string; links?: string[] }[]
networks?: string[]
digitalSkills?: string[]
socialSkills?: string[]
jobCategory?: string
jobTitle?: string
jobDesc?: string
consentPublicProfile: boolean
consentSearchable: boolean
updatedAt: string
updatedBy: string
reviewDueAt?: string
}
const router = Router()
// Hilfsfunktion f\u00fcr Volltextsuche
function buildSearchVector(profile: any): string {
const parts = [
profile.name,
profile.role,
profile.jobTitle,
profile.jobDesc,
...(profile.domains || []),
...(profile.tools || []),
...(profile.methods || []),
...(profile.industryKnowledge || []),
...(profile.digitalSkills || [])
].filter(Boolean)
return parts.join(' ').toLowerCase()
}
// GET /api/profiles - Suche mit Facetten
router.get('/',
authenticate,
[
query('query').optional().trim(),
query('dept').optional(),
query('loc').optional(),
query('jobCat').optional(),
query('tools').optional(),
query('methods').optional(),
query('lang').optional(),
query('page').optional().isInt({ min: 1 }).toInt(),
query('pageSize').optional().isInt({ min: 1, max: 100 }).toInt(),
query('sort').optional().isIn(['recency', 'relevance'])
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid query parameters', details: errors.array() }
})
}
const {
query: searchQuery,
dept,
loc,
jobCat,
tools,
methods,
lang,
page = 1,
pageSize = 20,
sort = 'relevance'
} = req.query
// Basis-Query mit Consent-Pr\u00fcfung
let sql = `
SELECT DISTINCT p.*
FROM profiles p
WHERE (p.consent_searchable = 1 OR p.updated_by = ?)
`
const params: any[] = [req.user!.id]
// Volltextsuche
if (searchQuery) {
sql += ` AND p.search_vector LIKE ?`
params.push(`%${searchQuery}%`)
}
// Facettenfilter
if (dept) {
sql += ` AND p.department = ?`
params.push(dept)
}
if (loc) {
sql += ` AND p.location = ?`
params.push(loc)
}
if (jobCat) {
sql += ` AND p.job_category = ?`
params.push(jobCat)
}
// Tool-Filter
if (tools) {
const toolList = tools.toString().split(',')
sql += ` AND EXISTS (
SELECT 1 FROM profile_tools pt
WHERE pt.profile_id = p.id
AND pt.tool IN (${toolList.map(() => '?').join(',')})
)`
params.push(...toolList)
}
// Methoden-Filter
if (methods) {
const methodList = methods.toString().split(',')
sql += ` AND EXISTS (
SELECT 1 FROM profile_methods pm
WHERE pm.profile_id = p.id
AND pm.method IN (${methodList.map(() => '?').join(',')})
)`
params.push(...methodList)
}
// Sprachen-Filter
if (lang) {
const [langCode, level] = lang.toString().split(':')
sql += ` AND EXISTS (
SELECT 1 FROM profile_languages pl
WHERE pl.profile_id = p.id
AND pl.code = ?
${level ? 'AND pl.level = ?' : ''}
)`
params.push(langCode)
if (level) params.push(level)
}
// Sortierung
if (sort === 'recency') {
sql += ` ORDER BY p.updated_at DESC`
} else {
sql += ` ORDER BY p.updated_at DESC` // TODO: Ranking nach Relevanz
}
// Pagination
const offset = ((page as number) - 1) * (pageSize as number)
sql += ` LIMIT ? OFFSET ?`
params.push(pageSize, offset)
const profiles = db.prepare(sql).all(...params)
// Lade zus\u00e4tzliche Daten f\u00fcr jedes Profil
const enrichedProfiles = profiles.map((profile: any) => {
// Lade Arrays
const domains = db.prepare('SELECT domain FROM profile_domains WHERE profile_id = ?').all(profile.id).map((r: any) => r.domain)
const tools = db.prepare('SELECT tool FROM profile_tools WHERE profile_id = ?').all(profile.id).map((r: any) => r.tool)
const methods = db.prepare('SELECT method FROM profile_methods WHERE profile_id = ?').all(profile.id).map((r: any) => r.method)
const industryKnowledge = db.prepare('SELECT knowledge FROM profile_industry_knowledge WHERE profile_id = ?').all(profile.id).map((r: any) => r.knowledge)
const regulatory = db.prepare('SELECT regulation FROM profile_regulatory WHERE profile_id = ?').all(profile.id).map((r: any) => r.regulation)
const networks = db.prepare('SELECT network FROM profile_networks WHERE profile_id = ?').all(profile.id).map((r: any) => r.network)
const digitalSkills = db.prepare('SELECT skill FROM profile_digital_skills WHERE profile_id = ?').all(profile.id).map((r: any) => r.skill)
const socialSkills = db.prepare('SELECT skill FROM profile_social_skills WHERE profile_id = ?').all(profile.id).map((r: any) => r.skill)
// Lade Sprachen
const languages = db.prepare('SELECT code, level FROM profile_languages WHERE profile_id = ?').all(profile.id)
// Lade Projekte
const projects = db.prepare('SELECT id, title, role, summary FROM profile_projects WHERE profile_id = ?').all(profile.id)
for (const project of projects as any[]) {
project.links = db.prepare('SELECT link FROM project_links WHERE project_id = ?').all(project.id).map((r: any) => r.link)
delete project.id
}
return {
...profile,
contacts: {
email: profile.email,
phone: profile.phone,
teams: profile.teams_link
},
domains,
tools,
methods,
industryKnowledge,
regulatory,
networks,
digitalSkills,
socialSkills,
languages,
projects,
consentPublicProfile: Boolean(profile.consent_public_profile),
consentSearchable: Boolean(profile.consent_searchable)
}
})
res.json({
success: true,
data: enrichedProfiles,
meta: {
page,
pageSize,
total: (db.prepare('SELECT COUNT(DISTINCT id) as count FROM profiles WHERE consent_searchable = 1 OR updated_by = ?').get(req.user!.id) as any)?.count || 0
}
})
} catch (error) {
next(error)
}
}
)
// GET /api/profiles/:id - Einzelnes Profil
router.get('/:id',
authenticate,
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
const profile = db.prepare(`
SELECT * FROM profiles WHERE id = ?
`).get(id) as any
if (!profile) {
return res.status(404).json({
success: false,
error: { message: 'Profile not found' }
})
}
// Pr\u00fcfe Zugriffsrechte
const isOwner = profile.updated_by === req.user!.id
const isAdmin = req.user!.role === 'admin'
if (!profile.consent_public_profile && !isOwner && !isAdmin) {
return res.status(403).json({
success: false,
error: { message: 'Access denied' }
})
}
// Lade zus\u00e4tzliche Daten
const domains = db.prepare('SELECT domain FROM profile_domains WHERE profile_id = ?').all(id).map((r: any) => r.domain)
const tools = db.prepare('SELECT tool FROM profile_tools WHERE profile_id = ?').all(id).map((r: any) => r.tool)
const methods = db.prepare('SELECT method FROM profile_methods WHERE profile_id = ?').all(id).map((r: any) => r.method)
const industryKnowledge = db.prepare('SELECT knowledge FROM profile_industry_knowledge WHERE profile_id = ?').all(id).map((r: any) => r.knowledge)
const regulatory = db.prepare('SELECT regulation FROM profile_regulatory WHERE profile_id = ?').all(id).map((r: any) => r.regulation)
const networks = db.prepare('SELECT network FROM profile_networks WHERE profile_id = ?').all(id).map((r: any) => r.network)
const digitalSkills = db.prepare('SELECT skill FROM profile_digital_skills WHERE profile_id = ?').all(id).map((r: any) => r.skill)
const socialSkills = db.prepare('SELECT skill FROM profile_social_skills WHERE profile_id = ?').all(id).map((r: any) => r.skill)
const languages = db.prepare('SELECT code, level FROM profile_languages WHERE profile_id = ?').all(id)
const projects = db.prepare('SELECT id, title, role, summary FROM profile_projects WHERE profile_id = ?').all(id)
for (const project of projects as any[]) {
project.links = db.prepare('SELECT link FROM project_links WHERE project_id = ?').all(project.id).map((r: any) => r.link)
delete project.id
}
const enrichedProfile = {
...profile,
contacts: {
email: profile.email,
phone: profile.phone,
teams: profile.teams_link
},
domains,
tools,
methods,
industryKnowledge,
regulatory,
networks,
digitalSkills,
socialSkills,
languages,
projects,
consentPublicProfile: Boolean(profile.consent_public_profile),
consentSearchable: Boolean(profile.consent_searchable)
}
res.json({ success: true, data: enrichedProfile })
} catch (error) {
next(error)
}
}
)
// POST /api/profiles - Neues Profil erstellen
router.post('/',
authenticate,
[
body('name').notEmpty().trim(),
body('consentPublicProfile').isBoolean(),
body('consentSearchable').isBoolean()
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const profileId = uuidv4()
const now = new Date().toISOString()
const reviewDueAt = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString()
const profileData = {
...req.body,
id: profileId,
created_at: now,
updated_at: now,
updated_by: req.user!.id,
review_due_at: reviewDueAt,
search_vector: buildSearchVector(req.body)
}
// Transaktion starten
const transaction = db.transaction(() => {
// Profil einf\u00fcgen
db.prepare(`
INSERT INTO profiles (
id, name, department, location, role, email, phone, teams_link,
job_category, job_title, job_desc,
consent_public_profile, consent_searchable,
created_at, updated_at, updated_by, review_due_at, search_vector
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
profileId,
profileData.name,
profileData.department || null,
profileData.location || null,
profileData.role || null,
profileData.contacts?.email || null,
profileData.contacts?.phone || null,
profileData.contacts?.teams || null,
profileData.jobCategory || null,
profileData.jobTitle || null,
profileData.jobDesc || null,
profileData.consentPublicProfile ? 1 : 0,
profileData.consentSearchable ? 1 : 0,
now,
now,
req.user!.id,
reviewDueAt,
profileData.search_vector
)
// Arrays einf\u00fcgen
const insertArray = (table: string, field: string, values: string[]) => {
if (values && values.length > 0) {
const stmt = db.prepare(`INSERT INTO ${table} (profile_id, ${field}) VALUES (?, ?)`)
for (const value of values) {
stmt.run(profileId, value)
}
}
}
insertArray('profile_domains', 'domain', profileData.domains)
insertArray('profile_tools', 'tool', profileData.tools)
insertArray('profile_methods', 'method', profileData.methods)
insertArray('profile_industry_knowledge', 'knowledge', profileData.industryKnowledge)
insertArray('profile_regulatory', 'regulation', profileData.regulatory)
insertArray('profile_networks', 'network', profileData.networks)
insertArray('profile_digital_skills', 'skill', profileData.digitalSkills)
insertArray('profile_social_skills', 'skill', profileData.socialSkills)
// Sprachen einf\u00fcgen
if (profileData.languages && profileData.languages.length > 0) {
const stmt = db.prepare(`INSERT INTO profile_languages (profile_id, code, level) VALUES (?, ?, ?)`)
for (const lang of profileData.languages) {
stmt.run(profileId, lang.code, lang.level)
}
}
// Projekte einf\u00fcgen
if (profileData.projects && profileData.projects.length > 0) {
for (const project of profileData.projects) {
const projectId = uuidv4()
db.prepare(`
INSERT INTO profile_projects (id, profile_id, title, role, summary, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`).run(projectId, profileId, project.title, project.role || null, project.summary || null, now)
if (project.links && project.links.length > 0) {
const linkStmt = db.prepare(`INSERT INTO project_links (project_id, link) VALUES (?, ?)`)
for (const link of project.links) {
linkStmt.run(projectId, link)
}
}
}
}
// Audit-Log
db.prepare(`
INSERT INTO audit_log (id, entity_type, entity_id, action, user_id, changes, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
uuidv4(),
'profile',
profileId,
'create',
req.user!.id,
JSON.stringify(profileData),
now
)
})
transaction()
res.status(201).json({
success: true,
data: { id: profileId },
message: 'Profile created successfully'
})
} catch (error) {
next(error)
}
}
)
// PUT /api/profiles/:id - Profil aktualisieren
router.put('/:id',
authenticate,
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
const now = new Date().toISOString()
const reviewDueAt = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString()
// Pr\u00fcfe ob Profil existiert und Berechtigungen
const existing = db.prepare('SELECT * FROM profiles WHERE id = ?').get(id) as any
if (!existing) {
return res.status(404).json({
success: false,
error: { message: 'Profile not found' }
})
}
const isOwner = existing.updated_by === req.user!.id
const isAdmin = req.user!.role === 'admin'
if (!isOwner && !isAdmin) {
return res.status(403).json({
success: false,
error: { message: 'Access denied' }
})
}
const profileData = {
...req.body,
updated_at: now,
updated_by: req.user!.id,
review_due_at: reviewDueAt,
search_vector: buildSearchVector(req.body)
}
// Transaktion f\u00fcr Update
const transaction = db.transaction(() => {
// Profil aktualisieren
db.prepare(`
UPDATE profiles SET
name = ?, department = ?, location = ?, role = ?,
email = ?, phone = ?, teams_link = ?,
job_category = ?, job_title = ?, job_desc = ?,
consent_public_profile = ?, consent_searchable = ?,
updated_at = ?, updated_by = ?, review_due_at = ?, search_vector = ?
WHERE id = ?
`).run(
profileData.name,
profileData.department || null,
profileData.location || null,
profileData.role || null,
profileData.contacts?.email || null,
profileData.contacts?.phone || null,
profileData.contacts?.teams || null,
profileData.jobCategory || null,
profileData.jobTitle || null,
profileData.jobDesc || null,
profileData.consentPublicProfile ? 1 : 0,
profileData.consentSearchable ? 1 : 0,
now,
req.user!.id,
reviewDueAt,
profileData.search_vector,
id
)
// Arrays aktualisieren (l\u00f6schen und neu einf\u00fcgen)
const updateArray = (table: string, field: string, values: string[]) => {
db.prepare(`DELETE FROM ${table} WHERE profile_id = ?`).run(id)
if (values && values.length > 0) {
const stmt = db.prepare(`INSERT INTO ${table} (profile_id, ${field}) VALUES (?, ?)`)
for (const value of values) {
stmt.run(id, value)
}
}
}
updateArray('profile_domains', 'domain', profileData.domains)
updateArray('profile_tools', 'tool', profileData.tools)
updateArray('profile_methods', 'method', profileData.methods)
updateArray('profile_industry_knowledge', 'knowledge', profileData.industryKnowledge)
updateArray('profile_regulatory', 'regulation', profileData.regulatory)
updateArray('profile_networks', 'network', profileData.networks)
updateArray('profile_digital_skills', 'skill', profileData.digitalSkills)
updateArray('profile_social_skills', 'skill', profileData.socialSkills)
// Sprachen aktualisieren
db.prepare('DELETE FROM profile_languages WHERE profile_id = ?').run(id)
if (profileData.languages && profileData.languages.length > 0) {
const stmt = db.prepare(`INSERT INTO profile_languages (profile_id, code, level) VALUES (?, ?, ?)`)
for (const lang of profileData.languages) {
stmt.run(id, lang.code, lang.level)
}
}
// Projekte aktualisieren
db.prepare('DELETE FROM profile_projects WHERE profile_id = ?').run(id)
if (profileData.projects && profileData.projects.length > 0) {
for (const project of profileData.projects) {
const projectId = uuidv4()
db.prepare(`
INSERT INTO profile_projects (id, profile_id, title, role, summary, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`).run(projectId, id, project.title, project.role || null, project.summary || null, now)
if (project.links && project.links.length > 0) {
const linkStmt = db.prepare(`INSERT INTO project_links (project_id, link) VALUES (?, ?)`)
for (const link of project.links) {
linkStmt.run(projectId, link)
}
}
}
}
// Audit-Log
db.prepare(`
INSERT INTO audit_log (id, entity_type, entity_id, action, user_id, changes, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
uuidv4(),
'profile',
id,
'update',
req.user!.id,
JSON.stringify({ before: existing, after: profileData }),
now
)
})
transaction()
res.json({
success: true,
message: 'Profile updated successfully'
})
} catch (error) {
next(error)
}
}
)
// GET /api/profiles/facets - Facettenwerte f\u00fcr Filter
router.get('/facets',
authenticate,
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const facets = {
departments: db.prepare('SELECT DISTINCT department FROM profiles WHERE department IS NOT NULL AND consent_searchable = 1 ORDER BY department').all().map((r: any) => r.department),
locations: db.prepare('SELECT DISTINCT location FROM profiles WHERE location IS NOT NULL AND consent_searchable = 1 ORDER BY location').all().map((r: any) => r.location),
jobCategories: db.prepare('SELECT DISTINCT job_category FROM profiles WHERE job_category IS NOT NULL AND consent_searchable = 1 ORDER BY job_category').all().map((r: any) => r.job_category),
tools: db.prepare('SELECT DISTINCT tool FROM profile_tools pt JOIN profiles p ON pt.profile_id = p.id WHERE p.consent_searchable = 1 ORDER BY tool').all().map((r: any) => r.tool),
methods: db.prepare('SELECT DISTINCT method FROM profile_methods pm JOIN profiles p ON pm.profile_id = p.id WHERE p.consent_searchable = 1 ORDER BY method').all().map((r: any) => r.method),
languages: db.prepare('SELECT DISTINCT code, level FROM profile_languages pl JOIN profiles p ON pl.profile_id = p.id WHERE p.consent_searchable = 1 ORDER BY code, level').all()
}
res.json({ success: true, data: facets })
} catch (error) {
next(error)
}
}
)
// GET /api/profiles/reminders/overdue - Veraltete Profile (Admin)
router.get('/reminders/overdue',
authenticate,
authorize('admin'),
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const now = new Date().toISOString()
const overdueProfiles = db.prepare(`
SELECT id, name, department, updated_at, review_due_at
FROM profiles
WHERE review_due_at < ?
ORDER BY review_due_at ASC
`).all(now)
res.json({ success: true, data: overdueProfiles })
} catch (error) {
next(error)
}
}
)
// POST /api/profiles/export - Export-Funktionalit\u00e4t
router.post('/export',
authenticate,
authorize('admin', 'superuser'),
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { filter, format = 'json' } = req.body
// Baue Query basierend auf Filter
let sql = `SELECT * FROM profiles WHERE consent_searchable = 1`
const params: any[] = []
if (filter?.department) {
sql += ` AND department = ?`
params.push(filter.department)
}
if (filter?.location) {
sql += ` AND location = ?`
params.push(filter.location)
}
const profiles = db.prepare(sql).all(...params)
if (format === 'csv') {
// CSV-Export
const csv = [
'Name,Department,Location,Role,Email,Phone,Job Category,Job Title',
...profiles.map((p: any) =>
`"${p.name}","${p.department || ''}","${p.location || ''}","${p.role || ''}","${p.email || ''}","${p.phone || ''}","${p.job_category || ''}","${p.job_title || ''}"`
)
].join('\n')
res.setHeader('Content-Type', 'text/csv')
res.setHeader('Content-Disposition', 'attachment; filename=profiles.csv')
res.send(csv)
} else {
// JSON-Export
res.json({ success: true, data: profiles })
}
} catch (error) {
next(error)
}
}
)
// POST /api/profiles/tags/suggest - Autocomplete f\u00fcr Tags
router.post('/tags/suggest',
authenticate,
[
body('category').notEmpty(),
body('query').notEmpty()
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { category, query } = req.body
const suggestions = db.prepare(`
SELECT value, description
FROM controlled_vocabulary
WHERE category = ? AND value LIKE ? AND is_active = 1
ORDER BY value
LIMIT 10
`).all(category, `${query}%`)
res.json({ success: true, data: suggestions })
} catch (error) {
next(error)
}
}
)
export default router

Datei anzeigen

@ -0,0 +1,87 @@
import { Router, Response, NextFunction } from 'express'
import { body, validationResult } from 'express-validator'
import { db } from '../config/secureDatabase'
import { authenticate, AuthRequest } from '../middleware/auth'
import { requirePermission } from '../middleware/roleAuth'
import { logger } from '../utils/logger'
const router = Router()
// Get system settings
router.get('/', authenticate, requirePermission('settings:read'), async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const settings = db.prepare('SELECT key, value, description FROM system_settings').all() as any[]
const settingsMap = settings.reduce((acc, setting) => {
acc[setting.key] = {
value: setting.value === 'true' || setting.value === 'false' ? setting.value === 'true' : setting.value,
description: setting.description
}
return acc
}, {})
res.json({ success: true, data: settingsMap })
} catch (error) {
logger.error('Error fetching settings:', error)
next(error)
}
})
// Update system setting
router.put('/:key',
authenticate,
requirePermission('settings:update'),
[
body('value').notEmpty()
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const { key } = req.params
const { value } = req.body
const now = new Date().toISOString()
// Convert boolean to string for storage
const stringValue = typeof value === 'boolean' ? value.toString() : value
// Check if setting exists
const existingSetting = db.prepare('SELECT key FROM system_settings WHERE key = ?').get(key)
if (existingSetting) {
// Update existing setting
db.prepare(`
UPDATE system_settings SET value = ?, updated_at = ?, updated_by = ?
WHERE key = ?
`).run(stringValue, now, req.user!.id, key)
logger.info(`System setting ${key} updated to ${stringValue} by user ${req.user!.username}`)
} else {
// Create new setting
db.prepare(`
INSERT INTO system_settings (key, value, updated_at, updated_by)
VALUES (?, ?, ?, ?)
`).run(key, stringValue, now, req.user!.id)
logger.info(`System setting ${key} created with value ${stringValue} by user ${req.user!.username}`)
}
res.json({
success: true,
message: 'Setting updated successfully',
data: { key, value: stringValue }
})
} catch (error) {
logger.error('Error updating setting:', error)
next(error)
}
}
)
export default router

508
backend/src/routes/skills.ts Normale Datei
Datei anzeigen

@ -0,0 +1,508 @@
import { Router } from 'express'
import { v4 as uuidv4 } from 'uuid'
import { db } from '../config/database'
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
import { DEFAULT_SKILLS } from '@skillmate/shared'
import { syncService } from '../services/syncService'
const router = Router()
// Get all skills
router.get('/', authenticate, async (req: AuthRequest, res, next) => {
try {
const skills = db.prepare(`
SELECT id, name, category, description, requires_certification, expires_after
FROM skills
ORDER BY category, name
`).all()
res.json({ success: true, data: skills })
} catch (error) {
next(error)
}
})
// Get skills hierarchy (categories -> subcategories -> skills)
router.get('/hierarchy', authenticate, async (req: AuthRequest, res, next) => {
try {
// Ensure vocabulary table exists (defensive)
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)
);
`)
let cats = [] as any[]
let subs = [] as any[]
try {
cats = db.prepare(`
SELECT value AS id, COALESCE(description, value) AS name
FROM controlled_vocabulary
WHERE category = 'skill_category'
ORDER BY id
`).all() as any[]
subs = db.prepare(`
SELECT value AS key, COALESCE(description, value) AS name
FROM controlled_vocabulary
WHERE category = 'skill_subcategory'
ORDER BY key
`).all() as any[]
} catch {}
const subByCat: Record<string, { id: string; name: string }[]> = {}
for (const s of subs) {
const [catId, subId] = String(s.key).split('.')
if (!catId || !subId) continue
if (!subByCat[catId]) subByCat[catId] = []
subByCat[catId].push({ id: subId, name: s.name })
}
const skills = db.prepare(`
SELECT id, name, category, description FROM skills
`).all() as any[]
const byKey: Record<string, { id: string; name: string; description?: string | null }[]> = {}
for (const s of skills) {
const key = s.category
if (!key) continue
if (!byKey[key]) byKey[key] = []
byKey[key].push({ id: s.id, name: s.name, description: s.description || null })
}
let hierarchy = cats.map(cat => ({
id: cat.id,
name: cat.name,
subcategories: (subByCat[cat.id] || []).map(sc => ({
id: sc.id,
name: sc.name,
skills: byKey[`${cat.id}.${sc.id}`] || []
}))
}))
// Fallback: if vocabulary is empty, derive from existing skills
if (hierarchy.length === 0) {
const seenCats = new Map<string, { id: string; name: string; subs: Map<string, { id: string; name: string; skills: any[] }> }>()
for (const s of skills) {
const [catId, subId] = String(s.category || '').split('.')
if (!catId || !subId) continue
if (!seenCats.has(catId)) seenCats.set(catId, { id: catId, name: catId, subs: new Map() })
const cat = seenCats.get(catId)!
if (!cat.subs.has(subId)) cat.subs.set(subId, { id: subId, name: subId, skills: [] })
cat.subs.get(subId)!.skills.push({ id: s.id, name: s.name, description: s.description || null })
}
hierarchy = Array.from(seenCats.values()).map(c => ({
id: c.id,
name: c.name,
subcategories: Array.from(c.subs.values())
}))
}
res.json({ success: true, data: hierarchy })
} catch (error) {
next(error)
}
})
// Initialize default skills (admin only)
router.post('/initialize',
authenticate,
authorize('admin'),
async (req: AuthRequest, res, next) => {
try {
const insertSkill = db.prepare(`
INSERT OR IGNORE INTO skills (id, name, category, description, requires_certification, expires_after)
VALUES (?, ?, ?, ?, ?, ?)
`)
let count = 0
for (const [category, skills] of Object.entries(DEFAULT_SKILLS)) {
for (const skillName of skills) {
const result = insertSkill.run(
uuidv4(),
skillName,
category,
null,
category === 'certificates' || category === 'weapons' ? 1 : 0,
category === 'certificates' ? 36 : null // 3 years for certificates
)
if (result.changes > 0) count++
}
}
res.json({
success: true,
message: `${count} skills initialized successfully`
})
} catch (error) {
next(error)
}
}
)
// Create custom skill (admin/poweruser only)
router.post('/',
authenticate,
authorize('admin', 'superuser'),
async (req: AuthRequest, res, next) => {
try {
const { id, name, category, description, requiresCertification, expiresAfter } = req.body
// Optional custom id to keep stable
let skillId = id && typeof id === 'string' && id.length > 0 ? id : uuidv4()
const exists = db.prepare('SELECT id FROM skills WHERE id = ?').get(skillId)
if (exists) {
return res.status(409).json({ success: false, error: { message: 'Skill ID already exists' } })
}
// Validate category refers to existing subcategory
if (!category || String(category).indexOf('.') === -1) {
return res.status(400).json({ success: false, error: { message: 'Category must be catId.subId' } })
}
const subExists = db
.prepare('SELECT 1 FROM controlled_vocabulary WHERE category = ? AND value = ?')
.get('skill_subcategory', category)
if (!subExists) {
return res.status(400).json({ success: false, error: { message: 'Unknown subcategory' } })
}
db.prepare(`
INSERT INTO skills (id, name, category, description, requires_certification, expires_after)
VALUES (?, ?, ?, ?, ?, ?)
`).run(
skillId,
name,
category,
description || null,
requiresCertification ? 1 : 0,
expiresAfter || null
)
// Queue sync for new skill
const newSkill = {
id: skillId,
name,
category,
description: description || null,
requiresCertification: requiresCertification || false,
expiresAfter: expiresAfter || null
}
await syncService.queueSync('skills', 'create', newSkill)
res.status(201).json({
success: true,
data: { id: skillId },
message: 'Skill created successfully'
})
} catch (error) {
next(error)
}
}
)
// Update skill (admin only)
router.put('/:id',
authenticate,
authorize('admin'),
async (req: AuthRequest, res, next) => {
try {
const { id } = req.params
const { name, category, description } = req.body
// Check if skill exists
const existing = db.prepare('SELECT id FROM skills WHERE id = ?').get(id)
if (!existing) {
return res.status(404).json({
success: false,
error: { message: 'Skill not found' }
})
}
// Validate new category if provided
if (category) {
if (String(category).indexOf('.') === -1) {
return res.status(400).json({ success: false, error: { message: 'Category must be catId.subId' } })
}
const subExists = db
.prepare('SELECT 1 FROM controlled_vocabulary WHERE category = ? AND value = ?')
.get('skill_subcategory', category)
if (!subExists) {
return res.status(400).json({ success: false, error: { message: 'Unknown subcategory' } })
}
}
// Update skill (partial)
db.prepare(`
UPDATE skills SET
name = COALESCE(?, name),
category = COALESCE(?, category),
description = COALESCE(?, description)
WHERE id = ?
`).run(name || null, category || null, (description ?? null), id)
// Queue sync for updated skill
const updatedSkill = {
id,
name,
category,
description: description || null
}
await syncService.queueSync('skills', 'update', updatedSkill)
res.json({
success: true,
message: 'Skill updated successfully'
})
} catch (error) {
next(error)
}
}
)
// Delete skill (admin only)
router.delete('/:id',
authenticate,
authorize('admin'),
async (req: AuthRequest, res, next) => {
try {
const { id } = req.params
const existing = db.prepare('SELECT id FROM skills WHERE id = ?').get(id)
if (!existing) {
return res.status(404).json({ success: false, error: { message: 'Skill not found' } })
}
db.prepare('DELETE FROM employee_skills WHERE skill_id = ?').run(id)
db.prepare('DELETE FROM skills WHERE id = ?').run(id)
await syncService.queueSync('skills', 'delete', { id })
res.json({ success: true, message: 'Skill deleted successfully' })
} catch (error) { next(error) }
}
)
// Category management
router.post('/categories', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { id, name } = req.body
if (!id || !name) return res.status(400).json({ success: false, error: { message: 'id and name required' } })
const now = new Date().toISOString()
const exists = db.prepare('SELECT id FROM controlled_vocabulary WHERE category = ? AND value = ?').get('skill_category', id)
if (exists) return res.status(409).json({ success: false, error: { message: 'Category already exists' } })
db.prepare('INSERT INTO controlled_vocabulary (id, category, value, description, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?)')
.run(uuidv4(), 'skill_category', id, name, 1, now)
res.status(201).json({ success: true, message: 'Category created' })
} catch (error) { next(error) }
})
router.put('/categories/:catId', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { catId } = req.params
const { name } = req.body
if (!name) return res.status(400).json({ success: false, error: { message: 'name required' } })
const updated = db.prepare('UPDATE controlled_vocabulary SET description = ? WHERE category = ? AND value = ?')
.run(name, 'skill_category', catId)
if (updated.changes === 0) return res.status(404).json({ success: false, error: { message: 'Category not found' } })
res.json({ success: true, message: 'Category updated' })
} catch (error) { next(error) }
})
router.delete('/categories/:catId', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { catId } = req.params
const tx = db.transaction(() => {
const subs = db.prepare('SELECT value FROM controlled_vocabulary WHERE category = ? AND value LIKE ?').all('skill_subcategory', `${catId}.%`) as any[]
for (const s of subs) {
db.prepare('DELETE FROM employee_skills WHERE skill_id IN (SELECT id FROM skills WHERE category = ?)').run(s.value)
db.prepare('DELETE FROM skills WHERE category = ?').run(s.value)
}
db.prepare('DELETE FROM controlled_vocabulary WHERE category = ? AND value LIKE ?').run('skill_subcategory', `${catId}.%`)
const del = db.prepare('DELETE FROM controlled_vocabulary WHERE category = ? AND value = ?').run('skill_category', catId)
return del.changes
})
const changes = tx()
if (!changes) return res.status(404).json({ success: false, error: { message: 'Category not found' } })
res.json({ success: true, message: 'Category deleted' })
} catch (error) { next(error) }
})
// Subcategory management
router.post('/categories/:catId/subcategories', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { catId } = req.params
const { id, name } = req.body
if (!id || !name) return res.status(400).json({ success: false, error: { message: 'id and name required' } })
const cat = db.prepare('SELECT 1 FROM controlled_vocabulary WHERE category = ? AND value = ?').get('skill_category', catId)
if (!cat) return res.status(400).json({ success: false, error: { message: 'Category does not exist' } })
const key = `${catId}.${id}`
const exists = db.prepare('SELECT id FROM controlled_vocabulary WHERE category = ? AND value = ?').get('skill_subcategory', key)
if (exists) return res.status(409).json({ success: false, error: { message: 'Subcategory already exists' } })
const now = new Date().toISOString()
db.prepare('INSERT INTO controlled_vocabulary (id, category, value, description, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?)')
.run(uuidv4(), 'skill_subcategory', key, name, 1, now)
res.status(201).json({ success: true, message: 'Subcategory created' })
} catch (error) { next(error) }
})
router.put('/categories/:catId/subcategories/:subId', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { catId, subId } = req.params
const { name } = req.body
if (!name) return res.status(400).json({ success: false, error: { message: 'name required' } })
const key = `${catId}.${subId}`
const updated = db.prepare('UPDATE controlled_vocabulary SET description = ? WHERE category = ? AND value = ?')
.run(name, 'skill_subcategory', key)
if (updated.changes === 0) return res.status(404).json({ success: false, error: { message: 'Subcategory not found' } })
res.json({ success: true, message: 'Subcategory updated' })
} catch (error) { next(error) }
})
router.delete('/categories/:catId/subcategories/:subId', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { catId, subId } = req.params
const key = `${catId}.${subId}`
const tx = db.transaction(() => {
db.prepare('DELETE FROM employee_skills WHERE skill_id IN (SELECT id FROM skills WHERE category = ?)').run(key)
db.prepare('DELETE FROM skills WHERE category = ?').run(key)
const del = db.prepare('DELETE FROM controlled_vocabulary WHERE category = ? AND value = ?').run('skill_subcategory', key)
return del.changes
})
const changes = tx()
if (!changes) return res.status(404).json({ success: false, error: { message: 'Subcategory not found' } })
res.json({ success: true, message: 'Subcategory deleted' })
} catch (error) { next(error) }
})
// Delete skill (admin only)
router.delete('/:id',
authenticate,
authorize('admin'),
async (req: AuthRequest, res, next) => {
try {
const { id } = req.params
// Check if skill exists
const existing = db.prepare('SELECT id FROM skills WHERE id = ?').get(id)
if (!existing) {
return res.status(404).json({
success: false,
error: { message: 'Skill not found' }
})
}
// Delete skill and related data
db.prepare('DELETE FROM employee_skills WHERE skill_id = ?').run(id)
db.prepare('DELETE FROM skills WHERE id = ?').run(id)
// Queue sync for deleted skill
await syncService.queueSync('skills', 'delete', { id })
res.json({
success: true,
message: 'Skill deleted successfully'
})
} catch (error) {
next(error)
}
}
)
// Category management
router.post('/categories', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { id, name } = req.body
if (!id || !name) return res.status(400).json({ success: false, error: { message: 'id and name required' } })
const now = new Date().toISOString()
const exists = db.prepare('SELECT id FROM controlled_vocabulary WHERE category = ? AND value = ?')
.get('skill_category', id)
if (exists) return res.status(409).json({ success: false, error: { message: 'Category already exists' } })
db.prepare('INSERT INTO controlled_vocabulary (id, category, value, description, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?)')
.run(uuidv4(), 'skill_category', id, name, 1, now)
res.status(201).json({ success: true, message: 'Category created' })
} catch (error) { next(error) }
})
router.put('/categories/:catId', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { catId } = req.params
const { name } = req.body
if (!name) return res.status(400).json({ success: false, error: { message: 'name required' } })
const updated = db.prepare('UPDATE controlled_vocabulary SET description = ? WHERE category = ? AND value = ?')
.run(name, 'skill_category', catId)
if (updated.changes === 0) return res.status(404).json({ success: false, error: { message: 'Category not found' } })
res.json({ success: true, message: 'Category updated' })
} catch (error) { next(error) }
})
router.delete('/categories/:catId', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { catId } = req.params
const tx = db.transaction(() => {
// Delete skills under all subcategories
const subs = db.prepare('SELECT value FROM controlled_vocabulary WHERE category = ? AND value LIKE ?').all('skill_subcategory', `${catId}.%`) as any[]
for (const s of subs) {
db.prepare('DELETE FROM employee_skills WHERE skill_id IN (SELECT id FROM skills WHERE category = ?)').run(s.value)
db.prepare('DELETE FROM skills WHERE category = ?').run(s.value)
}
db.prepare('DELETE FROM controlled_vocabulary WHERE category = ? AND value LIKE ?').run('skill_subcategory', `${catId}.%`)
const del = db.prepare('DELETE FROM controlled_vocabulary WHERE category = ? AND value = ?').run('skill_category', catId)
return del.changes
})
const changes = tx()
if (!changes) return res.status(404).json({ success: false, error: { message: 'Category not found' } })
res.json({ success: true, message: 'Category deleted' })
} catch (error) { next(error) }
})
// Subcategory management
router.post('/categories/:catId/subcategories', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { catId } = req.params
const { id, name } = req.body
if (!id || !name) return res.status(400).json({ success: false, error: { message: 'id and name required' } })
const cat = db.prepare('SELECT 1 FROM controlled_vocabulary WHERE category = ? AND value = ?').get('skill_category', catId)
if (!cat) return res.status(400).json({ success: false, error: { message: 'Category does not exist' } })
const key = `${catId}.${id}`
const exists = db.prepare('SELECT id FROM controlled_vocabulary WHERE category = ? AND value = ?')
.get('skill_subcategory', key)
if (exists) return res.status(409).json({ success: false, error: { message: 'Subcategory already exists' } })
const now = new Date().toISOString()
db.prepare('INSERT INTO controlled_vocabulary (id, category, value, description, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?)')
.run(uuidv4(), 'skill_subcategory', key, name, 1, now)
res.status(201).json({ success: true, message: 'Subcategory created' })
} catch (error) { next(error) }
})
router.put('/categories/:catId/subcategories/:subId', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { catId, subId } = req.params
const { name } = req.body
if (!name) return res.status(400).json({ success: false, error: { message: 'name required' } })
const key = `${catId}.${subId}`
const updated = db.prepare('UPDATE controlled_vocabulary SET description = ? WHERE category = ? AND value = ?')
.run(name, 'skill_subcategory', key)
if (updated.changes === 0) return res.status(404).json({ success: false, error: { message: 'Subcategory not found' } })
res.json({ success: true, message: 'Subcategory updated' })
} catch (error) { next(error) }
})
router.delete('/categories/:catId/subcategories/:subId', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { catId, subId } = req.params
const key = `${catId}.${subId}`
const tx = db.transaction(() => {
db.prepare('DELETE FROM employee_skills WHERE skill_id IN (SELECT id FROM skills WHERE category = ?)').run(key)
db.prepare('DELETE FROM skills WHERE category = ?').run(key)
const del = db.prepare('DELETE FROM controlled_vocabulary WHERE category = ? AND value = ?').run('skill_subcategory', key)
return del.changes
})
const changes = tx()
if (!changes) return res.status(404).json({ success: false, error: { message: 'Subcategory not found' } })
res.json({ success: true, message: 'Subcategory deleted' })
} catch (error) { next(error) }
})
export default router

123
backend/src/routes/sync.ts Normale Datei
Datei anzeigen

@ -0,0 +1,123 @@
import { Router } from 'express'
import { authenticate, authorize } from '../middleware/auth'
import { syncService } from '../services/syncService'
import { logger } from '../utils/logger'
import type { AuthRequest } from '../middleware/auth'
const router = Router()
// Receive sync data from another node
router.post('/receive', authenticate, async (req: AuthRequest, res, next) => {
try {
const nodeId = req.headers['x-node-id'] as string
if (!nodeId) {
return res.status(400).json({
error: { message: 'Missing node ID in headers' }
})
}
const result = await syncService.receiveSync(req.body)
res.json(result)
} catch (error) {
next(error)
}
})
// Get sync status
router.get('/status', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const status = syncService.getSyncStatus()
res.json({
success: true,
data: status
})
} catch (error) {
next(error)
}
})
// Trigger manual sync
router.post('/trigger', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
// Run sync in background
syncService.triggerSync().catch(error => {
logger.error('Background sync failed:', error)
})
res.json({
success: true,
message: 'Sync triggered successfully'
})
} catch (error) {
next(error)
}
})
// Sync with specific node
router.post('/sync/:nodeId', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { nodeId } = req.params
const result = await syncService.syncWithNode(nodeId)
res.json({
success: true,
data: result
})
} catch (error) {
next(error)
}
})
// Get sync conflicts
router.get('/conflicts', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { db } = await import('../config/database')
const conflicts = db.prepare(`
SELECT * FROM sync_conflicts
WHERE resolution_status = 'pending'
ORDER BY created_at DESC
`).all()
res.json({
success: true,
data: conflicts
})
} catch (error) {
next(error)
}
})
// Resolve sync conflict
router.post('/conflicts/:conflictId/resolve', authenticate, authorize('admin'), async (req: AuthRequest, res, next) => {
try {
const { conflictId } = req.params
const { resolution, data } = req.body
const { db } = await import('../config/database')
// Update conflict status
db.prepare(`
UPDATE sync_conflicts
SET resolution_status = 'resolved',
resolved_by = ?,
resolved_at = ?
WHERE id = ?
`).run(req.user!.id, new Date().toISOString(), conflictId)
// Apply resolution if needed
if (resolution === 'apply' && data) {
await syncService.receiveSync(data)
}
res.json({
success: true,
message: 'Conflict resolved successfully'
})
} catch (error) {
next(error)
}
})
export default router

151
backend/src/routes/upload.ts Normale Datei
Datei anzeigen

@ -0,0 +1,151 @@
import { Router } from 'express'
import multer from 'multer'
import path from 'path'
import fs from 'fs'
import { v4 as uuidv4 } from 'uuid'
import { authenticate, AuthRequest } from '../middleware/auth'
import { db } from '../config/database'
const router = Router()
// Ensure upload directory exists
const uploadDir = path.join(__dirname, '../../uploads/photos')
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })
}
// Configure multer for photo uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir)
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname)
const filename = `${uuidv4()}${ext}`
cb(null, filename)
}
})
const upload = multer({
storage,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB limit
},
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif|webp/
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase())
const mimetype = allowedTypes.test(file.mimetype)
if (mimetype && extname) {
return cb(null, true)
} else {
cb(new Error('Only image files are allowed'))
}
}
})
// Upload employee photo
// Only the user themself may change their own photo (no admin/superuser override)
router.post('/employee-photo/:employeeId',
authenticate,
(req: AuthRequest, res, next) => {
if (!req.user || req.user.employeeId !== req.params.employeeId) {
return res.status(403).json({ success: false, error: { message: 'Only the profile owner may change the photo' } })
}
next()
},
upload.single('photo'),
async (req: AuthRequest, res, next) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: { message: 'No file uploaded' }
})
}
const { employeeId } = req.params
// Check if employee exists
const employee = db.prepare('SELECT id, photo FROM employees WHERE id = ?').get(employeeId) as any
if (!employee) {
// Delete uploaded file
fs.unlinkSync(req.file.path)
return res.status(404).json({
success: false,
error: { message: 'Employee not found' }
})
}
// Delete old photo if exists
if (employee.photo) {
const oldPhotoPath = path.join(uploadDir, path.basename(employee.photo))
if (fs.existsSync(oldPhotoPath)) {
fs.unlinkSync(oldPhotoPath)
}
}
// Update employee photo URL
const photoUrl = `/uploads/photos/${req.file.filename}`
db.prepare('UPDATE employees SET photo = ?, updated_at = ?, updated_by = ? WHERE id = ?')
.run(photoUrl, new Date().toISOString(), req.user!.id, employeeId)
res.json({
success: true,
data: { photoUrl },
message: 'Photo uploaded successfully'
})
} catch (error) {
// Clean up uploaded file on error
if (req.file && fs.existsSync(req.file.path)) {
fs.unlinkSync(req.file.path)
}
next(error)
}
}
)
// Delete employee photo
router.delete('/employee-photo/:employeeId',
authenticate,
(req: AuthRequest, res, next) => {
if (!req.user || req.user.employeeId !== req.params.employeeId) {
return res.status(403).json({ success: false, error: { message: 'Only the profile owner may delete the photo' } })
}
next()
},
async (req: AuthRequest, res, next) => {
try {
const { employeeId } = req.params
// Get employee photo
const employee = db.prepare('SELECT id, photo FROM employees WHERE id = ?').get(employeeId) as any
if (!employee) {
return res.status(404).json({
success: false,
error: { message: 'Employee not found' }
})
}
if (employee.photo) {
const photoPath = path.join(uploadDir, path.basename(employee.photo))
if (fs.existsSync(photoPath)) {
fs.unlinkSync(photoPath)
}
}
// Clear photo from database
db.prepare('UPDATE employees SET photo = NULL, updated_at = ?, updated_by = ? WHERE id = ?')
.run(new Date().toISOString(), req.user!.id, employeeId)
res.json({
success: true,
message: 'Photo deleted successfully'
})
} catch (error) {
next(error)
}
}
)
export default router

311
backend/src/routes/users.ts Normale Datei
Datei anzeigen

@ -0,0 +1,311 @@
import { Router, Response, NextFunction } from 'express'
import { v4 as uuidv4 } from 'uuid'
import { body, validationResult } from 'express-validator'
import bcrypt from 'bcryptjs'
import { db } from '../config/database'
import { authenticate, AuthRequest } from '../middleware/auth'
import { requirePermission, requireAdminPanel } from '../middleware/roleAuth'
import { User, UserRole } from '@skillmate/shared'
const router = Router()
// Get all users (admin only)
router.get('/', authenticate, requirePermission('users:read'), async (req: AuthRequest, res, next) => {
try {
const users = db.prepare(`
SELECT id, username, email, role, employee_id, last_login, is_active, created_at, updated_at
FROM users
ORDER BY created_at DESC
`).all()
const formattedUsers: User[] = users.map((user: any) => ({
id: user.id,
username: user.username,
email: user.email,
role: user.role as UserRole,
employeeId: user.employee_id || undefined,
lastLogin: user.last_login ? new Date(user.last_login) : undefined,
isActive: Boolean(user.is_active),
createdAt: new Date(user.created_at),
updatedAt: new Date(user.updated_at)
}))
res.json({
success: true,
data: formattedUsers
})
} catch (error) {
next(error)
}
})
// Get user by ID
router.get('/:id', authenticate, requirePermission('users:read'), async (req: AuthRequest, res, next) => {
try {
const { id } = req.params
const user = db.prepare(`
SELECT id, username, email, role, employee_id, last_login, is_active, created_at, updated_at
FROM users
WHERE id = ?
`).get(id)
if (!user) {
return res.status(404).json({ error: 'User not found' })
}
const userRecord = user as any
const formattedUser: User = {
id: userRecord.id,
username: userRecord.username,
email: userRecord.email,
role: userRecord.role as UserRole,
employeeId: userRecord.employee_id || undefined,
lastLogin: userRecord.last_login ? new Date(userRecord.last_login) : undefined,
isActive: Boolean(userRecord.is_active),
createdAt: new Date(userRecord.created_at),
updatedAt: new Date(userRecord.updated_at)
}
res.json({
success: true,
data: formattedUser
})
} catch (error) {
next(error)
}
})
// Create new user (admin only)
router.post('/',
authenticate,
requirePermission('users:create'),
[
body('username').notEmpty().trim().isLength({ min: 3 }),
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 }),
body('role').isIn(['admin', 'superuser', 'user']),
body('employeeId').optional().isString()
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
error: 'Validation failed',
details: errors.array()
})
}
const { username, email, password, role, employeeId } = req.body
// Check if username already exists
const existingUser = db.prepare('SELECT id FROM users WHERE username = ?').get(username)
if (existingUser) {
return res.status(409).json({ error: 'Username already exists' })
}
// Check if email already exists
const existingEmail = db.prepare('SELECT id FROM users WHERE email = ?').get(email)
if (existingEmail) {
return res.status(409).json({ error: 'Email already exists' })
}
// Hash password
const hashedPassword = bcrypt.hashSync(password, 10)
const now = new Date().toISOString()
const userId = uuidv4()
// Enforce role policy: only admins can set role other than 'user'
const assignedRole = (req.user?.role === 'admin') ? role : 'user'
// Insert user
db.prepare(`
INSERT INTO users (id, username, email, password, role, employee_id, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
userId,
username,
email,
hashedPassword,
assignedRole,
employeeId || null,
1,
now,
now
)
const newUser: User = {
id: userId,
username,
email,
role: assignedRole as UserRole,
employeeId: employeeId || undefined,
isActive: true,
createdAt: new Date(now),
updatedAt: new Date(now)
}
res.status(201).json({
success: true,
data: newUser
})
} catch (error) {
next(error)
}
}
)
// Update user (admin only)
router.put('/:id',
authenticate,
requirePermission('users:update'),
[
body('username').optional().trim().isLength({ min: 3 }),
body('email').optional().isEmail().normalizeEmail(),
body('password').optional().isLength({ min: 6 }),
body('role').optional().isIn(['admin', 'superuser', 'user']),
body('employeeId').optional().isString(),
body('isActive').optional().isBoolean()
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
error: 'Validation failed',
details: errors.array()
})
}
const { id } = req.params
const { username, email, password, role, employeeId, isActive } = req.body
// Check if user exists
const existingUser = db.prepare('SELECT * FROM users WHERE id = ?').get(id)
if (!existingUser) {
return res.status(404).json({ error: 'User not found' })
}
// Prepare update fields
const updates = []
const values = []
if (username) {
// Check if new username is already taken (by another user)
const usernameCheck = db.prepare('SELECT id FROM users WHERE username = ? AND id != ?').get(username, id)
if (usernameCheck) {
return res.status(409).json({ error: 'Username already exists' })
}
updates.push('username = ?')
values.push(username)
}
if (email) {
// Check if new email is already taken (by another user)
const emailCheck = db.prepare('SELECT id FROM users WHERE email = ? AND id != ?').get(email, id)
if (emailCheck) {
return res.status(409).json({ error: 'Email already exists' })
}
updates.push('email = ?')
values.push(email)
}
if (password) {
const hashedPassword = bcrypt.hashSync(password, 10)
updates.push('password = ?')
values.push(hashedPassword)
}
if (role) {
updates.push('role = ?')
values.push(role)
}
if (employeeId !== undefined) {
updates.push('employee_id = ?')
values.push(employeeId || null)
}
if (isActive !== undefined) {
updates.push('is_active = ?')
values.push(isActive ? 1 : 0)
}
if (updates.length === 0) {
return res.status(400).json({ error: 'No fields to update' })
}
updates.push('updated_at = ?')
values.push(new Date().toISOString())
values.push(id)
db.prepare(`
UPDATE users
SET ${updates.join(', ')}
WHERE id = ?
`).run(...values)
// Return updated user
const updatedUser = db.prepare(`
SELECT id, username, email, role, employee_id, last_login, is_active, created_at, updated_at
FROM users
WHERE id = ?
`).get(id)
const updatedRecord = updatedUser as any
const formattedUser: User = {
id: updatedRecord.id,
username: updatedRecord.username,
email: updatedRecord.email,
role: updatedRecord.role as UserRole,
employeeId: updatedRecord.employee_id || undefined,
lastLogin: updatedRecord.last_login ? new Date(updatedRecord.last_login) : undefined,
isActive: Boolean(updatedRecord.is_active),
createdAt: new Date(updatedRecord.created_at),
updatedAt: new Date(updatedRecord.updated_at)
}
res.json({
success: true,
data: formattedUser
})
} catch (error) {
next(error)
}
}
)
// Delete user (admin only)
router.delete('/:id',
authenticate,
requirePermission('users:delete'),
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
// Prevent deleting self
if (req.user?.id === id) {
return res.status(400).json({ error: 'Cannot delete your own account' })
}
// Check if user exists
const existingUser = db.prepare('SELECT * FROM users WHERE id = ?').get(id)
if (!existingUser) {
return res.status(404).json({ error: 'User not found' })
}
// Delete user
db.prepare('DELETE FROM users WHERE id = ?').run(id)
res.json({
success: true,
message: 'User deleted successfully'
})
} catch (error) {
next(error)
}
}
)
export default router

Datei anzeigen

@ -0,0 +1,510 @@
import { Router, Response, NextFunction } from 'express'
import { body, validationResult } from 'express-validator'
import bcrypt from 'bcrypt'
import { v4 as uuidv4 } from 'uuid'
import { db, encryptedDb } from '../config/secureDatabase'
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
import { requirePermission } from '../middleware/roleAuth'
import { User, UserRole } from '@skillmate/shared'
import { FieldEncryption } from '../services/encryption'
import { emailService } from '../services/emailService'
import { logger } from '../utils/logger'
const router = Router()
// Get all users (admin only)
router.get('/', authenticate, requirePermission('users:read'), async (req: AuthRequest, res, next) => {
try {
const users = db.prepare(`
SELECT id, username, email, role, employee_id, last_login, is_active, created_at, updated_at
FROM users
ORDER BY username
`).all() as any[]
// Decrypt email addresses (handle decryption failures)
const decryptedUsers = users.map(user => {
let decryptedEmail = user.email
// Check if email looks encrypted (encrypted strings are typically longer and contain special characters)
if (user.email && user.email.includes('U2FsdGVkX1')) {
try {
decryptedEmail = FieldEncryption.decrypt(user.email) || user.email
} catch (error) {
logger.warn(`Failed to decrypt email for user ${user.username}, using raw value`)
// For compatibility with old unencrypted data or different encryption keys
decryptedEmail = user.email
}
}
return {
...user,
email: decryptedEmail,
isActive: Boolean(user.is_active),
lastLogin: user.last_login ? new Date(user.last_login) : null,
createdAt: new Date(user.created_at),
updatedAt: new Date(user.updated_at),
employeeId: user.employee_id
}
})
res.json({ success: true, data: decryptedUsers })
} catch (error) {
logger.error('Error fetching users:', error)
next(error)
}
})
// Update user role (admin only)
router.put('/:id/role',
authenticate,
requirePermission('users:update'),
[
body('role').isIn(['admin', 'superuser', 'user'])
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const { id } = req.params
const { role } = req.body
// Check if user exists
const existingUser = db.prepare('SELECT id, username FROM users WHERE id = ?').get(id) as any
if (!existingUser) {
return res.status(404).json({
success: false,
error: { message: 'User not found' }
})
}
// Prevent changing admin user role
if (existingUser.username === 'admin' && role !== 'admin') {
return res.status(403).json({
success: false,
error: { message: 'Cannot change admin user role' }
})
}
// Update role
db.prepare(`
UPDATE users SET role = ?, updated_at = ?
WHERE id = ?
`).run(role, new Date().toISOString(), id)
logger.info(`User role updated: ${existingUser.username} -> ${role}`)
res.json({ success: true, message: 'Role updated successfully' })
} catch (error) {
logger.error('Error updating user role:', error)
next(error)
}
}
)
// Bulk create users from employees
router.post('/bulk-create-from-employees',
authenticate,
requirePermission('users:create'),
[
body('employeeIds').isArray({ min: 1 }),
body('role').isIn(['admin', 'superuser', 'user'])
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const { employeeIds, role } = req.body as { employeeIds: string[]; role: UserRole }
const results: any[] = []
for (const employeeId of employeeIds) {
try {
const emp = encryptedDb.getEmployee(employeeId) as any
if (!emp) {
results.push({ employeeId, status: 'skipped', reason: 'employee_not_found' })
continue
}
const existsByEmployee = db.prepare('SELECT id FROM users WHERE employee_id = ?').get(employeeId)
if (existsByEmployee) {
results.push({ employeeId, status: 'skipped', reason: 'user_exists' })
continue
}
const baseUsername = (emp.email ? String(emp.email).split('@')[0] : `${emp.first_name}.${emp.last_name}`)
.toLowerCase().replace(/\s+/g, '')
let finalUsername = baseUsername
let i = 1
while (db.prepare('SELECT id FROM users WHERE username = ?').get(finalUsername)) {
finalUsername = `${baseUsername}${i++}`
}
const email: string | null = emp.email || null
const tempPassword = `Temp${Math.random().toString(36).slice(-8)}!@#`
const hashedPassword = await bcrypt.hash(tempPassword, 12)
const now = new Date().toISOString()
const userId = uuidv4()
db.prepare(`
INSERT INTO users (id, username, email, email_hash, password, role, employee_id, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
userId,
finalUsername,
email ? FieldEncryption.encrypt(email) : null,
email ? FieldEncryption.hash(email) : null,
hashedPassword,
role,
employeeId,
1,
now,
now
)
results.push({ employeeId, status: 'created', username: finalUsername, temporaryPassword: tempPassword })
} catch (err: any) {
logger.error('Bulk create error for employee ' + employeeId, err)
results.push({ employeeId, status: 'error', reason: err?.message || 'unknown_error' })
}
}
return res.status(201).json({ success: true, data: { results } })
} catch (error) {
logger.error('Error in bulk create from employees:', error)
next(error)
}
}
)
// Update user status (admin only)
router.put('/:id/status',
authenticate,
requirePermission('users:update'),
[
body('isActive').isBoolean()
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const { id } = req.params
const { isActive } = req.body
// Check if user exists
const existingUser = db.prepare('SELECT id, username FROM users WHERE id = ?').get(id) as any
if (!existingUser) {
return res.status(404).json({
success: false,
error: { message: 'User not found' }
})
}
// Prevent deactivating admin user
if (existingUser.username === 'admin' && !isActive) {
return res.status(403).json({
success: false,
error: { message: 'Cannot deactivate admin user' }
})
}
// Update status
db.prepare(`
UPDATE users SET is_active = ?, updated_at = ?
WHERE id = ?
`).run(isActive ? 1 : 0, new Date().toISOString(), id)
logger.info(`User status updated: ${existingUser.username} -> ${isActive ? 'active' : 'inactive'}`)
res.json({ success: true, message: 'Status updated successfully' })
} catch (error) {
logger.error('Error updating user status:', error)
next(error)
}
}
)
// Reset user password (admin only)
router.post('/:id/reset-password',
authenticate,
requirePermission('users:update'),
[
body('newPassword').optional().isLength({ min: 8 })
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const { id } = req.params
const { newPassword } = req.body
// Check if user exists
const existingUser = db.prepare('SELECT id, username FROM users WHERE id = ?').get(id) as any
if (!existingUser) {
return res.status(404).json({
success: false,
error: { message: 'User not found' }
})
}
// Generate password
const password = newPassword || `Temp${Math.random().toString(36).slice(-8)}!@#`
const hashedPassword = await bcrypt.hash(password, 12)
// Update password
db.prepare(`
UPDATE users SET password = ?, updated_at = ?
WHERE id = ?
`).run(hashedPassword, new Date().toISOString(), id)
logger.info(`Password reset for user: ${existingUser.username}`)
res.json({
success: true,
message: 'Password reset successfully',
data: { temporaryPassword: newPassword ? undefined : password }
})
} catch (error) {
logger.error('Error resetting password:', error)
next(error)
}
}
)
// Delete user (admin only)
router.delete('/:id',
authenticate,
requirePermission('users:delete'),
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
// Check if user exists
const existingUser = db.prepare('SELECT id, username FROM users WHERE id = ?').get(id) as any
if (!existingUser) {
return res.status(404).json({
success: false,
error: { message: 'User not found' }
})
}
// Prevent deleting admin user
if (existingUser.username === 'admin') {
return res.status(403).json({
success: false,
error: { message: 'Cannot delete admin user' }
})
}
// Delete user
db.prepare('DELETE FROM users WHERE id = ?').run(id)
logger.info(`User deleted: ${existingUser.username}`)
res.json({ success: true, message: 'User deleted successfully' })
} catch (error) {
logger.error('Error deleting user:', error)
next(error)
}
}
)
export default router
// Create user account from existing employee (admin only)
router.post('/create-from-employee',
authenticate,
requirePermission('users:create'),
[
body('employeeId').notEmpty().isString(),
body('username').optional().isString().isLength({ min: 3 }),
body('role').isIn(['admin', 'superuser', 'user'])
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const { employeeId, username, role } = req.body
// Check employee exists
const employee = encryptedDb.getEmployee(employeeId) as any
if (!employee) {
return res.status(404).json({ success: false, error: { message: 'Employee not found' } })
}
// Check if a user is already linked to this employee
const existingByEmployee = db.prepare('SELECT id FROM users WHERE employee_id = ?').get(employeeId)
if (existingByEmployee) {
return res.status(409).json({ success: false, error: { message: 'User already exists for this employee' } })
}
// Determine email and username
const email: string | null = employee.email || null
const finalUsername = username || (email ? String(email).split('@')[0] : `${employee.first_name}.${employee.last_name}`)
// Check username uniqueness
const usernameExists = db.prepare('SELECT id FROM users WHERE username = ?').get(finalUsername)
if (usernameExists) {
return res.status(409).json({ success: false, error: { message: 'Username already exists' } })
}
// Check email uniqueness if available
if (email) {
const emailHash = FieldEncryption.hash(email)
const emailExists = db.prepare('SELECT id FROM users WHERE email_hash = ?').get(emailHash)
if (emailExists) {
return res.status(409).json({ success: false, error: { message: 'Email already used by another account' } })
}
}
// Generate temp password
const tempPassword = `Temp${Math.random().toString(36).slice(-8)}!@#`
const hashedPassword = await bcrypt.hash(tempPassword, 12)
const now = new Date().toISOString()
const userId = uuidv4()
// Insert user with encrypted email
db.prepare(`
INSERT INTO users (id, username, email, email_hash, password, role, employee_id, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
userId,
finalUsername,
email ? FieldEncryption.encrypt(email) : null,
email ? FieldEncryption.hash(email) : null,
hashedPassword,
role,
employeeId,
1,
now,
now
)
logger.info(`User created from employee ${employeeId}: ${finalUsername} (${role})`)
return res.status(201).json({ success: true, data: { id: userId, username: finalUsername, temporaryPassword: tempPassword } })
} catch (error) {
logger.error('Error creating user from employee:', error)
next(error)
}
}
)
// Purge users: keep only admin and one specified email (admin only)
router.post('/purge',
authenticate,
authorize('admin'),
[
body('email').isEmail()
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const { email } = req.body as { email: string }
const emailHash = FieldEncryption.hash(email)
const total = (db.prepare('SELECT COUNT(*) as c FROM users').get() as any).c || 0
const keep = db.prepare('SELECT id, username FROM users WHERE username = ? OR email_hash = ?').all('admin', emailHash) as any[]
const delStmt = db.prepare('DELETE FROM users WHERE username <> ? AND (email_hash IS NULL OR email_hash <> ?)')
const info = delStmt.run('admin', emailHash)
logger.warn(`User purge executed by ${req.user?.username}. Kept ${keep.length}, deleted ${info.changes}.`)
return res.json({ success: true, data: { total, kept: keep.length, deleted: info.changes } })
} catch (error) {
logger.error('Error purging users:', error)
next(error)
}
}
)
// Send temporary password via email to user's email
router.post('/:id/send-temp-password',
authenticate,
requirePermission('users:update'),
[
body('password').notEmpty().isString().isLength({ min: 6 })
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: { message: 'Invalid input', details: errors.array() }
})
}
const { id } = req.params
const { password } = req.body as { password: string }
// Load user
const user = db.prepare('SELECT id, username, email, employee_id FROM users WHERE id = ?').get(id) as any
if (!user) {
return res.status(404).json({ success: false, error: { message: 'User not found' } })
}
// Decrypt email
const email = user.email ? FieldEncryption.decrypt(user.email) : null
if (!email) {
return res.status(400).json({ success: false, error: { message: 'User has no email address' } })
}
// Optional: get first name for nicer email copy
let firstName: string | undefined = undefined
if (user.employee_id) {
const emp = encryptedDb.getEmployee(user.employee_id)
if (emp && emp.first_name) firstName = emp.first_name
}
const emailNotificationsSetting = db.prepare('SELECT value FROM system_settings WHERE key = ?').get('email_notifications_enabled') as any
const emailNotificationsEnabled = emailNotificationsSetting?.value === 'true'
if (!emailNotificationsEnabled || !emailService.isServiceEnabled()) {
return res.status(503).json({ success: false, error: { message: 'Email service not enabled' } })
}
const sent = await emailService.sendInitialPassword(email, password, firstName)
if (!sent) {
return res.status(500).json({ success: false, error: { message: 'Failed to send email' } })
}
logger.info(`Temporary password email sent to ${email} for user ${user.username}`)
return res.json({ success: true, message: 'Temporary password email sent' })
} catch (error) {
logger.error('Error sending temporary password email:', error)
next(error)
}
}
)

Datei anzeigen

@ -0,0 +1,299 @@
import { Router, Request, Response } from 'express'
import { db } from '../config/database'
import { authenticateToken, AuthRequest } from '../middleware/auth'
import { v4 as uuidv4 } from 'uuid'
import { Workspace, WorkspaceFilter } from '@skillmate/shared'
const router = Router()
// Get all workspaces with optional filters
router.get('/', authenticateToken, (req: AuthRequest, res: Response) => {
try {
const filters: WorkspaceFilter = req.query
let query = 'SELECT * FROM workspaces WHERE is_active = 1'
const params: any[] = []
if (filters.type) {
query += ' AND type = ?'
params.push(filters.type)
}
if (filters.floor) {
query += ' AND floor = ?'
params.push(filters.floor)
}
if (filters.building) {
query += ' AND building = ?'
params.push(filters.building)
}
if (filters.min_capacity) {
query += ' AND capacity >= ?'
params.push(filters.min_capacity)
}
query += ' ORDER BY floor, name'
const workspaces = db.prepare(query).all(...params)
// Parse equipment JSON
const parsedWorkspaces = workspaces.map((ws: any) => ({
...ws,
equipment: ws.equipment ? JSON.parse(ws.equipment) : [],
is_active: Boolean(ws.is_active)
}))
res.json(parsedWorkspaces)
} catch (error) {
console.error('Error fetching workspaces:', error)
res.status(500).json({ error: 'Failed to fetch workspaces' })
}
})
// Get available workspaces for a time range
router.post('/availability', authenticateToken, (req: AuthRequest, res: Response) => {
try {
const { start_time, end_time, type, capacity } = req.body
if (!start_time || !end_time) {
return res.status(400).json({ error: 'Start and end time required' })
}
let query = `
SELECT w.* FROM workspaces w
WHERE w.is_active = 1
AND w.id NOT IN (
SELECT DISTINCT workspace_id FROM bookings
WHERE status = 'confirmed'
AND (
(start_time <= ? AND end_time > ?)
OR (start_time < ? AND end_time >= ?)
OR (start_time >= ? AND end_time <= ?)
)
)
`
const params = [start_time, start_time, end_time, end_time, start_time, end_time]
if (type) {
query += ' AND w.type = ?'
params.push(type)
}
if (capacity) {
query += ' AND w.capacity >= ?'
params.push(capacity)
}
query += ' ORDER BY w.floor, w.name'
const availableWorkspaces = db.prepare(query).all(...params)
// Parse equipment JSON
const parsedWorkspaces = availableWorkspaces.map((ws: any) => ({
...ws,
equipment: ws.equipment ? JSON.parse(ws.equipment) : [],
is_active: Boolean(ws.is_active)
}))
res.json(parsedWorkspaces)
} catch (error) {
console.error('Error checking availability:', error)
res.status(500).json({ error: 'Failed to check availability' })
}
})
// Get workspace by ID
router.get('/:id', authenticateToken, (req: AuthRequest, res: Response) => {
try {
const workspace = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(req.params.id)
if (!workspace) {
return res.status(404).json({ error: 'Workspace not found' })
}
// Parse equipment JSON
const parsedWorkspace = {
...(workspace as any),
equipment: (workspace as any).equipment ? JSON.parse((workspace as any).equipment) : [],
is_active: Boolean((workspace as any).is_active)
}
res.json(parsedWorkspace)
} catch (error) {
console.error('Error fetching workspace:', error)
res.status(500).json({ error: 'Failed to fetch workspace' })
}
})
// Create new workspace (admin only)
router.post('/', authenticateToken, (req: AuthRequest, res: Response) => {
if (req.user?.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' })
}
try {
const {
name,
type,
floor,
building,
capacity = 1,
equipment = [],
position_x,
position_y
} = req.body
if (!name || !type || !floor) {
return res.status(400).json({ error: 'Name, type, and floor are required' })
}
const id = uuidv4()
const now = new Date().toISOString()
db.prepare(`
INSERT INTO workspaces (
id, name, type, floor, building, capacity, equipment,
position_x, position_y, is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id, name, type, floor, building, capacity,
JSON.stringify(equipment), position_x, position_y,
1, now, now
)
const newWorkspace = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(id)
res.status(201).json({
...(newWorkspace as any),
equipment: JSON.parse((newWorkspace as any).equipment || '[]'),
is_active: Boolean((newWorkspace as any).is_active)
})
} catch (error) {
console.error('Error creating workspace:', error)
res.status(500).json({ error: 'Failed to create workspace' })
}
})
// Update workspace (admin only)
router.put('/:id', authenticateToken, (req: AuthRequest, res: Response) => {
if (req.user?.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' })
}
try {
const {
name,
type,
floor,
building,
capacity,
equipment,
position_x,
position_y,
is_active
} = req.body
const existing = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(req.params.id)
if (!existing) {
return res.status(404).json({ error: 'Workspace not found' })
}
const updates = []
const params = []
if (name !== undefined) {
updates.push('name = ?')
params.push(name)
}
if (type !== undefined) {
updates.push('type = ?')
params.push(type)
}
if (floor !== undefined) {
updates.push('floor = ?')
params.push(floor)
}
if (building !== undefined) {
updates.push('building = ?')
params.push(building)
}
if (capacity !== undefined) {
updates.push('capacity = ?')
params.push(capacity)
}
if (equipment !== undefined) {
updates.push('equipment = ?')
params.push(JSON.stringify(equipment))
}
if (position_x !== undefined) {
updates.push('position_x = ?')
params.push(position_x)
}
if (position_y !== undefined) {
updates.push('position_y = ?')
params.push(position_y)
}
if (is_active !== undefined) {
updates.push('is_active = ?')
params.push(is_active ? 1 : 0)
}
updates.push('updated_at = ?')
params.push(new Date().toISOString())
params.push(req.params.id)
db.prepare(`
UPDATE workspaces
SET ${updates.join(', ')}
WHERE id = ?
`).run(...params)
const updated = db.prepare('SELECT * FROM workspaces WHERE id = ?').get(req.params.id)
res.json({
...(updated as any),
equipment: JSON.parse((updated as any).equipment || '[]'),
is_active: Boolean((updated as any).is_active)
})
} catch (error) {
console.error('Error updating workspace:', error)
res.status(500).json({ error: 'Failed to update workspace' })
}
})
// Delete workspace (admin only)
router.delete('/:id', authenticateToken, (req: AuthRequest, res: Response) => {
if (req.user?.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' })
}
try {
// Check if workspace has active bookings
const activeBookings = db.prepare(`
SELECT COUNT(*) as count FROM bookings
WHERE workspace_id = ? AND status = 'confirmed'
AND end_time > ?
`).get(req.params.id, new Date().toISOString())
if ((activeBookings as any).count > 0) {
return res.status(400).json({
error: 'Cannot delete workspace with active bookings'
})
}
// Soft delete by setting is_active = 0
db.prepare(`
UPDATE workspaces SET is_active = 0, updated_at = ?
WHERE id = ?
`).run(new Date().toISOString(), req.params.id)
res.json({ message: 'Workspace deleted successfully' })
} catch (error) {
console.error('Error deleting workspace:', error)
res.status(500).json({ error: 'Failed to delete workspace' })
}
})
export default router