- 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 или локальный путь с пояснением
247 lines
10 KiB
TypeScript
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: 'Ошибка при получении статуса прочтения' });
|
|
}
|
|
}
|
|
);
|
|
}
|