import { Router } from "express"; import { z } from "zod"; import crypto from "crypto"; import { db } from "../db"; import { orgConnections, organizations, users } from "@shared/schema"; import { eq, and, or, inArray } from "drizzle-orm"; import { authenticateToken, requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware"; import { tenantIsolation } from "../middleware/tenant.middleware"; const router = Router(); // ── GET /api/org-connections/active — публичный список активных подключений (без токенов) ── // Нужен всем пользователям для создания кросс-орг чатов router.get("/api/org-connections/active", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const rows = await db .select() .from(orgConnections) .where(and( eq(orgConnections.status, "active"), or( eq(orgConnections.initiatorOrgId, orgId), eq(orgConnections.partnerOrgId, orgId), ) )); const orgIds = Array.from(new Set([ ...rows.map(r => r.initiatorOrgId), ...rows.filter(r => r.partnerOrgId).map(r => r.partnerOrgId!), ])); const orgs = orgIds.length ? await db .select({ id: organizations.id, displayName: organizations.displayName }) .from(organizations) .where(inArray(organizations.id, orgIds)) : []; const orgMap = new Map(orgs.map(o => [o.id, o])); const enriched = rows.map(r => ({ id: r.id, status: r.status, isInitiator: r.initiatorOrgId === orgId, initiatorOrg: orgMap.get(r.initiatorOrgId) ?? null, partnerOrg: r.partnerOrgId ? (orgMap.get(r.partnerOrgId) ?? null) : null, acceptedAt: r.acceptedAt, })); res.json({ success: true, connections: enriched }); } catch (err) { console.error("get active org-connections error:", err); res.status(500).json({ success: false, error: "Ошибка при загрузке подключений" }); } } ); // ── GET /api/org-connections — список подключений моей организации (admin only) ── router.get("/api/org-connections", authenticateToken, tenantIsolation, requirePermission('settings.manage'), async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const rows = await db .select() .from(orgConnections) .where(or( eq(orgConnections.initiatorOrgId, orgId), eq(orgConnections.partnerOrgId, orgId), )); // Enrich with org names const orgIds = Array.from(new Set([ ...rows.map(r => r.initiatorOrgId), ...rows.filter(r => r.partnerOrgId).map(r => r.partnerOrgId!), ])); const orgs = orgIds.length ? await db.select({ id: organizations.id, displayName: organizations.displayName, name: organizations.name }) .from(organizations) .where(inArray(organizations.id, orgIds)) : []; const orgMap = new Map(orgs.map(o => [o.id, o])); const enriched = rows.map(r => ({ id: r.id, initiatorOrgId: r.initiatorOrgId, partnerOrgId: r.partnerOrgId, status: r.status, // Expose token only to initiator for pending invites; otherwise mask it token: r.initiatorOrgId === orgId && r.status === "pending" ? r.token : undefined, createdAt: r.createdAt, acceptedAt: r.acceptedAt, initiatorOrg: orgMap.get(r.initiatorOrgId) ?? null, partnerOrg: r.partnerOrgId ? (orgMap.get(r.partnerOrgId) ?? null) : null, isInitiator: r.initiatorOrgId === orgId, })); res.json({ success: true, connections: enriched }); } catch (err) { console.error("get org-connections error:", err); res.status(500).json({ success: false, error: "Ошибка при загрузке подключений" }); } } ); // ── POST /api/org-connections/invite — создать токен приглашения ───────────── router.post("/api/org-connections/invite", authenticateToken, tenantIsolation, requirePermission('settings.manage'), async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const token = crypto.randomBytes(32).toString("hex"); const [conn] = await db .insert(orgConnections) .values({ initiatorOrgId: orgId, status: "pending", token }) .returning(); res.status(201).json({ success: true, connection: conn, token }); } catch (err) { console.error("invite org error:", err); res.status(500).json({ success: false, error: "Ошибка при создании приглашения" }); } } ); // ── POST /api/org-connections/accept — принять приглашение по токену ───────── const acceptSchema = z.object({ token: z.string().min(1) }); router.post("/api/org-connections/accept", authenticateToken, tenantIsolation, requirePermission('settings.manage'), async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const parsed = acceptSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ success: false, error: "Укажите токен приглашения" }); } const { token } = parsed.data; const [conn] = await db .select() .from(orgConnections) .where(eq(orgConnections.token, token)); if (!conn) { return res.status(404).json({ success: false, error: "Приглашение не найдено" }); } if (conn.status !== "pending") { return res.status(400).json({ success: false, error: "Приглашение уже использовано или отозвано" }); } if (conn.initiatorOrgId === orgId) { return res.status(400).json({ success: false, error: "Нельзя принять собственное приглашение" }); } // Атомарное обновление: WHERE id=? AND status='pending' // предотвращает race-condition при параллельных accepts const [updated] = await db .update(orgConnections) .set({ partnerOrgId: orgId, status: "active", acceptedAt: new Date() }) .where(and(eq(orgConnections.id, conn.id), eq(orgConnections.status, "pending"))) .returning(); if (!updated) { return res.status(409).json({ success: false, error: "Приглашение уже принято другим запросом" }); } // Enrich with org info const orgs = await db .select({ id: organizations.id, displayName: organizations.displayName, name: organizations.name }) .from(organizations) .where(inArray(organizations.id, [updated.initiatorOrgId, orgId])); const orgMap = new Map(orgs.map(o => [o.id, o])); res.json({ success: true, connection: { ...updated, initiatorOrg: orgMap.get(updated.initiatorOrgId) ?? null, partnerOrg: orgMap.get(orgId) ?? null, isInitiator: false, }, }); } catch (err) { console.error("accept org-connection error:", err); res.status(500).json({ success: false, error: "Ошибка при принятии приглашения" }); } } ); // ── DELETE /api/org-connections/:id — отозвать/удалить подключение ─────────── router.delete("/api/org-connections/:id", authenticateToken, tenantIsolation, requirePermission('settings.manage'), async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const connId = parseInt(req.params.id); if (isNaN(connId)) return res.status(400).json({ success: false, error: "Некорректный ID" }); const [conn] = await db .select() .from(orgConnections) .where(eq(orgConnections.id, connId)); if (!conn) return res.status(404).json({ success: false, error: "Подключение не найдено" }); if (conn.initiatorOrgId !== orgId && conn.partnerOrgId !== orgId) { return res.status(403).json({ success: false, error: "Нет доступа к этому подключению" }); } await db .update(orgConnections) .set({ status: "revoked" }) .where(eq(orgConnections.id, connId)); res.json({ success: true }); } catch (err) { console.error("delete org-connection error:", err); res.status(500).json({ success: false, error: "Ошибка при отзыве подключения" }); } } ); // ── GET /api/org-connections/:id/users — пользователи партнёрской орг ──────── router.get("/api/org-connections/:id/users", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const connId = parseInt(req.params.id); if (isNaN(connId)) return res.status(400).json({ success: false, error: "Некорректный ID" }); const [conn] = await db .select() .from(orgConnections) .where(and( eq(orgConnections.id, connId), eq(orgConnections.status, "active"), )); if (!conn) return res.status(404).json({ success: false, error: "Активное подключение не найдено" }); if (conn.initiatorOrgId !== orgId && conn.partnerOrgId !== orgId) { return res.status(403).json({ success: false, error: "Нет доступа к этому подключению" }); } const partnerOrgId = conn.initiatorOrgId === orgId ? conn.partnerOrgId! : conn.initiatorOrgId; // Возвращаем только имя и должность — без email const partnerUsers = await db .select({ id: users.id, firstName: users.firstName, lastName: users.lastName, position: users.position, organizationId: users.organizationId, }) .from(users) .where(and( eq(users.organizationId, partnerOrgId), eq(users.isActive, true), )); res.json({ success: true, users: partnerUsers, partnerOrgId }); } catch (err) { console.error("get org-connection users error:", err); res.status(500).json({ success: false, error: "Ошибка при загрузке пользователей" }); } } ); export default router;