Files
iistwin/server/routes/chat.messages.routes.ts
Ильяс Султанов 05e09fa08b MCP: сообщения задач, исполнители, документы, файлы, аудит — этап 2
- list_task_messages, get_task_assignees, list_document_templates, get_task_file, get_task_audit_log (read)
- send_task_message, generate_document (write); set_task_assignees (full)
- Логика POST /api/tasks/:id/messages вынесена в server/services/task-message.service.ts (переиспользуется route и MCP)
- get_task_file: нативный S3 presigned URL или локальный путь с пояснением
2026-07-21 17:19:22 +03:00

247 lines
10 KiB
TypeScript

import { Router } from "express";
import { z } from 'zod';
import { storage } from "../storage";
import { authenticateToken, 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";
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
router.post('/api/tasks/:id/messages',
tryBotServiceToken,
authenticateToken,
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 (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: 'Нет доступа к этой задаче' });
}
}
}
// Основная логика (сообщение + side-эффекты) вынесена в сервис —
// переиспользуется также MCP-сервером (инструмент send_task_message).
try {
const createdMessage = await sendTaskMessage({
task,
user: req.user!,
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: 'Ошибка при получении статуса прочтения' });
}
}
);
}