fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
596
server/routes/auth.users.routes.ts
Normal file
596
server/routes/auth.users.routes.ts
Normal file
@@ -0,0 +1,596 @@
|
||||
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,
|
||||
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: 'Ошибка при получении пользователей' });
|
||||
}
|
||||
});
|
||||
|
||||
// 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()
|
||||
})),
|
||||
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
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
if (!query || typeof query !== 'string' || query.trim().length === 0) {
|
||||
return res.status(400).json({ success: false, error: 'Поисковый запрос обязателен' });
|
||||
}
|
||||
|
||||
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(query.trim(), req.organizationId, searchLimit),
|
||||
storage.searchBots(query.trim(), 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: error.message || 'Ошибка загрузки подписок' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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: error.message || 'Ошибка сохранения подписки' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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: error.message || 'Ошибка удаления подписки' });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user