Files
iistwin/server/routes/user-profile.routes.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

298 lines
13 KiB
TypeScript

import { Router } from "express";
import { storage } from "../storage";
import { authenticateToken, requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { tenantIsolation } from "../middleware/tenant.middleware";
import { isValidFieldType } from "@shared/field-types";
import { canManageUser } from "../utils/user-access";
export function registerUserProfileRoutes(router: Router): void {
// === USER PROFILE TABS ===
router.get('/api/user-profile/tabs',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tabs = await storage.getUserProfileTabs(req.organizationId!);
res.json({ success: true, tabs });
} catch (error) {
console.error('Get user profile tabs error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении вкладок профиля' });
}
}
);
router.post('/api/user-profile/tabs',
authenticateToken,
tenantIsolation,
requirePermission('users.manage'),
async (req: AuthenticatedRequest, res) => {
try {
const { tabs } = req.body;
if (!Array.isArray(tabs)) {
return res.status(400).json({ success: false, error: 'Вкладки должны быть массивом' });
}
const savedTabs = await storage.bulkSaveUserProfileTabs(req.organizationId!, tabs);
res.json({ success: true, tabs: savedTabs, message: 'Вкладки сохранены' });
} catch (error) {
console.error('Save user profile tabs error:', error);
res.status(500).json({ success: false, error: 'Ошибка при сохранении вкладок профиля' });
}
}
);
// === USER PROFILE FIELDS ===
router.get('/api/user-profile/fields',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const fields = await storage.getUserProfileFields(req.organizationId!);
res.json({ success: true, fields });
} catch (error) {
console.error('Get user profile fields error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении полей профиля' });
}
}
);
router.post('/api/user-profile/fields',
authenticateToken,
tenantIsolation,
requirePermission('users.manage'),
async (req: AuthenticatedRequest, res) => {
try {
const { fields } = req.body;
if (!Array.isArray(fields)) {
return res.status(400).json({ success: false, error: 'Поля должны быть массивом' });
}
const existingFields = await storage.getUserProfileFields(req.organizationId!);
const incomingIds = fields.filter((f: Record<string, unknown>) => f.id).map((f: Record<string, unknown>) => f.id as number);
const fieldsToDelete = existingFields.filter(existing => !incomingIds.includes(existing.id));
// Check if fields to delete are in use
if (fieldsToDelete.length > 0) {
const allUsers = await storage.getUsersByOrganization(req.organizationId!);
const usedFieldIds = new Set<number>();
for (const user of allUsers) {
const values = await storage.getUserProfileFieldValues(user.id);
for (const value of values) {
usedFieldIds.add(value.fieldId);
}
}
for (const field of fieldsToDelete) {
if (usedFieldIds.has(field.id)) {
return res.status(400).json({
success: false,
error: `Невозможно удалить поле "${field.name}" - оно используется в профилях пользователей. Сначала удалите значения этого поля во всех профилях.`
});
}
}
}
const fieldIdsToDelete = fieldsToDelete.map(f => f.id);
const normalizedFields = fields.map((f: Record<string, unknown>) => ({
...f,
maxFileCount: f.maxFileCount != null && parseInt(String(f.maxFileCount)) > 0 ? parseInt(String(f.maxFileCount)) : null,
maxFileSizeMB: f.maxFileSizeMB != null && parseInt(String(f.maxFileSizeMB)) > 0 ? parseInt(String(f.maxFileSizeMB)) : null,
}));
// Validate field types for new fields
for (const field of fields as Array<Record<string, unknown>>) {
const fieldId = field.id as number | undefined;
const isNew = !fieldId || (typeof fieldId === 'number' && fieldId < 0);
const fieldType = field.type as string;
if (isNew && fieldType && !isValidFieldType(fieldType, 'user')) {
return res.status(400).json({
success: false,
error: `Тип поля "${fieldType}" нельзя использовать в профиле пользователя.`,
});
}
}
const resultFields = await storage.bulkSaveUserProfileFields(
req.organizationId!,
fieldIdsToDelete,
normalizedFields as Parameters<typeof storage.bulkSaveUserProfileFields>[2]
);
res.status(201).json({ success: true, message: 'Поля сохранены', fields: resultFields });
} catch (error) {
console.error('Save user profile fields error:', error);
res.status(500).json({ success: false, error: 'Ошибка при сохранении полей профиля' });
}
}
);
// === USER PROFILE DETAIL ===
router.get('/api/users/:id/profile',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const userId = parseInt(req.params.id);
if (isNaN(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 detail = await storage.getUserProfileDetail(userId, req.organizationId!);
const canEdit = await canManageUser(req.user, userId, req.organizationId!) || req.user?.id === userId;
res.json({ success: true, ...detail, canEdit });
} catch (error) {
console.error('Get user profile detail error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении профиля пользователя' });
}
}
);
// === USER PROFILE AUDIT LOG ===
router.get('/api/users/:id/profile/audit-log',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const userId = parseInt(req.params.id);
if (isNaN(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 auditLog = await storage.getUserProfileAuditLog(userId, req.organizationId!);
res.json({ success: true, auditLog });
} catch (error) {
console.error('Get user profile audit log error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении истории изменений' });
}
}
);
// === USER PROFILE FIELD VALUES ===
router.get('/api/users/:id/field-values',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const userId = parseInt(req.params.id);
if (isNaN(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 values = await storage.getUserProfileFieldValues(userId);
res.json({ success: true, fieldValues: values });
} catch (error) {
console.error('Get user profile field values error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении значений полей' });
}
}
);
router.post('/api/users/:id/field-values',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const userId = parseInt(req.params.id);
if (isNaN(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 canEdit = await canManageUser(req.user, userId, req.organizationId!) || req.user?.id === userId;
if (!canEdit) {
return res.status(403).json({ success: false, error: 'Недостаточно прав' });
}
const { fieldValues } = req.body;
if (!Array.isArray(fieldValues)) {
return res.status(400).json({ success: false, error: 'Значения полей должны быть массивом' });
}
const changedBy = req.user!.id;
const createdValues = [];
for (const fv of fieldValues) {
const existing = await storage.getUserProfileFieldValue(userId, fv.fieldId);
if (fv.value === null || fv.value === undefined) {
if (existing) {
await storage.deleteUserProfileFieldValueWithAudit(userId, fv.fieldId, req.organizationId!, changedBy);
}
} else {
if (existing) {
const updated = await storage.updateUserProfileFieldValueWithAudit(userId, fv.fieldId, { value: fv.value }, req.organizationId!, changedBy);
createdValues.push(updated);
} else {
const created = await storage.createUserProfileFieldValueWithAudit({
userId,
fieldId: fv.fieldId,
value: fv.value,
}, req.organizationId!, changedBy);
createdValues.push(created);
}
}
}
res.json({ success: true, fieldValues: createdValues });
} catch (error) {
console.error('Save user profile field values error:', error);
res.status(500).json({ success: false, error: 'Ошибка при сохранении значений полей' });
}
}
);
router.patch('/api/users/:id/field-values/:fieldId',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const userId = parseInt(req.params.id);
const fieldId = parseInt(req.params.fieldId);
if (isNaN(userId) || isNaN(fieldId)) {
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 canEdit = await canManageUser(req.user, userId, req.organizationId!) || req.user?.id === userId;
if (!canEdit) {
return res.status(403).json({ success: false, error: 'Недостаточно прав' });
}
const changedBy = req.user!.id;
const { value } = req.body;
const existing = await storage.getUserProfileFieldValue(userId, fieldId);
if (value === null || value === undefined) {
if (existing) {
await storage.deleteUserProfileFieldValueWithAudit(userId, fieldId, req.organizationId!, changedBy);
}
res.json({ success: true, fieldValue: null });
} else {
if (existing) {
const updated = await storage.updateUserProfileFieldValueWithAudit(userId, fieldId, { value }, req.organizationId!, changedBy);
res.json({ success: true, fieldValue: updated });
} else {
const created = await storage.createUserProfileFieldValueWithAudit({ userId, fieldId, value }, req.organizationId!, changedBy);
res.json({ success: true, fieldValue: created });
}
}
} catch (error) {
console.error('Patch user profile field value error:', error);
res.status(500).json({ success: false, error: 'Ошибка при обновлении значения поля' });
}
}
);
}