fix(files): бот может скачивать файлы — API-ключ на /api/files и /uploads + трекинг абсолютных URL

Проблема: прямое скачивание /api/files/<key> с X-Api-Key давало 403 (файловые
эндпоинты принимали только JWT), а get_task_file отвечал «не отслеживается» —
77 файлов со старыми АБСОЛЮТНЫМИ URL (https://iistwin.ru/api/files/...) не
попадали в file_uploads: startup-backfill понимал только относительные ссылки.

- tryPresignedOrAuth: ветка X-Api-Key — resolve ключа, req.apiKey/organizationId,
  tenant-контекст; владение проверяет canAccessFile, скоупы форм — новый
  checkApiKeyFileScope (файл привязан к задаче → форма должна быть разрешена ключом);
- /api/files/:key/presigned теперь тоже через tryPresignedOrAuth (боты могут
  выпускать presigned-ссылки);
- server/utils/file-tracking.ts: trackFileOnDemand — догрузка по требованию из
  task_field_values/task_messages по ссылке любого вида; используется в
  canAccessFile и MCP get_task_file;
- startup-backfill: паттерны покрывают абсолютные URL (substring FROM regex).
This commit is contained in:
2026-09-29 10:36:41 +03:00
parent 2ac9da7220
commit 4bac21ff71
3 changed files with 218 additions and 18 deletions

View File

@@ -39,6 +39,7 @@ import type { Request, Response } from "express";
import type { Task, ApiKeyScopes, OrganizationApiKey, SafeUser, Bot } from "@shared/schema";
import { normalizeApiKeyScopes, isFormAllowedByScopes } from "./utils/api-key";
import { createUploadTicket } from "./utils/upload-tickets";
import { trackFileOnDemand } from "./utils/file-tracking";
import beautify from "js-beautify";
import {
semanticSearch,
@@ -4252,12 +4253,24 @@ To block task creation from task.before_create, set: ctx.result = { allow: false
if (!task) return mcpError(`Задача ${taskId} не найдена`);
if (!isFormAllowed(task.formId)) return formDenied(task.formId);
// Файл должен отслеживаться в file_uploads и принадлежать организации (как canAccessFile в index.ts)
const [upload] = await db
// Файл должен отслеживаться в file_uploads и принадлежать организации (как canAccessFile в index.ts).
// Если не отслеживается — догружаем по требованию (старые абсолютные URL и прочие
// неохваченные startup-backfill случаи, см. utils/file-tracking.ts).
let [upload] = await db
.select()
.from(fileUploads)
.where(eq(fileUploads.fileKey, fileKey))
.limit(1);
if (!upload) {
const tracked = await trackFileOnDemand(fileKey);
if (tracked) {
[upload] = await db
.select()
.from(fileUploads)
.where(eq(fileUploads.fileKey, fileKey))
.limit(1);
}
}
if (!upload) return mcpError("Файл не найден или не отслеживается");
if (upload.organizationId !== organizationId) {
return mcpError("Файл принадлежит другой организации");