import { Router } from "express"; import { z } from "zod"; import { db } from "../db"; import { conversations, conversationMembers, conversationMessages, users, orgConnections, bots } from "@shared/schema"; import { eq, and, inArray, desc, sql, or } from "drizzle-orm"; import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware"; import { tenantIsolation } from "../middleware/tenant.middleware"; import { eventBus } from "./shared"; import { isMember, getConversationWithMeta } from "./messenger.helpers"; export function registerMessengerConversationRoutes(router: Router): void { // GET /api/messenger/search router.get("/api/messenger/search", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = req.user!.id; const orgId = req.organizationId!; const q = ((req.query.q as string) ?? "").trim(); if (!q) return res.json({ success: true, people: [], groups: [], external: [], bots: [] }); const lq = `%${q.toLowerCase()}%`; const people = await db .select({ id: users.id, firstName: users.firstName, middleName: users.middleName, lastName: users.lastName, position: users.position }) .from(users) .where(and( eq(users.organizationId, orgId), sql`users.is_active = true`, sql`users.id != ${userId}`, or( sql`lower(users.first_name || ' ' || users.middle_name || ' ' || users.last_name) like ${lq}`, sql`lower(users.last_name || ' ' || users.first_name || ' ' || users.middle_name) like ${lq}`, ), )) .limit(20); const myConvIds = await db .select({ convId: conversationMembers.conversationId }) .from(conversationMembers) .where(eq(conversationMembers.userId, userId)); let groups: { id: number; name: string }[] = []; if (myConvIds.length) { groups = await db .select({ id: conversations.id, name: conversations.name }) .from(conversations) .where(and( inArray(conversations.id, myConvIds.map(r => r.convId)), eq(conversations.type, "group"), sql`lower(conversations.name) like ${lq}`, )) .limit(10) as { id: number; name: string }[]; } const activeConns = await db .select() .from(orgConnections) .where(and( eq(orgConnections.status, "active"), or( eq(orgConnections.initiatorOrgId, orgId), eq(orgConnections.partnerOrgId!, orgId), ), )); let external: { id: number; firstName: string; lastName: string; position?: string | null; organizationId: number; connectionId: number }[] = []; if (activeConns.length) { const partnerOrgIds = activeConns.map(c => c.initiatorOrgId === orgId ? c.partnerOrgId! : c.initiatorOrgId ); const extUsers = await db .select({ id: users.id, firstName: users.firstName, middleName: users.middleName, lastName: users.lastName, position: users.position, organizationId: users.organizationId }) .from(users) .where(and( inArray(users.organizationId, partnerOrgIds), sql`users.is_active = true`, or( sql`lower(users.first_name || ' ' || users.middle_name || ' ' || users.last_name) like ${lq}`, sql`lower(users.last_name || ' ' || users.first_name || ' ' || users.middle_name) like ${lq}`, ), )) .limit(20); external = extUsers.map(u => { const conn = activeConns.find(c => (c.initiatorOrgId === orgId && c.partnerOrgId === u.organizationId) || (c.partnerOrgId === orgId && c.initiatorOrgId === u.organizationId) )!; return { ...u, connectionId: conn.id }; }); } // Search bots accessible to this user const orgBots = await db .select({ id: bots.id, name: bots.name, login: bots.login, avatarUrl: bots.avatarUrl, accessPolicy: bots.accessPolicy }) .from(bots) .where(and( eq(bots.organizationId, orgId), eq(bots.isActive, true), or( sql`lower(${bots.name}) like ${lq}`, sql`lower(${bots.login}) like ${lq}`, ), )) .limit(10); // Filter by access policy using user from request const accessibleBots = orgBots.filter(bot => { const policy = bot.accessPolicy as { mode?: string; roles?: string[]; userIds?: number[] } | null; if (!policy || policy.mode === 'all') return true; if (policy.mode === 'roles') return (policy.roles ?? []).includes(req.user!.appRole); if (policy.mode === 'users') return (policy.userIds ?? []).includes(req.user!.id); return false; }).map(({ accessPolicy: _, ...b }) => b); res.json({ success: true, people, groups, external, bots: accessibleBots }); } catch (err) { console.error("messenger search error:", err); res.status(500).json({ success: false, error: "Ошибка поиска" }); } } ); // GET /api/messenger/conversations router.get("/api/messenger/conversations", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = req.user!.id; const myConvIds = await db .select({ convId: conversationMembers.conversationId }) .from(conversationMembers) .where(eq(conversationMembers.userId, userId)); if (!myConvIds.length) return res.json({ success: true, conversations: [] }); const ids = myConvIds.map(r => r.convId); const result = await Promise.all(ids.map(id => getConversationWithMeta(id, userId))); const sorted = result .filter(Boolean) .sort((a, b) => { const aTime = a!.lastMessage?.createdAt ?? a!.createdAt ?? new Date(0); const bTime = b!.lastMessage?.createdAt ?? b!.createdAt ?? new Date(0); return new Date(bTime).getTime() - new Date(aTime).getTime(); }); res.json({ success: true, conversations: sorted }); } catch (err) { console.error("messenger conversations error:", err); res.status(500).json({ success: false, error: "Ошибка при загрузке диалогов" }); } } ); // GET /api/messenger/unread-count router.get("/api/messenger/unread-count", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = req.user!.id; const myConvIds = await db .select({ convId: conversationMembers.conversationId, lastReadAt: conversationMembers.lastReadAt, reactionUnreadCount: conversationMembers.reactionUnreadCount }) .from(conversationMembers) .where(eq(conversationMembers.userId, userId)); let total = 0; for (const { convId, lastReadAt, reactionUnreadCount } of myConvIds) { const condition = lastReadAt ? and( eq(conversationMessages.conversationId, convId), eq(conversationMessages.isDeleted, false), sql`${conversationMessages.createdAt} > ${lastReadAt}`, sql`(${conversationMessages.authorId} IS NULL OR ${conversationMessages.authorId} != ${userId})`, ) : and( eq(conversationMessages.conversationId, convId), eq(conversationMessages.isDeleted, false), sql`(${conversationMessages.authorId} IS NULL OR ${conversationMessages.authorId} != ${userId})`, ); const [row] = await db .select({ count: sql`count(*)::int` }) .from(conversationMessages) .where(condition); total += (row?.count ?? 0) + (reactionUnreadCount ?? 0); } res.json({ success: true, unreadCount: total }); } catch (err) { console.error("unread-count error:", err); res.status(500).json({ success: false, error: "Ошибка" }); } } ); // POST /api/messenger/conversations — create or find conversation const createConvSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("direct"), targetUserId: z.number().int().positive() }), z.object({ type: z.literal("group"), name: z.string().min(1).max(200), memberIds: z.array(z.number().int().positive()).min(1) }), z.object({ type: z.literal("external_direct"), connectionId: z.number().int().positive(), targetUserId: z.number().int().positive() }), z.object({ type: z.literal("external_group"), connectionId: z.number().int().positive(), name: z.string().min(1).max(200), memberIds: z.array(z.number().int().positive()).min(1) }), z.object({ type: z.literal("bot_direct"), botId: z.number().int().positive() }), ]); router.post("/api/messenger/conversations", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = req.user!.id; const orgId = req.organizationId!; const parsed = createConvSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ success: false, error: "Некорректный запрос", details: parsed.error.issues }); } const body = parsed.data; if (body.type === "direct") { const targetUserId = body.targetUserId; if (targetUserId === userId) { return res.status(400).json({ success: false, error: "Нельзя создать диалог с самим собой" }); } const [targetUser] = await db .select({ id: users.id }) .from(users) .where(and(eq(users.id, targetUserId), eq(users.organizationId, orgId))); if (!targetUser) { return res.status(404).json({ success: false, error: "Пользователь не найден" }); } interface DirectConvRow { conversation_id: number } const existing = await db.execute(sql` SELECT cm1.conversation_id FROM conversation_members cm1 JOIN conversation_members cm2 ON cm1.conversation_id = cm2.conversation_id JOIN conversations c ON c.id = cm1.conversation_id WHERE cm1.user_id = ${userId} AND cm2.user_id = ${targetUserId} AND c.organization_id = ${orgId} AND c.type = 'direct' LIMIT 1 `); if (existing.rows.length > 0) { const existingConvId = (existing.rows[0] as unknown as DirectConvRow).conversation_id; const meta = await getConversationWithMeta(existingConvId, userId); return res.json({ success: true, conversation: meta }); } const [conv] = await db .insert(conversations) .values({ organizationId: orgId, type: "direct", createdBy: userId }) .returning(); await db.insert(conversationMembers).values([ { conversationId: conv.id, userId }, { conversationId: conv.id, userId: targetUserId }, ]); const meta = await getConversationWithMeta(conv.id, userId); const metaForTarget = await getConversationWithMeta(conv.id, targetUserId); eventBus.publishEvent({ type: "conv_created", data: metaForTarget, organizationId: orgId, userId: targetUserId }); return res.status(201).json({ success: true, conversation: meta }); } if (body.type === "group") { const { name, memberIds } = body; const allMemberIds = Array.from(new Set([userId, ...memberIds])); const orgUsers = await db .select({ id: users.id }) .from(users) .where(and(inArray(users.id, allMemberIds), eq(users.organizationId, orgId))); if (orgUsers.length !== allMemberIds.length) { return res.status(400).json({ success: false, error: "Некоторые пользователи не найдены в организации" }); } const [conv] = await db .insert(conversations) .values({ organizationId: orgId, type: "group", name, createdBy: userId }) .returning(); await db.insert(conversationMembers).values( allMemberIds.map(uid => ({ conversationId: conv.id, userId: uid })) ); const meta = await getConversationWithMeta(conv.id, userId); for (const uid of allMemberIds) { if (uid !== userId) { const metaForUid = await getConversationWithMeta(conv.id, uid); eventBus.publishEvent({ type: "conv_created", data: metaForUid, organizationId: orgId, userId: uid }); } } return res.status(201).json({ success: true, conversation: meta }); } if (body.type === "external_direct") { const { connectionId, targetUserId } = body; const [conn] = await db .select() .from(orgConnections) .where(and(eq(orgConnections.id, connectionId), eq(orgConnections.status, "active"))); if (!conn || (conn.initiatorOrgId !== orgId && conn.partnerOrgId !== orgId)) { return res.status(403).json({ success: false, error: "Активное подключение не найдено" }); } const partnerOrgId = conn.initiatorOrgId === orgId ? conn.partnerOrgId! : conn.initiatorOrgId; const [targetUser] = await db .select({ id: users.id, organizationId: users.organizationId }) .from(users) .where(and(eq(users.id, targetUserId), eq(users.organizationId, partnerOrgId))); if (!targetUser) { return res.status(404).json({ success: false, error: "Пользователь партнёрской организации не найден" }); } interface DirectConvRow { conversation_id: number } const existing = await db.execute(sql` SELECT cm1.conversation_id FROM conversation_members cm1 JOIN conversation_members cm2 ON cm1.conversation_id = cm2.conversation_id JOIN conversations c ON c.id = cm1.conversation_id WHERE cm1.user_id = ${userId} AND cm2.user_id = ${targetUserId} AND c.type = 'direct' LIMIT 1 `); if (existing.rows.length > 0) { const existingConvId = (existing.rows[0] as unknown as DirectConvRow).conversation_id; const meta = await getConversationWithMeta(existingConvId, userId); return res.json({ success: true, conversation: meta }); } const [conv] = await db .insert(conversations) .values({ organizationId: orgId, type: "direct", createdBy: userId }) .returning(); await db.insert(conversationMembers).values([ { conversationId: conv.id, userId }, { conversationId: conv.id, userId: targetUserId, externalOrgId: partnerOrgId }, ]); const meta = await getConversationWithMeta(conv.id, userId); const metaForTarget = await getConversationWithMeta(conv.id, targetUserId); eventBus.publishEvent({ type: "conv_created", data: metaForTarget, organizationId: partnerOrgId, userId: targetUserId }); return res.status(201).json({ success: true, conversation: meta }); } if (body.type === "external_group") { const { connectionId, name, memberIds } = body; const [conn] = await db .select() .from(orgConnections) .where(and(eq(orgConnections.id, connectionId), eq(orgConnections.status, "active"))); if (!conn || (conn.initiatorOrgId !== orgId && conn.partnerOrgId !== orgId)) { return res.status(403).json({ success: false, error: "Активное подключение не найдено" }); } const partnerOrgId = conn.initiatorOrgId === orgId ? conn.partnerOrgId! : conn.initiatorOrgId; const localUserIds: number[] = []; const externalUserIds: number[] = []; const allUsers = memberIds.length ? await db.select({ id: users.id, organizationId: users.organizationId }).from(users).where(inArray(users.id, memberIds)) : []; for (const u of allUsers) { if (u.organizationId === orgId) localUserIds.push(u.id); else if (u.organizationId === partnerOrgId) externalUserIds.push(u.id); } if (externalUserIds.length === 0) { return res.status(400).json({ success: false, error: "Добавьте хотя бы одного участника из партнёрской организации" }); } const allLocalIds = Array.from(new Set([userId, ...localUserIds])); const [conv] = await db .insert(conversations) .values({ organizationId: orgId, type: "group", name, createdBy: userId }) .returning(); const memberValues = [ ...allLocalIds.map(uid => ({ conversationId: conv.id, userId: uid })), ...externalUserIds.map(uid => ({ conversationId: conv.id, userId: uid, externalOrgId: partnerOrgId })), ]; await db.insert(conversationMembers).values(memberValues); const meta = await getConversationWithMeta(conv.id, userId); const allParticipants = [...allLocalIds, ...externalUserIds]; for (const uid of allParticipants) { if (uid !== userId) { const metaForUid = await getConversationWithMeta(conv.id, uid); const uidOrgId = externalUserIds.includes(uid) ? partnerOrgId : orgId; eventBus.publishEvent({ type: "conv_created", data: metaForUid, organizationId: uidOrgId, userId: uid }); } } return res.status(201).json({ success: true, conversation: meta }); } if (body.type === "bot_direct") { const { botId } = body; const [bot] = await db .select() .from(bots) .where(and(eq(bots.id, botId), eq(bots.organizationId, orgId), eq(bots.isActive, true))); if (!bot) { return res.status(404).json({ success: false, error: "Бот не найден" }); } // Check access policy const policy = bot.accessPolicy as { mode?: string; roles?: string[]; userIds?: number[] } | null; const hasAccess = !policy || policy.mode === 'all' || (policy.mode === 'roles' && (policy.roles ?? []).includes(req.user!.appRole)) || (policy.mode === 'users' && (policy.userIds ?? []).includes(userId)); if (!hasAccess) { return res.status(403).json({ success: false, error: `Нет доступа к боту ${bot.name}` }); } // Find existing bot_direct conversation for this user+bot const existingRows = await db.execute(sql` SELECT c.id FROM conversations c JOIN conversation_members cm ON cm.conversation_id = c.id WHERE c.type = 'bot_direct' AND c.bot_id = ${botId} AND c.organization_id = ${orgId} AND cm.user_id = ${userId} LIMIT 1 `); if (existingRows.rows.length > 0) { interface BotConvRow { id: number } const existingConvId = (existingRows.rows[0] as unknown as BotConvRow).id; const meta = await getConversationWithMeta(existingConvId, userId); return res.json({ success: true, conversation: meta }); } const [conv] = await db .insert(conversations) .values({ organizationId: orgId, type: "bot_direct", botId, createdBy: userId }) .returning(); await db.insert(conversationMembers).values([{ conversationId: conv.id, userId }]); const meta = await getConversationWithMeta(conv.id, userId); return res.status(201).json({ success: true, conversation: meta }); } return res.status(400).json({ success: false, error: "Некорректный тип" }); } catch (err) { console.error("create conversation error:", err); res.status(500).json({ success: false, error: "Ошибка при создании диалога" }); } } ); // GET /api/messenger/conversations/:id router.get("/api/messenger/conversations/:id", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = req.user!.id; const convId = parseInt(req.params.id); if (isNaN(convId)) return res.status(400).json({ success: false, error: "Некорректный ID" }); if (!await isMember(convId, userId)) { return res.status(403).json({ success: false, error: "Нет доступа к диалогу" }); } const meta = await getConversationWithMeta(convId, userId); if (!meta) return res.status(404).json({ success: false, error: "Диалог не найден" }); res.json({ success: true, conversation: meta }); } catch (err) { console.error("get conversation error:", err); res.status(500).json({ success: false, error: "Ошибка" }); } } ); // PATCH /api/messenger/conversations/:id — rename group chat router.patch("/api/messenger/conversations/:id", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = req.user!.id; const convId = parseInt(req.params.id); if (isNaN(convId)) return res.status(400).json({ success: false, error: "Некорректный ID" }); if (!await isMember(convId, userId)) { return res.status(403).json({ success: false, error: "Нет доступа" }); } const { name } = req.body; if (typeof name !== "string" || !name.trim()) { return res.status(400).json({ success: false, error: "Имя не может быть пустым" }); } await db .update(conversations) .set({ name: name.trim(), updatedAt: new Date() }) .where(eq(conversations.id, convId)); const meta = await getConversationWithMeta(convId, userId); res.json({ success: true, conversation: meta }); } catch (err) { console.error("patch conversation error:", err); res.status(500).json({ success: false, error: "Ошибка" }); } } ); // POST /api/messenger/conversations/:id/members router.post("/api/messenger/conversations/:id/members", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = req.user!.id; const orgId = req.organizationId!; const convId = parseInt(req.params.id); if (isNaN(convId)) return res.status(400).json({ success: false, error: "Некорректный ID" }); if (!await isMember(convId, userId)) { return res.status(403).json({ success: false, error: "Нет доступа" }); } const [conv] = await db .select({ type: conversations.type, organizationId: conversations.organizationId, createdBy: conversations.createdBy, isDefault: conversations.isDefault }) .from(conversations) .where(eq(conversations.id, convId)); if (conv?.type !== "group") { return res.status(400).json({ success: false, error: "Нельзя добавить участника в личный диалог" }); } const isGroupCreator = conv.createdBy === userId; const [reqUser] = await db.select({ appRole: users.appRole }).from(users).where(eq(users.id, userId)); const isOrgAdmin = reqUser?.appRole === "admin"; if (conv.isDefault) { if (!isOrgAdmin) { return res.status(403).json({ success: false, error: "Добавлять участников в эту группу может только администратор организации" }); } } else if (!isGroupCreator && !isOrgAdmin) { return res.status(403).json({ success: false, error: "Добавлять участников может только создатель группы или администратор организации" }); } const { targetUserId } = req.body; if (!targetUserId) return res.status(400).json({ success: false, error: "Укажите пользователя" }); const convHostOrgId = conv.organizationId; const [targetUser] = await db .select({ id: users.id, organizationId: users.organizationId }) .from(users) .where(eq(users.id, targetUserId)); if (!targetUser) return res.status(404).json({ success: false, error: "Пользователь не найден" }); const isExternal = targetUser.organizationId !== convHostOrgId; let externalOrgId: number | null = null; if (isExternal) { const [activeConn] = await db .select({ id: orgConnections.id }) .from(orgConnections) .where(and( eq(orgConnections.status, "active"), or( and(eq(orgConnections.initiatorOrgId, convHostOrgId), eq(orgConnections.partnerOrgId!, targetUser.organizationId)), and(eq(orgConnections.initiatorOrgId, targetUser.organizationId), eq(orgConnections.partnerOrgId!, convHostOrgId)), ) )); if (!activeConn) { return res.status(403).json({ success: false, error: "Нет активного подключения с организацией этого пользователя" }); } externalOrgId = targetUser.organizationId; } else { if (targetUser.organizationId !== convHostOrgId) { return res.status(404).json({ success: false, error: "Пользователь не найден в организации" }); } } await db .insert(conversationMembers) .values({ conversationId: convId, userId: targetUserId, externalOrgId }) .onConflictDoNothing(); const meta = await getConversationWithMeta(convId, userId); const metaForNewMember = await getConversationWithMeta(convId, targetUserId); eventBus.publishEvent({ type: "conv_created", data: metaForNewMember, organizationId: externalOrgId ?? convHostOrgId, userId: targetUserId, }); res.json({ success: true, conversation: meta }); } catch (err) { console.error("add member error:", err); res.status(500).json({ success: false, error: "Ошибка" }); } } ); // PATCH /api/messenger/conversations/:id/mute — toggle mute for current user router.patch("/api/messenger/conversations/:id/mute", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = req.user!.id; const convId = parseInt(req.params.id); if (isNaN(convId)) return res.status(400).json({ success: false, error: "Некорректный ID" }); if (!await isMember(convId, userId)) { return res.status(403).json({ success: false, error: "Нет доступа" }); } const { muted } = req.body; if (typeof muted !== "boolean") { return res.status(400).json({ success: false, error: "Параметр muted должен быть boolean" }); } await db .update(conversationMembers) .set({ mutedAt: muted ? new Date() : null }) .where(and( eq(conversationMembers.conversationId, convId), eq(conversationMembers.userId, userId), )); const meta = await getConversationWithMeta(convId, userId); res.json({ success: true, conversation: meta }); } catch (err) { console.error("mute conversation error:", err); res.status(500).json({ success: false, error: "Ошибка" }); } } ); // DELETE /api/messenger/conversations/:id/members/:memberId router.delete("/api/messenger/conversations/:id/members/:memberId", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = req.user!.id; const convId = parseInt(req.params.id); const targetId = parseInt(req.params.memberId); if (isNaN(convId) || isNaN(targetId)) return res.status(400).json({ success: false, error: "Некорректный ID" }); if (!await isMember(convId, userId)) { return res.status(403).json({ success: false, error: "Нет доступа" }); } const [conv] = await db .select({ createdBy: conversations.createdBy, type: conversations.type }) .from(conversations) .where(eq(conversations.id, convId)); if (!conv) return res.status(404).json({ success: false, error: "Диалог не найден" }); if (conv.type !== "group") return res.status(400).json({ success: false, error: "Нельзя удалить из личного диалога" }); const [reqUser] = await db.select({ appRole: users.appRole }).from(users).where(eq(users.id, userId)); const isOrgAdmin = reqUser?.appRole === "admin"; if (targetId !== userId && conv.createdBy !== userId && !isOrgAdmin) { return res.status(403).json({ success: false, error: "Недостаточно прав" }); } await db .delete(conversationMembers) .where(and( eq(conversationMembers.conversationId, convId), eq(conversationMembers.userId, targetId), )); res.json({ success: true }); } catch (err) { console.error("remove member error:", err); res.status(500).json({ success: false, error: "Ошибка" }); } } ); // DELETE /api/messenger/conversations/:id — delete group conversation router.delete("/api/messenger/conversations/:id", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const userId = req.user!.id; const convId = parseInt(req.params.id); if (isNaN(convId)) return res.status(400).json({ success: false, error: "Некорректный ID" }); if (!await isMember(convId, userId)) { return res.status(403).json({ success: false, error: "Нет доступа" }); } const [conv] = await db .select({ createdBy: conversations.createdBy, type: conversations.type, isDefault: conversations.isDefault }) .from(conversations) .where(eq(conversations.id, convId)); if (!conv) return res.status(404).json({ success: false, error: "Диалог не найден" }); if (conv.type !== "group") return res.status(400).json({ success: false, error: "Нельзя удалить личный диалог" }); if (conv.isDefault) return res.status(403).json({ success: false, error: "Нельзя удалить системную группу" }); const [reqUser] = await db.select({ appRole: users.appRole }).from(users).where(eq(users.id, userId)); const isOrgAdmin = reqUser?.appRole === "admin"; if (conv.createdBy !== userId && !isOrgAdmin) { return res.status(403).json({ success: false, error: "Удалить группу может только создатель или администратор организации" }); } await db.delete(conversations).where(eq(conversations.id, convId)); res.json({ success: true }); } catch (err) { console.error("delete conversation error:", err); res.status(500).json({ success: false, error: "Ошибка" }); } } ); }