import { Router } from "express"; import { z } from 'zod'; import { storage } from "../storage"; import { userStatusesStorage } from "../storage/user-statuses.storage"; import { requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware"; import { validateTenantAccess } from "../middleware/tenant.middleware"; import { validateRequest } from "../middleware/validation.middleware"; import { hashPassword } from "../utils/password"; import { logAudit, getClientIp } from "../utils/audit"; import { canManageUser, filterUsersByAccess, getManagedUserIds } from "../utils/user-access"; import { db } from "../db"; import { roles, roleMembers, userStatuses, insertUserStatusSchema, updateUserStatusSchema } from "@shared/schema"; import { eq, and, inArray } from "drizzle-orm"; export function registerUserRoutes(router: ReturnType): void { router.get('/api/users/:id(\\d+)', validateTenantAccess, async (req: AuthenticatedRequest, res) => { try { const userId = Number(req.params.id); if (!Number.isFinite(userId)) { return res.status(400).json({ success: false, error: 'Некорректный ID пользователя' }); } const user = await storage.getUser(userId); if (!user || user.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } const canEdit = await canManageUser(req.user, userId, req.organizationId!); res.json({ success: true, user: { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, middleName: user.middleName, position: user.position, phone: user.phone, appRole: user.appRole, isActive: user.isActive, emailVerified: user.emailVerified, statusId: user.statusId, lastLogin: user.lastLogin, createdAt: user.createdAt, canEdit } }); } catch (error) { console.error('Get user error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении пользователя' }); } }); async function handleListUsers(req: AuthenticatedRequest, res: import("express").Response) { const page = Math.max(1, parseInt(req.query.page as string) || 1); const pageSize = Math.min(100, Math.max(1, parseInt(req.query.pageSize as string) || 25)); let search = typeof req.query.search === 'string' ? req.query.search.trim() : undefined; let roleId = req.query.roleId ? parseInt(req.query.roleId as string) : undefined; let appRole: string | undefined = typeof req.query.appRole === 'string' ? req.query.appRole : undefined; let isActive: boolean | undefined = req.query.isActive === 'true' ? true : req.query.isActive === 'false' ? false : undefined; let sortBy: 'name' | 'createdAt' | 'lastLogin' = (req.query.sortBy as 'name' | 'createdAt' | 'lastLogin') || 'name'; let order: 'asc' | 'desc' = (req.query.order as 'asc' | 'desc') || 'asc'; // Обратная совместимость со старым форматом фильтров Users.tsx const filtersStr = typeof req.query.filters === 'string' ? req.query.filters : ''; if (filtersStr) { try { const filters = JSON.parse(filtersStr) as Record; if (filters.name && !search) search = filters.name; if (filters.email && !search) search = filters.email; if (filters.position && !search) search = filters.position; if (filters.role) { appRole = filters.role === 'Администратор' ? 'admin' : filters.role === 'Пользователь' ? 'user' : undefined; } if (filters.status) { isActive = filters.status === 'Активен' ? true : filters.status === 'Неактивен' ? false : undefined; } } catch { /* ignore */ } } const sortColumn = typeof req.query.sortColumn === 'string' ? req.query.sortColumn : undefined; const sortDirection = typeof req.query.sortDirection === 'string' ? req.query.sortDirection : undefined; if (sortColumn === 'name') sortBy = 'name'; if (sortColumn === 'createdAt') sortBy = 'createdAt'; if (sortColumn === 'lastLogin') sortBy = 'lastLogin'; if (sortDirection === 'asc' || sortDirection === 'desc') order = sortDirection; const isAdmin = req.user?.appRole === 'admin'; const managedIds = isAdmin ? null : await getManagedUserIds(req.user!.id, req.organizationId!); const result = await storage.listUsers(req.organizationId!, { search, roleId, appRole, isActive, sortBy, order, limit: pageSize, offset: (page - 1) * pageSize, }); let filteredUsers = result.users; if (managedIds !== null) { filteredUsers = result.users.filter(u => managedIds.has(u.id) || u.id === req.user!.id); } const userIds = filteredUsers.map(u => u.id); const userRoles = userIds.length > 0 ? await db .select({ userId: roleMembers.userId, roleId: roleMembers.roleId, roleName: roles.name }) .from(roleMembers) .innerJoin(roles, eq(roleMembers.roleId, roles.id)) .where(and(inArray(roleMembers.userId, userIds), eq(roles.organizationId, req.organizationId!))) : []; const rolesByUser = new Map(); for (const ur of userRoles) { if (!rolesByUser.has(ur.userId)) rolesByUser.set(ur.userId, []); rolesByUser.get(ur.userId)!.push({ id: ur.roleId, name: ur.roleName }); } const allStatuses = await userStatusesStorage.listByOrganization(req.organizationId!); const statusById = new Map(allStatuses.map(s => [s.id, s])); res.json({ success: true, users: filteredUsers.map(u => { const status = u.statusId ? statusById.get(u.statusId) : undefined; return { id: u.id, email: u.email, firstName: u.firstName, lastName: u.lastName, middleName: u.middleName, position: u.position, phone: u.phone, appRole: u.appRole, isActive: u.isActive, emailVerified: u.emailVerified, statusId: u.statusId, status: status ? { id: status.id, name: status.name, color: status.color } : null, lastLogin: u.lastLogin, createdAt: u.createdAt, organizationalRoles: rolesByUser.get(u.id) || [], }; }), total: managedIds === null ? result.total : filteredUsers.length, page, pageSize, }); } router.get('/api/users', validateTenantAccess, async (req: AuthenticatedRequest, res) => { try { await handleListUsers(req, res); } catch (error) { console.error('List users error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении списка пользователей' }); } }); router.get('/api/users/list', validateTenantAccess, async (req: AuthenticatedRequest, res) => { try { await handleListUsers(req, res); } catch (error) { console.error('List users error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении списка пользователей' }); } }); router.get('/api/users/column-values', validateTenantAccess, async (req: AuthenticatedRequest, res) => { try { const column = typeof req.query.column === 'string' ? req.query.column : ''; const search = typeof req.query.search === 'string' ? req.query.search.toLowerCase() : ''; const isAdmin = req.user?.appRole === 'admin'; const managedIds = isAdmin ? null : await getManagedUserIds(req.user!.id, req.organizationId!); const allUsers = await storage.getUsersByOrganization(req.organizationId!); let users = managedIds === null ? allUsers : allUsers.filter(u => managedIds.has(u.id) || u.id === req.user!.id); let values: string[] = []; switch (column) { case 'name': values = users.map(u => `${u.lastName} ${u.firstName} ${u.middleName || ''}`.trim()); break; case 'email': values = users.map(u => u.email); break; case 'role': values = users.map(u => u.appRole === 'admin' ? 'Администратор' : 'Пользователь'); break; case 'status': values = users.map(u => u.isActive ? 'Активен' : 'Неактивен'); break; case 'position': values = users.map(u => u.position || '').filter(Boolean); break; default: values = []; } const unique = Array.from(new Set(values)).filter(v => v.toLowerCase().includes(search)).slice(0, 50); res.json({ success: true, values: unique }); } catch (error) { console.error('Get user column values error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении значений колонки' }); } }); router.put('/api/users/:id(\\d+)', validateTenantAccess, async (req: AuthenticatedRequest, res) => { try { const userId = Number(req.params.id); if (!Number.isFinite(userId)) { return res.status(400).json({ success: false, error: 'Некорректный ID пользователя' }); } const user = await storage.getUser(userId); if (!user || user.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } const canEdit = await canManageUser(req.user, userId, req.organizationId!); if (!canEdit) { return res.status(403).json({ success: false, error: 'Недостаточно прав' }); } // Только админ может менять appRole, isActive и организационные роли const isAdmin = req.user?.appRole === 'admin'; const updates: Partial = { firstName: req.body.firstName, lastName: req.body.lastName, middleName: req.body.middleName, position: req.body.position, phone: req.body.phone, additionalPhones: req.body.additionalPhones, additionalEmails: req.body.additionalEmails, }; const oldRole = user.appRole; if (isAdmin) { updates.appRole = req.body.appRole; updates.isActive = req.body.isActive; updates.statusId = req.body.statusId; } const updatedUser = await storage.updateUser(userId, updates); // Audit log for system profile fields const auditFields = [ { name: 'Телефон', old: user.phone, new: updates.phone }, { name: 'Дополнительные телефоны', old: user.additionalPhones, new: updates.additionalPhones }, { name: 'Дополнительные email', old: user.additionalEmails, new: updates.additionalEmails }, ]; for (const auditField of auditFields) { if (JSON.stringify(auditField.old ?? null) !== JSON.stringify(auditField.new ?? null)) { await storage.createUserProfileAuditLog({ userId, organizationId: req.organizationId!, action: 'field_value.updated', fieldName: auditField.name, oldValue: auditField.old ?? null, newValue: auditField.new ?? null, changedBy: req.user!.id, changedByName: req.user ? `${req.user.firstName || ''} ${req.user.lastName || ''}`.trim() : undefined, }); } } if (isAdmin && req.body.appRole !== undefined && req.body.appRole !== oldRole) { logAudit({ action: 'user.role.changed', userId: req.user!.id, organizationId: req.organizationId!, details: { targetUserId: userId, oldRole, newRole: req.body.appRole, }, ip: getClientIp(req), userAgent: req.headers['user-agent'] ?? null, }); } // Update organizational roles if provided (only admin) if (isAdmin && req.body.organizationalRoleIds !== undefined) { await storage.setUserOrganizationalRoles(userId, req.body.organizationalRoleIds); } res.json({ success: true, message: 'Пользователь обновлен', user: { id: updatedUser.id, email: updatedUser.email, firstName: updatedUser.firstName, lastName: updatedUser.lastName, middleName: updatedUser.middleName, position: updatedUser.position, phone: updatedUser.phone, appRole: updatedUser.appRole, isActive: updatedUser.isActive, emailVerified: updatedUser.emailVerified, lastLogin: updatedUser.lastLogin, createdAt: updatedUser.createdAt } }); } catch (error) { console.error('Update user error:', error); res.status(500).json({ success: false, error: 'Ошибка при обновлении пользователя' }); } } ); // Change user password endpoint for admins router.put('/api/users/:id/password', requirePermission('users.manage'), validateTenantAccess, validateRequest(z.object({ password: z.string().min(1, 'Пароль обязателен') })), async (req: AuthenticatedRequest, res) => { try { const userId = parseInt(req.params.id); const user = await storage.getUser(userId); if (!user || user.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } // Hash the new password with validation const passwordHash = await hashPassword(req.body.password); // Update user password await storage.updateUser(userId, { passwordHash, updatedAt: new Date() }); // Revoke all existing sessions for the user to force re-login await storage.deleteUserSessions(userId); res.json({ success: true, message: 'Пароль пользователя изменен' }); } catch (error) { console.error('Change password error:', error); // Check if it's a password validation error if (error instanceof Error && error.message.includes('Пароль должен')) { return res.status(400).json({ success: false, error: error.message }); } res.status(500).json({ success: false, error: 'Ошибка при изменении пароля' }); } } ); router.delete('/api/users/:id', requirePermission('users.manage'), validateTenantAccess, async (req: AuthenticatedRequest, res) => { try { const userId = parseInt(req.params.id); const user = await storage.getUser(userId); if (!user || user.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } // Don't allow deleting yourself if (userId === req.user.id) { return res.status(400).json({ success: false, error: 'Нельзя удалить самого себя' }); } await storage.deleteUser(userId); res.json({ success: true, message: 'Пользователь удален' }); } catch (error) { console.error('Delete user error:', error); res.status(500).json({ success: false, error: 'Ошибка при удалении пользователя' }); } } ); // Organization stats router.get('/api/organizations/stats', async (req: AuthenticatedRequest, res) => { try { const users = await storage.getUsersByOrganization(req.organizationId!); const stats = { totalUsers: users.length, activeUsers: users.filter(u => u.isActive).length, adminUsers: users.filter(u => u.appRole === 'admin').length, inactiveUsers: users.filter(u => !u.isActive).length, unverifiedUsers: users.filter(u => !u.emailVerified).length }; res.json({ success: true, stats }); } catch (error) { console.error('Get stats error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении статистики' }); } }); // Get organizational roles for a user router.get('/api/users/:id/roles', validateTenantAccess, async (req: AuthenticatedRequest, res) => { try { const userId = Number(req.params.id); const user = await storage.getUser(userId); if (!user || user.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } const roles = await storage.getUserOrganizationalRoles(userId, req.organizationId!); res.json({ success: true, roles }); } catch (error) { console.error('Get user roles error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении ролей' }); } } ); // Update organizational roles for a user router.put('/api/users/:id/roles', requirePermission('users.manage'), validateTenantAccess, async (req: AuthenticatedRequest, res) => { try { const userId = Number(req.params.id); const user = await storage.getUser(userId); if (!user || user.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } const roleIds = Array.isArray(req.body.roleIds) ? req.body.roleIds.map(Number) : []; await storage.setUserOrganizationalRoles(userId, roleIds); res.json({ success: true }); } catch (error) { console.error('Set user roles error:', error); res.status(500).json({ success: false, error: 'Ошибка при обновлении ролей' }); } } ); // Linked tasks for user profile dossier router.get('/api/users/:id(\\d+)/linked-tasks', validateTenantAccess, async (req: AuthenticatedRequest, res) => { try { const userId = Number(req.params.id); if (!Number.isFinite(userId)) { return res.status(400).json({ success: false, error: 'Некорректный ID пользователя' }); } const targetUser = await storage.getUser(userId); if (!targetUser || targetUser.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } const canView = await canManageUser(req.user, userId, req.organizationId!) || req.user?.id === userId; if (!canView) { return res.status(403).json({ success: false, error: 'Недостаточно прав' }); } const linked = await storage.getLinkedTasksForUser(userId, req.organizationId!); const appRole = req.user!.appRole; const accessibleIds = await storage.getAccessibleTaskIds(req.user!.id, req.organizationId!, appRole); const filtered = accessibleIds !== null ? linked.filter((item) => accessibleIds.has(item.task.id)) : linked; res.json({ success: true, tasks: filtered.map((item) => ({ id: item.task.id, title: item.task.title, formId: item.form.id, formName: item.form.name, statusId: item.status?.id, statusName: item.status?.name, statusColor: item.status?.color, isCompleted: item.task.isCompleted, dueDate: item.task.dueDate, createdAt: item.task.createdAt, updatedAt: item.task.updatedAt, fieldId: item.field.id, fieldName: item.field.name, })), }); } catch (error) { console.error('Get linked tasks error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении связанных задач' }); } } ); // Get application roles (system roles) router.get('/api/app-roles', async (_req, res) => { try { const { appRoles: arTable } = await import('@shared/schema'); const { db } = await import('../db'); const rows = await db.select().from(arTable).orderBy(arTable.name); res.json({ success: true, appRoles: rows }); } catch (error) { console.error('Get app roles error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении ролей' }); } }); // --- User statuses --- router.get('/api/user-statuses', validateTenantAccess, async (req: AuthenticatedRequest, res) => { try { const statuses = await userStatusesStorage.listByOrganization(req.organizationId!); res.json({ success: true, statuses }); } catch (error) { console.error('List user statuses error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении статусов' }); } }); router.post('/api/user-statuses', requirePermission('users.manage'), validateTenantAccess, validateRequest(insertUserStatusSchema.omit({ organizationId: true })), async (req: AuthenticatedRequest, res) => { try { const status = await userStatusesStorage.create({ ...req.body, organizationId: req.organizationId!, }); res.json({ success: true, status }); } catch (error) { console.error('Create user status error:', error); res.status(500).json({ success: false, error: 'Ошибка при создании статуса' }); } } ); router.put('/api/user-statuses/:id', requirePermission('users.manage'), validateTenantAccess, validateRequest(updateUserStatusSchema.partial()), async (req: AuthenticatedRequest, res) => { try { const id = Number(req.params.id); if (!Number.isFinite(id)) { return res.status(400).json({ success: false, error: 'Некорректный ID' }); } const status = await userStatusesStorage.update(id, req.organizationId!, req.body); res.json({ success: true, status }); } catch (error) { console.error('Update user status error:', error); res.status(500).json({ success: false, error: 'Ошибка при обновлении статуса' }); } } ); router.delete('/api/user-statuses/:id', requirePermission('users.manage'), validateTenantAccess, async (req: AuthenticatedRequest, res) => { try { const id = Number(req.params.id); if (!Number.isFinite(id)) { return res.status(400).json({ success: false, error: 'Некорректный ID' }); } await userStatusesStorage.delete(id, req.organizationId!); res.json({ success: true }); } catch (error) { console.error('Delete user status error:', error); res.status(500).json({ success: false, error: 'Ошибка при удалении статуса' }); } } ); router.put('/api/users/:id(\d+)/status', validateTenantAccess, validateRequest(z.object({ statusId: z.union([z.number(), z.null()]) })), async (req: AuthenticatedRequest, res) => { try { const userId = Number(req.params.id); const { statusId } = req.body; const user = await storage.getUser(userId); if (!user || user.organizationId !== req.organizationId) { return res.status(404).json({ success: false, error: 'Пользователь не найден' }); } const canEdit = await canManageUser(req.user, userId, req.organizationId!); if (!canEdit) { return res.status(403).json({ success: false, error: 'Недостаточно прав' }); } if (statusId !== null) { const status = await userStatusesStorage.getById(statusId); if (!status || status.organizationId !== req.organizationId) { return res.status(400).json({ success: false, error: 'Статус не найден' }); } } await storage.updateUser(userId, { statusId }); res.json({ success: true }); } catch (error) { console.error('Update user status error:', error); res.status(500).json({ success: false, error: 'Ошибка при изменении статуса' }); } } ); }