Перейти к основному содержимому

GET /api/v1/profile/persons/{personId}/documents/{documentId}/scans/{fileId}/download-link — Presigned-ссылка на скан (getDocumentScanLink)

Назначение

Выдать короткоживущую presigned-ссылку на скачивание скана документа из сервиса storage. Файлы сканов содержат PII и никогда не отдаются через profile напрямую — доступ гейтится этим методом (record-level владельца) и TTL ссылки. Вызывает инвестор из ЛК для скачивания собственного скана.

Metadata

ПолеЗначение
Сервис/контекстprofile
API areapublic
Feature groupdocuments
Статусdraft
Документdocs/06-services/profile/api/public/get-document-scan-link.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (Bearer JWT), UserType = Investor
Auth policy / Permissionsauthenticated; permission profile.documents.view-self
Scope (RBAC)self
Record-levelOwnerUserId = UserId; чужие id → 404
Tenant isolationне применяется (ADR-0052)
Auditread-sensitive: каждая выдача ссылки логируется (UserId, DocumentId, FileId, TTL) — ссылка даёт доступ к PII-файлу

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/profile/persons/{personId}/documents/{documentId}/scans/{fileId}/download-link
Success status200
Idempotency headerне применяется
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
personIdrouteGuidДаNotEmptyчужая → 404
documentIdrouteGuidДаNotEmpty; документ персоны (включая архивный — просмотр истории допустим)чужой → 404
fileIdrouteGuidДаNotEmpty; присутствует в ScanList документаиначе 404

Модель ответа DocumentScanLinkModel

ПолеТипОбязательностьИсточникMaskingОписание
urlstringДаstorage GetDownloadFileLinkне логируется (содержит подпись)Presigned-ссылка на скачивание.
expiresAtDateTimeOffsetДаstorageИстечение ссылки (TTL 5 минут).
fileNamestringДаDocumentScan.FileNameДля сохранения файла клиентом.
contentTypestringДаDocumentScan.ContentTypeMIME из whitelist.

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор (актор User, UserId из токена); tenant не применяется. Требуется право profile.documents.view-self.
  2. Проверка входа. personId/documentId/fileId заданы. Нарушение → ValidationError.
  3. Проверка предусловий. Профиль владельца существует; иначе PROFILE_NOT_FOUND. Тип пользователя — Investor (иначе PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE, INV-PRF-4; чтение допустимо и для TalentSync-инвестора, и для заблокированного — только просмотр собственных данных, мутаций нет). Персона personId принадлежит владельцу; иначе PROFILE_PERSON_NOT_FOUND. Документ documentId принадлежит персоне (включая архивный — история читается); иначе PROFILE_DOCUMENT_NOT_FOUND. Скан fileId присутствует в ScanList документа; иначе PROFILE_SCAN_NOT_FOUND.
  4. Формирование ссылки. У storage запрашивается presigned-ссылка на файл fileId с TTL 5 минут (timeout 5 с, без retry); недоступность storage → PROFILE_STORAGE_UNAVAILABLE. В ответ маппятся url, expiresAt (из storage), fileName, contentType (из метаданных скана).
  5. Результат. Модель DocumentScanLinkModel; HTTP 200. Ссылка одноразового характера по TTL; повторный запрос выдаёт новую. Значение url наружу отдаётся, но в логи не пишется.

Диаграмма

Побочные эффекты

ТипДетали
Внешние вызовыstorage: presigned-ссылка на файл fileId с TTL — timeout 5 с, без retry

Ссылки не кэшируются (короткий TTL); интеграционных событий и уведомлений нет.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
ValidationError400Пустые route-idперечень невалидных полейno retry
PROFILE_NOT_FOUND404Профиль не создан«Профиль ещё создаётся»retry
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403не Investor«Доступно только инвесторам»no retry
PROFILE_PERSON_NOT_FOUND404Нет персоны / чужая«Персона не найдена»no retry
PROFILE_DOCUMENT_NOT_FOUND404Нет документа«Документ не найден»no retry
PROFILE_SCAN_NOT_FOUND404fileId не в ScanList«Скан не найден»no retry
PROFILE_STORAGE_UNAVAILABLE502storage недоступен / файл утрачен«Хранилище временно недоступно»retry

Совместимость и наблюдаемость

  • Контракт storage-границы — внешний сервис; изменение TTL — конфигурация, не контракт.
  • Logs: UserId, DocumentId, FileId, ttl (сам url — никогда); metrics: profile_api_operations_total{operation="getDocumentScanLink",result}, profile_storage_calls_total; traces — OTel.

Acceptance Criteria

  • Given активный документ владельца со сканом fileId, When GET download-link, Then 200; url (presigned, TTL 5 мин), expiresAt, fileName, contentType; факт выдачи логируется (UserId, DocumentId, FileId, TTL), значение url в лог не пишется.
  • Given архивный документ владельца со сканом (просмотр истории), When GET, Then 200 — чтение допустимо.
  • Given fileId отсутствует в ScanList документа, When GET, Then PROFILE_SCAN_NOT_FOUND (404).
  • Given documentId чужой, When GET, Then PROFILE_DOCUMENT_NOT_FOUND (404); чужой personIdPROFILE_PERSON_NOT_FOUND (404) — существование чужих id не раскрывается.
  • Given storage не отвечает за 5 с или файл утрачен, When GET, Then PROFILE_STORAGE_UNAVAILABLE (502), retry.
  • Given заблокированный или TalentSync-инвестор запрашивает собственный скан, When GET, Then 200 (чтение своих данных допустимо, мутаций нет); UserType != InvestorPROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE (403).
  • Given повторный запрос ссылки, When два GET, Then каждый раз выдаётся новая короткоживущая ссылка (TTL 5 мин); ссылки не кэшируются.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы