244 lines
7.5 KiB
TypeScript
244 lines
7.5 KiB
TypeScript
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<string, unknown> | null }): boolean {
|
|
const roles = getUserRoles(user);
|
|
if (roles.includes(UserRole.SUPER_ADMIN)) return true;
|
|
return !!(user.permissions as Record<string, unknown> | 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<string, unknown> | 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 };
|