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

511 Zeilen
17 KiB
TypeScript

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