Files
iistwin/server/routes/chat.messages.routes.ts

274 lines
12 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 { storage } from "../storage";
import { authenticateToken, authenticateTokenOrApiKey, tryBotServiceToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { tenantIsolation } from "../middleware/tenant.middleware";
import { validateRequest } from "../middleware/validation.middleware";
import { createTaskMessageSchema } from "@shared/schema";
import { eventBus } from "./shared";
import { sendTaskMessage, SendTaskMessageError } from "../services/task-message.service";
import { isFormAllowedByScopes } from "../utils/api-key";
export function registerChatMessageRoutes(router: Router): void {
// Get task messages
router.get('/api/tasks/:id/messages',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const taskId = parseInt(req.params.id);
if (isNaN(taskId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID задачи' });
}
const taskRolesForRead = await storage.getTaskRoles(taskId, req.organizationId!);
if (taskRolesForRead.length > 0) {
const hasAccess = await storage.canUserAccessTask(
taskId, req.user!.id, req.organizationId!, req.user!.appRole
);
if (!hasAccess) {
return res.status(403).json({ success: false, error: 'Нет доступа к этой задаче' });
}
}
const afterId = req.query.afterId ? parseInt(req.query.afterId as string) : undefined;
const messages = await storage.getTaskMessages(taskId, req.organizationId!, afterId, req.user!.id);
res.json({ success: true, messages });
} catch (error) {
console.error('Get task messages error:', error);
res.status(500).json({ success: false, error: 'Ошибка при загрузке сообщений' });
}
}
);
// Create task message (JWT пользователя или API-ключ организации)
router.post('/api/tasks/:id/messages',
tryBotServiceToken,
authenticateTokenOrApiKey,
tenantIsolation,
validateRequest(createTaskMessageSchema.omit({ taskId: true })),
async (req: AuthenticatedRequest, res) => {
try {
const taskId = parseInt(req.params.id);
if (isNaN(taskId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID задачи' });
}
const { message: msgBody, attachments: msgAttachments } = req.body;
if (!msgBody?.trim() && (!msgAttachments || msgAttachments.length === 0)) {
return res.status(400).json({ success: false, error: 'Сообщение или вложения обязательны' });
}
const task = await storage.getTask(taskId, req.organizationId!);
if (!task) {
return res.status(404).json({ success: false, error: 'Задача не найдена' });
}
{
const taskRolesForGuard = await storage.getTaskRoles(taskId, req.organizationId!);
if (req.user && taskRolesForGuard.length > 0) {
const hasAccess = await storage.canUserAccessTask(
taskId, req.user.id, req.organizationId!, req.user.appRole
);
if (!hasAccess) {
return res.status(403).json({ success: false, error: 'Нет доступа к этой задаче' });
}
}
}
// Проверки для авторизации по API-ключу (JWT-путь не меняется)
if (req.apiKey) {
if (req.apiKey.scopes.mode === 'read') {
return res.status(403).json({ success: false, error: 'API-ключ в режиме read не может отправлять сообщения' });
}
if (!isFormAllowedByScopes(req.apiKey.scopes, task.formId)) {
return res.status(403).json({ success: false, error: 'Доступ к форме этой задачи запрещён правами API-ключа' });
}
}
// Автор сообщения: пользователь (JWT); по ключу бота — сам бот;
// по ключу без бота — владелец ключа
let messageUser = req.user ?? null;
let messageBotId: number | null = null;
if (!messageUser && req.apiKey) {
if (req.apiKey.botId) {
messageBotId = req.apiKey.botId;
} else {
messageUser = await storage.getUser(req.apiKey.createdBy);
}
}
if (!messageUser && !messageBotId) {
return res.status(401).json({ success: false, error: 'Не удалось определить автора сообщения' });
}
// Основная логика (сообщение + side-эффекты) вынесена в сервис —
// переиспользуется также MCP-сервером (инструмент send_task_message).
try {
const createdMessage = await sendTaskMessage({
task,
user: messageUser ?? undefined,
botId: messageBotId,
organizationId: req.organizationId!,
message: req.body.message || '',
messageType: req.body.messageType,
replyToMessageId: req.body.replyToMessageId,
mentionedUserIds: req.body.mentionedUserIds,
mentionedBotIds: req.body.mentionedBotIds,
attachments: req.body.attachments,
bodyAuthorId: req.body.authorId,
isBotToken: req.isBotToken === true,
});
res.status(201).json({
success: true,
message: 'Сообщение добавлено',
taskMessage: createdMessage
});
} catch (err) {
if (err instanceof SendTaskMessageError) {
return res.status(err.status).json({ success: false, error: err.message });
}
throw err;
}
} catch (error) {
console.error('Create task message error:', error);
res.status(500).json({ success: false, error: 'Ошибка при создании сообщения' });
}
}
);
// Update task message
router.put('/api/messages/:id',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const messageId = parseInt(req.params.id);
if (isNaN(messageId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID сообщения' });
}
const { message } = req.body;
if (!message || typeof message !== 'string' || message.trim().length === 0) {
return res.status(400).json({ success: false, error: 'Текст сообщения обязателен' });
}
const existingMessage = await storage.getTaskMessage(messageId, req.organizationId!);
if (!existingMessage) {
return res.status(404).json({ success: false, error: 'Сообщение не найдено' });
}
if (existingMessage.authorId !== req.user!.id && req.user!.appRole !== 'admin') {
return res.status(403).json({ success: false, error: 'Нет прав для редактирования этого сообщения' });
}
const updatedMessage = await storage.updateTaskMessage(
messageId, req.organizationId!, { message: message.trim() }
);
res.json({ success: true, message: 'Сообщение обновлено', taskMessage: updatedMessage });
} catch (error) {
console.error('Update task message error:', error);
res.status(500).json({ success: false, error: 'Ошибка при обновлении сообщения' });
}
}
);
// Delete task message
router.delete('/api/messages/:id',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const messageId = parseInt(req.params.id);
if (isNaN(messageId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID сообщения' });
}
const existingMessage = await storage.getTaskMessage(messageId, req.organizationId!);
if (!existingMessage) {
return res.status(404).json({ success: false, error: 'Сообщение не найдено' });
}
if (existingMessage.authorId !== req.user!.id && req.user!.appRole !== 'admin') {
return res.status(403).json({ success: false, error: 'Нет прав для удаления этого сообщения' });
}
await storage.deleteTaskMessage(messageId, req.organizationId!);
res.json({ success: true, message: 'Сообщение удалено' });
} catch (error) {
console.error('Delete task message error:', error);
res.status(500).json({ success: false, error: 'Ошибка при удалении сообщения' });
}
}
);
// Mark message as read
router.post('/api/messages/:id/read',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const messageId = parseInt(req.params.id);
if (isNaN(messageId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID сообщения' });
}
const targetMessage = await storage.getTaskMessage(messageId, req.organizationId!);
if (!targetMessage) {
return res.status(404).json({ success: false, error: 'Сообщение не найдено' });
}
const messageRead = await storage.markMessageAsRead(
messageId, req.user!.id, req.organizationId!
);
if (targetMessage && targetMessage.authorId !== req.user!.id) {
eventBus.publishEvent({
type: 'message_read',
data: {
messageId: messageId,
taskId: targetMessage.taskId,
readerId: req.user!.id,
readerName: `${req.user!.firstName} ${req.user!.middleName || ''} ${req.user!.lastName}`.trim(),
authorId: targetMessage.authorId
},
organizationId: req.organizationId!,
userId: targetMessage.authorId ?? undefined
});
}
res.json({ success: true, message: 'Сообщение отмечено как прочитанное', messageRead });
} catch (error) {
console.error('Mark message as read error:', error);
res.status(500).json({ success: false, error: 'Ошибка при отметке сообщения как прочитанного' });
}
}
);
// Get message read receipts
router.get('/api/messages/:id/reads',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const messageId = parseInt(req.params.id);
if (isNaN(messageId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID сообщения' });
}
const message = await storage.getTaskMessage(messageId, req.organizationId!);
if (!message) {
return res.status(404).json({ success: false, error: 'Сообщение не найдено' });
}
const reads = await storage.getMessageReads(messageId, req.organizationId!);
res.json({ success: true, reads });
} catch (error) {
console.error('Get message reads error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении статуса прочтения' });
}
}
);
}