feat: объединение страниц профиля — /profile рендерит карточку пользователя

- /profile теперь обёртка MyProfilePage над UserProfilePage (один экран для своего и чужого профиля)
- self-вкладки из ProfileSettings перенесены: Уведомления, Offline, Интерфейс, Безопасность (components/profile/*)
- ProfileSettings.tsx удалён
- админ видит вкладки Уведомления и Offline на чужой карточке с данными целевого пользователя
  (userId-параметр в GET/POST /api/subscriptions, PATCH/DELETE по владельцу, GET /api/sync/config?userId=; проверка canManageUser)
- Роль/Активен/Статус disabled для не-админа (сервер их и раньше игнорировал)
- пункт меню «Настройки профиля» переименован в «Мой профиль»
This commit is contained in:
2026-08-05 11:48:07 +03:00
parent 667bddeac9
commit 47869971e6
13 changed files with 1089 additions and 971 deletions

View File

@@ -5,6 +5,7 @@ import { tenantIsolation } from "../middleware/tenant.middleware";
import { validateRequest } from "../middleware/validation.middleware";
import { createSubscriptionSchema, updateSubscriptionSchema } from "@shared/schema";
import { notificationService } from "../services/notification.service";
import { canManageUser } from "../utils/user-access";
export function registerChatNotificationRoutes(router: Router): void {
// Get user notifications
@@ -97,13 +98,27 @@ export function registerChatNotificationRoutes(router: Router): void {
);
// Get user subscriptions
// ?userId=N — подписки другого пользователя (только при canManageUser: админ/руководитель)
router.get('/api/subscriptions',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
let targetUserId = req.user!.id;
if (req.query.userId !== undefined) {
const queryUserId = parseInt(String(req.query.userId));
if (isNaN(queryUserId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID пользователя' });
}
if (queryUserId !== req.user!.id) {
if (!(await canManageUser(req.user, queryUserId, req.organizationId!))) {
return res.status(403).json({ success: false, error: 'Нет прав на просмотр подписок этого пользователя' });
}
targetUserId = queryUserId;
}
}
const subscriptions = await notificationService.getUserSubscriptions(
req.user!.id, req.organizationId!
targetUserId, req.organizationId!
);
res.json({ success: true, subscriptions });
} catch (error) {
@@ -114,14 +129,22 @@ export function registerChatNotificationRoutes(router: Router): void {
);
// Create subscription
// body.userId — создать подписку другому пользователю (только при canManageUser)
router.post('/api/subscriptions',
authenticateToken,
tenantIsolation,
validateRequest(createSubscriptionSchema),
async (req: AuthenticatedRequest, res) => {
try {
let targetUserId = req.user!.id;
if (req.body.userId !== undefined && req.body.userId !== req.user!.id) {
if (!(await canManageUser(req.user, req.body.userId, req.organizationId!))) {
return res.status(403).json({ success: false, error: 'Нет прав на управление подписками этого пользователя' });
}
targetUserId = req.body.userId;
}
const subscription = await notificationService.createSubscription({
userId: req.user!.id,
userId: targetUserId,
organizationId: req.organizationId!,
eventTypeCode: req.body.eventTypeCode,
targetType: req.body.targetType,
@@ -147,8 +170,17 @@ export function registerChatNotificationRoutes(router: Router): void {
if (isNaN(subscriptionId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID подписки' });
}
const existing = await notificationService.getSubscriptionById(subscriptionId, req.organizationId!);
if (!existing) {
return res.status(404).json({ success: false, error: 'Подписка не найдена' });
}
// Владелец подписки или canManageUser над владельцем (админ/руководитель)
if (existing.userId !== req.user!.id &&
!(await canManageUser(req.user, existing.userId, req.organizationId!))) {
return res.status(403).json({ success: false, error: 'Нет прав на изменение этой подписки' });
}
const subscription = await notificationService.updateSubscription(
subscriptionId, req.user!.id, req.organizationId!, req.body
subscriptionId, existing.userId, req.organizationId!, req.body
);
if (!subscription) {
return res.status(404).json({ success: false, error: 'Подписка не найдена' });
@@ -171,7 +203,16 @@ export function registerChatNotificationRoutes(router: Router): void {
if (isNaN(subscriptionId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID подписки' });
}
await notificationService.deleteSubscription(subscriptionId, req.user!.id, req.organizationId!);
const existing = await notificationService.getSubscriptionById(subscriptionId, req.organizationId!);
if (!existing) {
return res.status(404).json({ success: false, error: 'Подписка не найдена' });
}
// Владелец подписки или canManageUser над владельцем (админ/руководитель)
if (existing.userId !== req.user!.id &&
!(await canManageUser(req.user, existing.userId, req.organizationId!))) {
return res.status(403).json({ success: false, error: 'Нет прав на удаление этой подписки' });
}
await notificationService.deleteSubscription(subscriptionId, existing.userId, req.organizationId!);
res.json({ success: true, message: 'Подписка удалена' });
} catch (error) {
console.error('Delete subscription error:', error);

View File

@@ -4,6 +4,7 @@ import { storage } from "../storage";
import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { tenantIsolation } from "../middleware/tenant.middleware";
import { logAudit } from "../utils/audit";
import { canManageUser } from "../utils/user-access";
import { z } from "zod";
const OFFLINE_TASK_LIMIT = 500;
@@ -33,12 +34,26 @@ export function registerSyncRoutes(app: import("express").Express): void {
* - формы с включённым глобальным offlineCache
* - персональные подписки пользователя
* - автоматизации, разрешённые для офлайн-выполнения
* ?userId=N — конфигурация другого пользователя (только при canManageUser: админ/руководитель)
*/
router.get('/config', async (req: AuthenticatedRequest, res) => {
try {
const userId = req.user!.id;
let userId = req.user!.id;
const organizationId = req.organizationId!;
if (req.query.userId !== undefined) {
const queryUserId = parseInt(String(req.query.userId));
if (isNaN(queryUserId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID пользователя' });
}
if (queryUserId !== userId) {
if (!(await canManageUser(req.user, queryUserId, organizationId))) {
return res.status(403).json({ success: false, error: 'Нет прав на просмотр конфигурации этого пользователя' });
}
userId = queryUserId;
}
}
const [globalForms, subscriptions, automations] = await Promise.all([
storage.getFormsWithOfflineCache(organizationId),
storage.getUserOfflineSubscriptions(userId, organizationId),