- MCP: getActor (владелец ключа + бот), аудит changedByName=бот/label ключа, botId, metadata.source='mcp'; аудит для update_task/update_task_status - sendTaskMessage: botId (messageType 'bot'), починен путь isBotToken без req.user - authenticateTokenOrApiKey: JWT или X-Api-Key с белым списком endpoint'ов (/api/upload, messages, field-values, create task), проверки скоупов, аудит source='api' - Лимиты файлов через env: UPLOAD_IMAGE_MAX_MB=25, UPLOAD_DOC_MAX_MB=100, UPLOAD_MAX_MB=100 - MCP upload-инструменты: параметр fileUrl для привязки уже загруженного файла; новый инструмент get_api_guide
274 lines
12 KiB
TypeScript
274 lines
12 KiB
TypeScript
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);
|
||
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: 'Ошибка при получении статуса прочтения' });
|
||
}
|
||
}
|
||
);
|
||
}
|