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