Атрибуция от бота в MCP/REST + REST API по ключу (этап 2)

- 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
This commit is contained in:
2026-07-22 15:18:04 +03:00
parent 68153caef2
commit 8cfd49fd9f
13 changed files with 720 additions and 246 deletions

View File

@@ -16,6 +16,7 @@ import { validateRequiredFields } from "../utils/validate-required-fields";
import { shiftRelatedTasks, calculateDateShift } from "../services/gantt-shift.service";
import { runAutomationsByTrigger, type AutomationRunResult } from "./automation.routes";
import { normalizeFieldValueForStorage } from "../utils/normalize-field-value";
import { resolveRestActor, checkApiKeyWriteAccess } from "../utils/api-key-actor";
import { db, withTenant } from "../db";
import { eq, and, sql } from "drizzle-orm";
@@ -51,9 +52,16 @@ export function registerTaskCrudWriteRoutes(router: ReturnType<typeof import("ex
return res.status(404).json({ success: false, error: 'Форма не найдена' });
}
const canAccessForm = await storage.canUserAccessForm(req.user!.id, formId, req.organizationId!, 'participate');
if (!canAccessForm) {
return res.status(403).json({ success: false, error: 'Нет доступа к этой форме' });
if (req.user) {
const canAccessForm = await storage.canUserAccessForm(req.user.id, formId, req.organizationId!, 'participate');
if (!canAccessForm) {
return res.status(403).json({ success: false, error: 'Нет доступа к этой форме' });
}
}
// Проверки для авторизации по API-ключу (JWT-путь не меняется)
const apiKeyErrCreate = checkApiKeyWriteAccess(req, formId);
if (apiKeyErrCreate) {
return res.status(403).json({ success: false, error: apiKeyErrCreate });
}
const { customFields, dueDate, ...baseTaskData } = req.body;
@@ -137,11 +145,17 @@ export function registerTaskCrudWriteRoutes(router: ReturnType<typeof import("ex
}
}
// Автор: пользователь (JWT) или владелец API-ключа
const createdByUserId = req.user?.id ?? req.apiKey?.createdBy;
if (!createdByUserId) {
return res.status(401).json({ success: false, error: 'Не удалось определить автора задачи' });
}
const taskData = {
...baseTaskData,
formId,
organizationId: req.organizationId!,
createdBy: req.user!.id,
createdBy: createdByUserId,
dueDate: parsedDueDate,
parentTaskId,
};
@@ -197,14 +211,15 @@ export function registerTaskCrudWriteRoutes(router: ReturnType<typeof import("ex
}
}
const creatorName = req.user ? (`${req.user.firstName || ''} ${req.user.middleName || ''} ${req.user.lastName || ''}`.trim() || req.user.email) : 'API';
const creatorActor = await resolveRestActor(req, req.organizationId!);
storage.addTaskAuditLog({
taskId: task.id,
organizationId: req.organizationId!,
action: 'task.created',
changedBy: req.user?.id ?? null,
changedByName: creatorName,
metadata: { title: task.title },
changedBy: creatorActor.changedBy,
changedByName: creatorActor.changedByName,
botId: creatorActor.botId,
metadata: { title: task.title, ...(creatorActor.source ? { source: creatorActor.source } : {}) },
}).catch((auditErr: unknown) => { console.error('Audit log error:', auditErr); });
if (customFields && typeof customFields === 'object') {
@@ -307,8 +322,8 @@ export function registerTaskCrudWriteRoutes(router: ReturnType<typeof import("ex
finalTask = await storage.updateTask(task.id, req.organizationId!, { title: computedTitle });
}
if (finalTask.assignedTo && finalTask.assignedTo !== req.user!.id) {
notifyTaskAssigned(finalTask, finalTask.assignedTo, req.user!.id, req.organizationId!)
if (finalTask.assignedTo && finalTask.assignedTo !== req.user?.id) {
notifyTaskAssigned(finalTask, finalTask.assignedTo, req.user?.id ?? null, req.organizationId!)
.catch((err) => console.error('[TaskCreate] notifyTaskAssigned error:', err));
}