Атрибуция от бота в 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:
@@ -1,7 +1,8 @@
|
||||
import { Router } from "express";
|
||||
import { storage } from "../storage";
|
||||
import { authenticateToken, requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware";
|
||||
import { authenticateToken, authenticateTokenOrApiKey, requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware";
|
||||
import { tenantIsolation } from "../middleware/tenant.middleware";
|
||||
import { resolveRestActor, checkApiKeyWriteAccess } from "../utils/api-key-actor";
|
||||
import { buildSystemFieldValues, buildTableRowMap, deleteRemovedFiles, eventBus, formatFieldValueForTitle, resolveTaskFieldTitles } from "./shared";
|
||||
import { evaluateAutoTransitions } from "../utils/auto-transitions";
|
||||
import { tasksMinimalCache } from "../utils/cache";
|
||||
@@ -63,7 +64,7 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
|
||||
|
||||
// Create/Update task field values (bulk)
|
||||
router.post('/api/tasks/:id/field-values',
|
||||
authenticateToken,
|
||||
authenticateTokenOrApiKey,
|
||||
tenantIsolation,
|
||||
async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
@@ -77,14 +78,19 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
|
||||
return res.status(404).json({ success: false, error: 'Задача не найдена' });
|
||||
}
|
||||
|
||||
{
|
||||
if (req.user) {
|
||||
const hasAccess = await storage.canUserAccessTask(
|
||||
taskId, req.user!.id, req.organizationId!, req.user!.appRole
|
||||
taskId, req.user.id, req.organizationId!, req.user.appRole
|
||||
);
|
||||
if (!hasAccess) {
|
||||
return res.status(403).json({ success: false, error: 'Нет доступа к этой задаче' });
|
||||
}
|
||||
}
|
||||
// Проверки для авторизации по API-ключу (JWT-путь не меняется)
|
||||
const apiKeyErrPost = checkApiKeyWriteAccess(req, existingTask.formId);
|
||||
if (apiKeyErrPost) {
|
||||
return res.status(403).json({ success: false, error: apiKeyErrPost });
|
||||
}
|
||||
|
||||
const { fieldValues } = req.body;
|
||||
if (!Array.isArray(fieldValues)) {
|
||||
@@ -226,7 +232,7 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
|
||||
}
|
||||
}
|
||||
|
||||
const fieldEditorName = req.user ? (`${req.user.firstName || ''} ${req.user.middleName || ''} ${req.user.lastName || ''}`.trim() || req.user.email) : 'API';
|
||||
const fieldActor = await resolveRestActor(req, req.organizationId!);
|
||||
const offlineEnqueuedAtPost = parseOfflineTimestamp(req);
|
||||
for (const fieldValue of fieldValues) {
|
||||
const field = fieldMap.get(fieldValue.fieldId);
|
||||
@@ -247,11 +253,14 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
|
||||
fieldName: field.name,
|
||||
oldValue: oldVal ?? null,
|
||||
newValue: newVal ?? null,
|
||||
changedBy: req.user?.id ?? null,
|
||||
changedByName: fieldEditorName,
|
||||
changedBy: fieldActor.changedBy,
|
||||
changedByName: fieldActor.changedByName,
|
||||
botId: fieldActor.botId,
|
||||
metadata: {
|
||||
displayOldValue: displayOld || null,
|
||||
displayNewValue: displayNew || null,
|
||||
// Атрибуция канала: 'api' при изменении по API-ключу
|
||||
...(fieldActor.source ? { source: fieldActor.source } : {}),
|
||||
},
|
||||
...(offlineEnqueuedAtPost ? { createdAt: offlineEnqueuedAtPost } : {}),
|
||||
}).catch((auditErr: unknown) => { console.error('Audit log error:', auditErr); });
|
||||
@@ -365,7 +374,7 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
|
||||
|
||||
// Update single field value (inline editing)
|
||||
router.patch('/api/tasks/:id/field-values/:fieldId',
|
||||
authenticateToken,
|
||||
authenticateTokenOrApiKey,
|
||||
tenantIsolation,
|
||||
async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
@@ -380,14 +389,19 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
|
||||
return res.status(404).json({ success: false, error: 'Задача не найдена' });
|
||||
}
|
||||
|
||||
{
|
||||
if (req.user) {
|
||||
const hasAccess = await storage.canUserAccessTask(
|
||||
taskId, req.user!.id, req.organizationId!, req.user!.appRole
|
||||
taskId, req.user.id, req.organizationId!, req.user.appRole
|
||||
);
|
||||
if (!hasAccess) {
|
||||
return res.status(403).json({ success: false, error: 'Нет доступа к этой задаче' });
|
||||
}
|
||||
}
|
||||
// Проверки для авторизации по API-ключу (JWT-путь не меняется)
|
||||
const apiKeyErrPatch = checkApiKeyWriteAccess(req, existingTask.formId);
|
||||
if (apiKeyErrPatch) {
|
||||
return res.status(403).json({ success: false, error: apiKeyErrPatch });
|
||||
}
|
||||
|
||||
const { value } = req.body;
|
||||
|
||||
@@ -473,9 +487,7 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
|
||||
});
|
||||
}
|
||||
|
||||
const editorName = req.user
|
||||
? (`${req.user.firstName || ''} ${req.user.middleName || ''} ${req.user.lastName || ''}`.trim() || req.user.email)
|
||||
: 'API';
|
||||
const patchActor = await resolveRestActor(req, req.organizationId!);
|
||||
const offlineEnqueuedAtPatch = parseOfflineTimestamp(req);
|
||||
const oldStr = oldVal === null || oldVal === undefined ? '' : String(oldVal);
|
||||
const newStr = normalizedValue === null || normalizedValue === undefined ? '' : String(normalizedValue);
|
||||
@@ -488,8 +500,10 @@ export function registerTaskFieldRoutes(router: ReturnType<typeof import("expres
|
||||
fieldName: field.name,
|
||||
oldValue: oldVal ?? null,
|
||||
newValue: normalizedValue ?? null,
|
||||
changedBy: req.user?.id ?? null,
|
||||
changedByName: editorName,
|
||||
changedBy: patchActor.changedBy,
|
||||
changedByName: patchActor.changedByName,
|
||||
botId: patchActor.botId,
|
||||
...(patchActor.source ? { metadata: { source: patchActor.source } } : {}),
|
||||
...(offlineEnqueuedAtPatch ? { createdAt: offlineEnqueuedAtPatch } : {}),
|
||||
}).catch((auditErr: unknown) => { console.error('Audit log error (inline edit):', auditErr); });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user