import { Router, Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import { UserRole, UserStatus } from '@prisma/client'; import { usersService } from './users.service'; import { createUserSchema, updateUserSchema, listUsersSchema } from './users.schemas'; import { validate } from '../../middleware/validate'; import { authenticate } from '../../middleware/auth.middleware'; import { requireRole } from '../../middleware/rbac.middleware'; import { hasAnyRole, ADMIN_ROLES, getUserRoles } from '../../utils/roles'; import { prisma } from '../../config/database'; import { emailService } from '../../services/email.service'; import { env } from '../../config/env'; import { logger } from '../../utils/logger'; import { userProvisioningService } from '../../services/user-provisioning/provisioning.service'; /** Check if user can manage other users (SUPER_ADMIN or canManageUsers permission) */ function canManageUsers(user: { roles?: unknown; role?: UserRole; permissions?: Record | null }): boolean { const roles = getUserRoles(user); if (roles.includes(UserRole.SUPER_ADMIN)) return true; return !!(user.permissions as Record | null)?.canManageUsers; } /** Middleware: require user management permission */ function requireUserManagement(req: Request, res: Response, next: NextFunction) { if (!req.user) { res.status(401).json({ error: { message: 'Authentication required', code: 'AUTH_REQUIRED' } }); return; } if (!canManageUsers(req.user as any)) { res.status(403).json({ error: { message: 'Insufficient permissions', code: 'FORBIDDEN' } }); return; } next(); } const router = Router(); // All user routes require authentication router.use(authenticate); // GET /api/users — list users (any admin) router.get( '/', requireRole(...ADMIN_ROLES), validate(listUsersSchema, 'query'), async (req: Request, res: Response, next: NextFunction) => { try { const result = await usersService.findAll(req.query as any); res.json(result); } catch (err) { next(err); } } ); // GET /api/users/:id — get user (admin or self) router.get( '/:id', async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; const isAdminUser = hasAnyRole(req.user!, ADMIN_ROLES); const isSelf = req.user!.id === id; if (!isAdminUser && !isSelf) { res.status(403).json({ error: { message: 'Insufficient permissions', code: 'FORBIDDEN' } }); return; } const user = await usersService.findById(id); res.json(user); } catch (err) { next(err); } } ); // POST /api/users — create user (SUPER_ADMIN or canManageUsers) router.post( '/', requireUserManagement, validate(createUserSchema), async (req: Request, res: Response, next: NextFunction) => { try { const user = await usersService.create(req.body); res.status(201).json(user); } catch (err) { next(err); } } ); // PUT /api/users/:id — update user (admin or self, role changes require user management perm) router.put( '/:id', async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; const isAdminUser = hasAnyRole(req.user!, ADMIN_ROLES); const isSelf = req.user!.id === id; const canManage = canManageUsers(req.user as any); if (!isAdminUser && !isSelf) { res.status(403).json({ error: { message: 'Insufficient permissions', code: 'FORBIDDEN' } }); return; } // Only users with management permission can change role, roles, or status if (!canManage) { delete req.body.role; delete req.body.roles; delete req.body.status; delete req.body.permissions; } const parsed = updateUserSchema.parse(req.body); const user = await usersService.update(id, parsed); res.json(user); } catch (err) { next(err); } } ); // POST /api/users/:id/approve — approve pending user (SUPER_ADMIN or canManageUsers) router.post( '/:id/approve', requireUserManagement, async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; const user = await prisma.user.findUnique({ where: { id } }); if (!user) { res.status(404).json({ error: { message: 'User not found', code: 'USER_NOT_FOUND' } }); return; } if (user.status !== UserStatus.PENDING_APPROVAL) { res.status(400).json({ error: { message: 'User is not pending approval', code: 'INVALID_STATUS' } }); return; } await prisma.user.update({ where: { id }, data: { status: UserStatus.ACTIVE }, }); // Send approval notification email const adminUrl = env.ADMIN_URL || 'http://localhost:3000'; await emailService.sendAccountApprovedEmail({ recipientEmail: user.email, recipientName: user.name || 'there', loginUrl: `${adminUrl}/login`, }).catch(err => logger.error('Failed to send approval email:', err)); // Fire-and-forget: provision approved user to eager services userProvisioningService.onUserCreated({ id: user.id, email: user.email, name: user.name, role: user.role, roles: user.roles, status: 'ACTIVE', permissions: user.permissions as Record | null, }).catch(err => logger.warn('User provisioning hook (approve) failed:', err)); res.json({ message: 'User approved', userId: id }); } catch (err) { next(err); } } ); // POST /api/users/:id/reject — reject pending user (SUPER_ADMIN or canManageUsers) const rejectSchema = z.object({ reason: z.string().max(500).optional(), }); router.post( '/:id/reject', requireUserManagement, validate(rejectSchema), async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; const user = await prisma.user.findUnique({ where: { id } }); if (!user) { res.status(404).json({ error: { message: 'User not found', code: 'USER_NOT_FOUND' } }); return; } if (user.status !== UserStatus.PENDING_APPROVAL) { res.status(400).json({ error: { message: 'User is not pending approval', code: 'INVALID_STATUS' } }); return; } await prisma.user.update({ where: { id }, data: { status: UserStatus.INACTIVE }, }); res.json({ message: 'User rejected', userId: id }); } catch (err) { next(err); } } ); // GET /api/users/:id/contact — get linked Contact for a user (any admin) router.get( '/:id/contact', requireRole(...ADMIN_ROLES), async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; const contact = await prisma.contact.findFirst({ where: { userId: id, mergedIntoId: null }, select: { id: true, displayName: true, email: true, phone: true, tags: true, supportLevel: true, primarySource: true, profileToken: true, coverPhotoPath: true, }, }); res.json({ contact: contact || null }); } catch (err) { next(err); } } ); // DELETE /api/users/:id — delete user (SUPER_ADMIN or canManageUsers) router.delete( '/:id', requireUserManagement, async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; await usersService.delete(id); res.status(204).send(); } catch (err) { next(err); } } ); export { router as usersRouter };