- TaskChat: atBottomRef-гвард (scrollHeight-scrollTop-clientHeight<80) — чужие сообщения больше не дёргают чат вниз, когда пользователь читает историю; свои сообщения и смена задачи скроллят как раньше - ChatAttachments: img w-180 h-180 object-cover вместо max-* — высота строки не меняется при догрузке, measureElement виртуализатора не дёргает список - упоминания: голый @ открывает список сразу; текстовый триггер от 3 букв; сервер ищет ILIKE по имени/отчеству/фамилии/email и отдаёт всех при пустом q; починен мёртвый fallback-резолв (Response без .json()) - groupByDate/flattenGroupedMessages обёрнуты в useMemo
652 lines
27 KiB
TypeScript
652 lines
27 KiB
TypeScript
import { Router } from "express";
|
||
import { z } from 'zod';
|
||
import { storage } from "../storage";
|
||
import { authService } from "../services/auth.service";
|
||
import { authenticateToken, requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware";
|
||
import { tenantIsolation } from "../middleware/tenant.middleware";
|
||
import { validateRequest } from "../middleware/validation.middleware";
|
||
import {
|
||
createUserCustomFieldSchema, updateUserCustomFieldSchema,
|
||
setUserCustomValueSchema,
|
||
} from "@shared/schema";
|
||
|
||
export function registerUserManagementRoutes(router: Router): void {
|
||
// === User Custom Fields routes ===
|
||
|
||
router.get('/api/user-custom-fields',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
requirePermission('users.manage'),
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const fields = await storage.getUserCustomFields(req.organizationId!);
|
||
res.json({ success: true, fields });
|
||
} catch (error) {
|
||
console.error('Get user custom fields error:', error);
|
||
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
|
||
}
|
||
}
|
||
);
|
||
|
||
router.post('/api/user-custom-fields',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
requirePermission('users.manage'),
|
||
validateRequest(createUserCustomFieldSchema),
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const existingFields = await storage.getUserCustomFields(req.organizationId!);
|
||
if (existingFields.some(f => f.code === req.body.code)) {
|
||
return res.status(400).json({ success: false, error: 'Поле с таким кодом уже существует' });
|
||
}
|
||
const field = await storage.createUserCustomField(req.organizationId!, req.body);
|
||
res.status(201).json({ success: true, field });
|
||
} catch (error) {
|
||
console.error('Create user custom field error:', error);
|
||
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
|
||
}
|
||
}
|
||
);
|
||
|
||
router.patch('/api/user-custom-fields/:id',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
requirePermission('users.manage'),
|
||
validateRequest(updateUserCustomFieldSchema),
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const id = parseInt(req.params.id);
|
||
const existing = await storage.getUserCustomField(id, req.organizationId!);
|
||
if (!existing) {
|
||
return res.status(404).json({ success: false, error: 'Поле не найдено' });
|
||
}
|
||
if (req.body.code && req.body.code !== existing.code) {
|
||
const allFields = await storage.getUserCustomFields(req.organizationId!);
|
||
if (allFields.some(f => f.code === req.body.code && f.id !== id)) {
|
||
return res.status(400).json({ success: false, error: 'Поле с таким кодом уже существует' });
|
||
}
|
||
}
|
||
const field = await storage.updateUserCustomField(id, req.organizationId!, req.body);
|
||
res.json({ success: true, field });
|
||
} catch (error) {
|
||
console.error('Update user custom field error:', error);
|
||
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
|
||
}
|
||
}
|
||
);
|
||
|
||
router.delete('/api/user-custom-fields/:id',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
requirePermission('users.manage'),
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const id = parseInt(req.params.id);
|
||
await storage.deleteUserCustomField(id, req.organizationId!);
|
||
res.json({ success: true, message: 'Поле удалено' });
|
||
} catch (error) {
|
||
console.error('Delete user custom field error:', error);
|
||
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
|
||
}
|
||
}
|
||
);
|
||
|
||
router.post('/api/user-custom-fields/reorder',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
requirePermission('users.manage'),
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const { fieldIds } = req.body;
|
||
if (!Array.isArray(fieldIds)) {
|
||
return res.status(400).json({ success: false, error: 'fieldIds должен быть массивом' });
|
||
}
|
||
await storage.reorderUserCustomFields(req.organizationId!, fieldIds);
|
||
res.json({ success: true });
|
||
} catch (error) {
|
||
console.error('Reorder user custom fields error:', error);
|
||
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Get custom values for a user
|
||
router.get('/api/users/:userId/custom-values',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const userId = parseInt(req.params.userId);
|
||
const targetUser = await storage.getUser(userId);
|
||
if (!targetUser || targetUser.organizationId !== req.organizationId) {
|
||
return res.status(404).json({ success: false, error: 'Пользователь не найден' });
|
||
}
|
||
const appRole = req.user?.appRole;
|
||
if (appRole !== 'admin' && req.user?.id !== userId) {
|
||
return res.status(403).json({ success: false, error: 'Недостаточно прав' });
|
||
}
|
||
const values = await storage.getUserCustomValues(userId, req.organizationId!);
|
||
res.json({ success: true, values });
|
||
} catch (error) {
|
||
console.error('Get user custom values error:', error);
|
||
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Set custom values for a user (admin only, or self)
|
||
router.put('/api/users/:userId/custom-values',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const userId = parseInt(req.params.userId);
|
||
const targetUser = await storage.getUser(userId);
|
||
if (!targetUser || targetUser.organizationId !== req.organizationId) {
|
||
return res.status(404).json({ success: false, error: 'Пользователь не найден' });
|
||
}
|
||
const appRole = req.user?.appRole;
|
||
if (appRole !== 'admin' && req.user?.id !== userId) {
|
||
return res.status(403).json({ success: false, error: 'Недостаточно прав' });
|
||
}
|
||
const { values } = req.body;
|
||
if (!Array.isArray(values)) {
|
||
return res.status(400).json({ success: false, error: 'values должен быть массивом' });
|
||
}
|
||
await storage.setUserCustomValues(userId, values, req.organizationId!);
|
||
const updatedValues = await storage.getUserCustomValues(userId, req.organizationId!);
|
||
res.json({ success: true, values: updatedValues });
|
||
} catch (error) {
|
||
console.error('Set user custom values error:', error);
|
||
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// === User management routes ===
|
||
|
||
// Public list endpoint — accessible to all org members (needed for chats, assignments, etc.)
|
||
router.get('/api/users/list',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const users = await storage.getUsersByOrganization(req.organizationId!);
|
||
const orgRoles = await storage.getRoles(req.organizationId!);
|
||
|
||
let filtersObj: Record<string, string> = {};
|
||
try { filtersObj = req.query.filters ? JSON.parse(req.query.filters as string) : {}; } catch {}
|
||
const sortColumn = (req.query.sortColumn as string) || '';
|
||
const sortDirection = (req.query.sortDirection as string) === 'desc' ? 'desc' : 'asc';
|
||
|
||
// Build role membership map for all users
|
||
const roleMembersMap = new Map<number, Array<{ id: number; name: string }>>();
|
||
for (const r of orgRoles) {
|
||
for (const m of r.members) {
|
||
if (!roleMembersMap.has(m.userId)) roleMembersMap.set(m.userId, []);
|
||
roleMembersMap.get(m.userId)!.push({ id: r.id, name: r.name });
|
||
}
|
||
}
|
||
|
||
const getUserColVal = (u: any, col: string): string => {
|
||
if (col === 'name') return `${u.firstName} ${u.middleName || ''} ${u.lastName}`.trim();
|
||
if (col === 'email') return u.email || '';
|
||
if (col === 'role') return u.appRole || '';
|
||
if (col === 'status') return u.isActive ? 'Активен' : 'Неактивен';
|
||
if (col === 'position') return u.position || '';
|
||
return '';
|
||
};
|
||
|
||
let safeUsers = users.map(user => ({
|
||
id: user.id,
|
||
email: user.email,
|
||
firstName: user.firstName,
|
||
lastName: user.lastName,
|
||
middleName: user.middleName,
|
||
position: user.position,
|
||
appRole: user.appRole,
|
||
isActive: user.isActive,
|
||
emailVerified: user.emailVerified,
|
||
lastLogin: user.lastLogin,
|
||
lastActivityAt: user.lastActivityAt,
|
||
createdAt: user.createdAt,
|
||
organizationalRoles: roleMembersMap.get(user.id) || [],
|
||
}));
|
||
|
||
Object.entries(filtersObj).forEach(([col, val]) => {
|
||
if (!val) return;
|
||
const valLower = (val as string).toLowerCase();
|
||
safeUsers = safeUsers.filter(u => getUserColVal(u, col).toLowerCase().includes(valLower));
|
||
});
|
||
|
||
if (sortColumn) {
|
||
safeUsers = safeUsers.sort((a, b) => {
|
||
const av = getUserColVal(a, sortColumn).toLowerCase();
|
||
const bv = getUserColVal(b, sortColumn).toLowerCase();
|
||
const cmp = av.localeCompare(bv, 'ru');
|
||
return sortDirection === 'asc' ? cmp : -cmp;
|
||
});
|
||
}
|
||
|
||
res.json({ success: true, users: safeUsers });
|
||
} catch (error) {
|
||
console.error('Get users list error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при получении пользователей' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Admin-only user list (kept for backward compatibility with admin integrations)
|
||
router.get('/api/users',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
requirePermission('users.manage'),
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const users = await storage.getUsersByOrganization(req.organizationId!);
|
||
const orgRoles = await storage.getRoles(req.organizationId!);
|
||
|
||
let filtersObj: Record<string, string> = {};
|
||
try { filtersObj = req.query.filters ? JSON.parse(req.query.filters as string) : {}; } catch {}
|
||
const sortColumn = (req.query.sortColumn as string) || '';
|
||
const sortDirection = (req.query.sortDirection as string) === 'desc' ? 'desc' : 'asc';
|
||
|
||
// Build role membership map for all users
|
||
const roleMembersMap = new Map<number, Array<{ id: number; name: string }>>();
|
||
for (const r of orgRoles) {
|
||
for (const m of r.members) {
|
||
if (!roleMembersMap.has(m.userId)) roleMembersMap.set(m.userId, []);
|
||
roleMembersMap.get(m.userId)!.push({ id: r.id, name: r.name });
|
||
}
|
||
}
|
||
|
||
const getUserColVal = (u: any, col: string): string => {
|
||
if (col === 'name') return `${u.firstName} ${u.middleName || ''} ${u.lastName}`.trim();
|
||
if (col === 'email') return u.email || '';
|
||
if (col === 'role') return u.appRole || '';
|
||
if (col === 'status') return u.isActive ? 'Активен' : 'Неактивен';
|
||
if (col === 'position') return u.position || '';
|
||
return '';
|
||
};
|
||
|
||
let safeUsers = users.map(user => ({
|
||
id: user.id,
|
||
email: user.email,
|
||
firstName: user.firstName,
|
||
lastName: user.lastName,
|
||
middleName: user.middleName,
|
||
position: user.position,
|
||
appRole: user.appRole,
|
||
isActive: user.isActive,
|
||
emailVerified: user.emailVerified,
|
||
lastLogin: user.lastLogin,
|
||
lastActivityAt: user.lastActivityAt,
|
||
createdAt: user.createdAt,
|
||
gpsAccess: user.gpsAccess,
|
||
financeAccess: user.financeAccess,
|
||
organizationalRoles: roleMembersMap.get(user.id) || [],
|
||
}));
|
||
|
||
Object.entries(filtersObj).forEach(([col, val]) => {
|
||
if (!val) return;
|
||
const valLower = (val as string).toLowerCase();
|
||
safeUsers = safeUsers.filter(u => getUserColVal(u, col).toLowerCase().includes(valLower));
|
||
});
|
||
|
||
if (sortColumn) {
|
||
safeUsers = safeUsers.sort((a, b) => {
|
||
const av = getUserColVal(a, sortColumn).toLowerCase();
|
||
const bv = getUserColVal(b, sortColumn).toLowerCase();
|
||
const cmp = av.localeCompare(bv, 'ru');
|
||
return sortDirection === 'asc' ? cmp : -cmp;
|
||
});
|
||
}
|
||
|
||
res.json({ success: true, users: safeUsers });
|
||
} catch (error) {
|
||
console.error('Get users error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при получении пользователей' });
|
||
}
|
||
});
|
||
|
||
// PATCH /api/users/:id/module-access — флаги доступа к модулям GPS/Финансы (только admin)
|
||
router.patch('/api/users/:id/module-access',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
if (req.user!.appRole !== 'admin') {
|
||
return res.status(403).json({ success: false, error: 'Требуются права администратора' });
|
||
}
|
||
const userId = parseInt(req.params.id);
|
||
if (isNaN(userId)) {
|
||
return res.status(400).json({ success: false, error: 'Некорректный ID пользователя' });
|
||
}
|
||
|
||
const { gpsAccess, financeAccess } = req.body ?? {};
|
||
const updates: { gpsAccess?: boolean; financeAccess?: boolean } = {};
|
||
if (gpsAccess !== undefined) {
|
||
if (typeof gpsAccess !== 'boolean') {
|
||
return res.status(400).json({ success: false, error: 'gpsAccess: boolean' });
|
||
}
|
||
updates.gpsAccess = gpsAccess;
|
||
}
|
||
if (financeAccess !== undefined) {
|
||
if (typeof financeAccess !== 'boolean') {
|
||
return res.status(400).json({ success: false, error: 'financeAccess: boolean' });
|
||
}
|
||
updates.financeAccess = financeAccess;
|
||
}
|
||
if (Object.keys(updates).length === 0) {
|
||
return res.status(400).json({ success: false, error: 'Передайте gpsAccess и/или financeAccess' });
|
||
}
|
||
|
||
// Tenant isolation: редактируем только пользователя своей организации
|
||
const target = await storage.getUser(userId);
|
||
if (!target || target.organizationId !== req.organizationId!) {
|
||
return res.status(404).json({ success: false, error: 'Пользователь не найден' });
|
||
}
|
||
|
||
const updated = await storage.updateUser(userId, updates);
|
||
res.json({
|
||
success: true,
|
||
user: { id: updated.id, gpsAccess: updated.gpsAccess, financeAccess: updated.financeAccess },
|
||
});
|
||
} catch (error) {
|
||
console.error('Update user module access error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при обновлении доступа к модулям' });
|
||
}
|
||
});
|
||
|
||
|
||
// Column values for users (autocomplete for filters)
|
||
router.get('/api/users/column-values',
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const column = (req.query.column as string) || '';
|
||
const search = ((req.query.search as string) || '').toLowerCase();
|
||
let context: Record<string, string> = {};
|
||
try { context = req.query.context ? JSON.parse(req.query.context as string) : {}; } catch {}
|
||
|
||
let users = await storage.getUsersByOrganization(req.organizationId!);
|
||
|
||
const getUserColVal = (u: any, col: string): string => {
|
||
if (col === 'name') return `${u.firstName} ${u.middleName || ''} ${u.lastName}`.trim();
|
||
if (col === 'email') return u.email || '';
|
||
if (col === 'role') return u.appRole || '';
|
||
if (col === 'status') return u.isActive ? 'Активен' : 'Неактивен';
|
||
if (col === 'position') return u.position || '';
|
||
return '';
|
||
};
|
||
|
||
Object.entries(context).forEach(([col, val]) => {
|
||
if (!val) return;
|
||
const vl = (val as string).toLowerCase();
|
||
users = users.filter(u => getUserColVal(u, col).toLowerCase().includes(vl));
|
||
});
|
||
|
||
const seen = new Set<string>();
|
||
const values: string[] = [];
|
||
for (const u of users) {
|
||
const val = getUserColVal(u, column);
|
||
if (!val || seen.has(val)) continue;
|
||
if (search && !val.toLowerCase().includes(search)) continue;
|
||
seen.add(val);
|
||
values.push(val);
|
||
if (values.length >= 20) break;
|
||
}
|
||
|
||
res.json({ values });
|
||
} catch (error) {
|
||
res.status(500).json({ error: 'Ошибка получения значений' });
|
||
}
|
||
}
|
||
);
|
||
|
||
router.post('/api/users',
|
||
requirePermission('users.manage'),
|
||
validateRequest(z.object({
|
||
email: z.string().email('Неверный формат email'),
|
||
firstName: z.string().min(1, 'Имя обязательно'),
|
||
lastName: z.string().min(1, 'Фамилия обязательна'),
|
||
middleName: z.string().optional(),
|
||
position: z.string().optional(),
|
||
appRole: z.string().min(1, 'Выберите роль доступа'),
|
||
organizationalRoleIds: z.array(z.number()).optional(),
|
||
sendInvite: z.boolean().default(true),
|
||
statusId: z.union([z.number(), z.null()]).optional(),
|
||
// Необязательный явный пароль; состав дополнительно проверяет hashPassword (400 с текстом правила)
|
||
password: z.string().min(8, 'Пароль должен содержать минимум 8 символов').optional()
|
||
})),
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const result = await authService.createUser(req.organizationId!, {
|
||
email: req.body.email,
|
||
firstName: req.body.firstName,
|
||
lastName: req.body.lastName,
|
||
middleName: req.body.middleName,
|
||
position: req.body.position,
|
||
appRole: req.body.appRole,
|
||
organizationalRoleIds: req.body.organizationalRoleIds,
|
||
sendInvite: req.body.sendInvite,
|
||
statusId: req.body.statusId ?? undefined,
|
||
password: req.body.password
|
||
});
|
||
|
||
if (result.success && result.user) {
|
||
res.status(201).json({
|
||
success: true,
|
||
message: result.message,
|
||
user: {
|
||
id: result.user.id,
|
||
email: result.user.email,
|
||
firstName: result.user.firstName,
|
||
lastName: result.user.lastName,
|
||
middleName: result.user.middleName,
|
||
position: result.user.position,
|
||
appRole: result.user.appRole,
|
||
isActive: result.user.isActive,
|
||
emailVerified: result.user.emailVerified,
|
||
createdAt: result.user.createdAt
|
||
}
|
||
});
|
||
} else {
|
||
res.status(400).json({ success: false, error: result.error });
|
||
}
|
||
} catch (error) {
|
||
console.error('Create user error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при создании пользователя' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Search users for @mentions (must be declared before /:id route)
|
||
router.get('/api/users/search',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const { q: query, limit = '10' } = req.query;
|
||
|
||
if (!query || typeof query !== 'string' || query.trim().length === 0) {
|
||
return res.status(400).json({ success: false, error: 'Поисковый запрос обязателен' });
|
||
}
|
||
|
||
if (!req.organizationId || typeof req.organizationId !== 'number' || isNaN(req.organizationId)) {
|
||
console.error('Invalid organizationId in search users:', req.organizationId);
|
||
return res.status(400).json({ success: false, error: 'Ошибка идентификации организации' });
|
||
}
|
||
|
||
const searchLimit = Math.min(parseInt(limit as string) || 10, 20);
|
||
const users = await storage.searchUsers(query.trim(), req.organizationId, searchLimit);
|
||
|
||
res.json({
|
||
success: true,
|
||
users: users.map(user => ({
|
||
id: user.id,
|
||
firstName: user.firstName,
|
||
lastName: user.lastName,
|
||
email: user.email,
|
||
position: user.position
|
||
}))
|
||
});
|
||
} catch (error) {
|
||
console.error('Search users error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при поиске пользователей' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Unified search for @mentions - returns both users and bots
|
||
router.get('/api/mentions/search',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const { q: query, limit = '10' } = req.query;
|
||
|
||
// Пустой запрос допустим (голый '@' в чате): возвращаем первых пользователей
|
||
// организации и всех доступных ботов
|
||
const trimmedQuery = typeof query === 'string' ? query.trim() : '';
|
||
|
||
if (!req.organizationId || typeof req.organizationId !== 'number') {
|
||
return res.status(400).json({ success: false, error: 'Ошибка идентификации организации' });
|
||
}
|
||
|
||
const searchLimit = Math.min(parseInt(limit as string) || 10, 20);
|
||
const [users, allBots] = await Promise.all([
|
||
storage.searchUsers(trimmedQuery, req.organizationId, searchLimit),
|
||
storage.searchBots(trimmedQuery, req.organizationId, searchLimit)
|
||
]);
|
||
|
||
const currentUser = req.user!;
|
||
const bots = allBots.filter(bot => {
|
||
const policy = bot.accessPolicy;
|
||
if (!policy || policy.mode === 'all') return true;
|
||
if (policy.mode === 'roles') {
|
||
const roles = policy.roles ?? [];
|
||
if (roles.length === 0) return false;
|
||
return roles.includes(currentUser.appRole ?? '');
|
||
}
|
||
if (policy.mode === 'users') {
|
||
const userIds = policy.userIds ?? [];
|
||
if (userIds.length === 0) return false;
|
||
return userIds.includes(currentUser.id);
|
||
}
|
||
return false;
|
||
});
|
||
|
||
const suggestions = [
|
||
...users.map(user => ({
|
||
id: user.id,
|
||
type: 'user' as const,
|
||
name: `${user.firstName} ${user.middleName || ''} ${user.lastName}`.trim(),
|
||
firstName: user.firstName,
|
||
lastName: user.lastName,
|
||
email: user.email,
|
||
position: user.position
|
||
})),
|
||
...bots.map(bot => ({
|
||
id: bot.id,
|
||
type: 'bot' as const,
|
||
name: bot.name,
|
||
firstName: bot.name,
|
||
lastName: '',
|
||
avatarUrl: bot.avatarUrl
|
||
}))
|
||
].slice(0, searchLimit);
|
||
|
||
res.json({ success: true, suggestions });
|
||
} catch (error) {
|
||
console.error('Search mentions error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при поиске' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// === User offline subscriptions ===
|
||
|
||
const subscriptionSchema = z.object({
|
||
formId: z.number().int().positive(),
|
||
isActive: z.boolean().optional(),
|
||
includeRelatedTasks: z.boolean().optional(),
|
||
includeFiles: z.boolean().optional(),
|
||
});
|
||
|
||
async function canManageUserSubscriptions(req: AuthenticatedRequest, targetUserId: number): Promise<boolean> {
|
||
if (req.user!.id === targetUserId) return true;
|
||
return storage.hasAppRolePermission(req.user!.appRole, ['users.manage']);
|
||
}
|
||
|
||
router.get('/api/users/:userId/offline-subscriptions',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const userId = parseInt(req.params.userId);
|
||
if (isNaN(userId)) return res.status(400).json({ success: false, error: 'Неверный ID пользователя' });
|
||
if (!(await canManageUserSubscriptions(req, userId))) {
|
||
return res.status(403).json({ success: false, error: 'Нет прав на просмотр подписок' });
|
||
}
|
||
const subscriptions = await storage.getUserOfflineSubscriptions(userId, req.organizationId!);
|
||
res.json({ success: true, subscriptions });
|
||
} catch (error: any) {
|
||
console.error('Get offline subscriptions error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка загрузки подписок' });
|
||
}
|
||
}
|
||
);
|
||
|
||
router.post('/api/users/:userId/offline-subscriptions',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
validateRequest(subscriptionSchema),
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const userId = parseInt(req.params.userId);
|
||
if (isNaN(userId)) return res.status(400).json({ success: false, error: 'Неверный ID пользователя' });
|
||
if (!(await canManageUserSubscriptions(req, userId))) {
|
||
return res.status(403).json({ success: false, error: 'Нет прав на управление подписками' });
|
||
}
|
||
|
||
const { formId, isActive, includeRelatedTasks, includeFiles } = req.body;
|
||
const existing = await storage.getUserOfflineSubscription(userId, formId);
|
||
const subscription = await storage.upsertUserOfflineSubscription({
|
||
organizationId: req.organizationId!,
|
||
userId,
|
||
formId,
|
||
isActive: isActive ?? existing?.isActive ?? true,
|
||
includeRelatedTasks: includeRelatedTasks ?? existing?.includeRelatedTasks ?? true,
|
||
includeFiles: includeFiles ?? existing?.includeFiles ?? false,
|
||
});
|
||
res.json({ success: true, subscription });
|
||
} catch (error: any) {
|
||
console.error('Upsert offline subscription error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка сохранения подписки' });
|
||
}
|
||
}
|
||
);
|
||
|
||
router.delete('/api/users/:userId/offline-subscriptions/:formId',
|
||
authenticateToken,
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const userId = parseInt(req.params.userId);
|
||
const formId = parseInt(req.params.formId);
|
||
if (isNaN(userId) || isNaN(formId)) {
|
||
return res.status(400).json({ success: false, error: 'Неверные параметры' });
|
||
}
|
||
if (!(await canManageUserSubscriptions(req, userId))) {
|
||
return res.status(403).json({ success: false, error: 'Нет прав на управление подписками' });
|
||
}
|
||
await storage.deleteUserOfflineSubscription(userId, formId);
|
||
res.json({ success: true });
|
||
} catch (error: any) {
|
||
console.error('Delete offline subscription error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка удаления подписки' });
|
||
}
|
||
}
|
||
);
|
||
}
|