Files
iistwin/server/routes/messenger.conversations.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

678 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<number>`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: "Ошибка" });
}
}
);
}