openapi: 3.0.3 info: title: Notes Service API version: "0.0.1" description: | REST API сервиса **notes-backend** (`aero/notes-backend`) — управление заметками к сущностям (workspace), их ссылками, документами и вложениями, а также генерацией документов через workflow. Сервис написан на Python (**FastAPI**, pydantic v1). Приложение собирается фабрикой `get_app` в `src/app/main.py`. Публичный роутинг подключается с префиксом `/api/v1` (`src/app/routers/__init__.py`) и включает группы `notes`, `links`, `documents`, `attachments`. Опционально (при `ENABLE_ND=true`) подключается роутер `nd_service` с префиксом `/api/v1/nd` (`src/app/nd_service/router.py`). ### Аутентификация Аутентификация выполняется middleware `DjangoUserMiddleware` (`src/app/middleware.py`). При `DJANGO_USE=true` каждый запрос (кроме путей, содержащих `/nd`) должен содержать заголовок `Authorization` — токен проверяется обращением к Django `/client/settings/`. Опционально передаётся заголовок `Identity` (режим Zitadel). Если заголовок `Authorization` отсутствует — возвращается `401`. При `DJANGO_USE=false` middleware подставляет тестового пользователя- администратора (использовать только локально). Дополнительно, права проверяются зависимостью `PermissionManager`: для не-админов метод сопоставляется с правом (`base.can_add_note` для POST, `base.can_view_note` для GET, `base.can_change_note` для PUT/PATCH, `base.can_delete_note` для DELETE); при отсутствии права — `403`. Часть эндпоинтов НД (`/api/v1/nd/nd_proxy/*` на запись) защищена JWT (`JWTBearer`, включается флагом `ND_JWT_ENABLE`). ### Замечания (расхождения кода) - Ошибки валидации тела/параметров (Pydantic) отдаются FastAPI в стандартном формате `422`. - Подключение к БД всегда идёт с `sslmode=verify-full` независимо от значения `PG_SSL_MODE`. - Многие пути завершаются слэшем (`/api/v1/notes/{id}/`). contact: name: notes-backend url: https://gitlab/aero/notes-backend servers: - url: https://api.sarex.io/notes description: Production (ingress, BASE_HOST) - url: https://api.preprod.sarex.io/notes description: Preprod (ingress, BASE_HOST) - url: https://stage-api.sarex.io/notes description: Stage (ingress, BASE_HOST) - url: http://notes-backend-service:8000 description: Внутрикластерный адрес (ClusterIP) - url: http://localhost:8000 description: Локальный запуск (gunicorn/docker, порт 8000) tags: - name: notes description: Заметки — создание, просмотр, обновление, поиск, документы и вложения - name: links description: Ссылки, привязанные к заметкам - name: documents description: Документы, привязанные к заметкам - name: attachments description: Вложения заметок - name: nd_service description: Проксирование НД (подключается при ENABLE_ND=true) security: - bearerAuth: [] paths: /api/v1/notes/: post: tags: [notes] summary: Создать заметку requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BaseNote' responses: '201': description: Создано content: application/json: schema: $ref: '#/components/schemas/Note' '400': description: Некорректный company_id get: tags: [notes] summary: Список заметок parameters: - { name: search, in: query, schema: { type: string } } - { name: limit, in: query, schema: { type: integer, default: 1000, minimum: 0 } } - { name: offset, in: query, schema: { type: integer, default: 0, minimum: 0 } } - { name: created_from, in: query, schema: { type: string, format: date-time } } - { name: created_to, in: query, schema: { type: string, format: date-time } } - { name: time_start, in: query, schema: { type: string, format: date-time } } - { name: time_end, in: query, schema: { type: string, format: date-time } } - { name: author, in: query, schema: { type: string } } - { name: description, in: query, schema: { type: boolean } } - { name: document, in: query, schema: { type: boolean } } - { name: resource_id, in: query, description: "Список UUID через запятую", schema: { type: string } } responses: '200': description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/Note' /api/v1/notes/{service}/{entity}/{instance_id}/: get: tags: [notes] summary: Заметки по сервису/сущности/инстансу parameters: - { name: service, in: path, required: true, schema: { $ref: '#/components/schemas/Service' } } - { name: entity, in: path, required: true, schema: { $ref: '#/components/schemas/Entity' } } - { name: instance_id, in: path, required: true, schema: { type: string } } - { name: full, in: query, description: "Вернуть расширенные заметки (ExtendedNote)", schema: { type: boolean, default: false } } - { name: search, in: query, schema: { type: string } } - { name: limit, in: query, schema: { type: integer, default: 1000 } } - { name: offset, in: query, schema: { type: integer, default: 0 } } responses: '200': description: OK content: application/json: schema: type: array items: oneOf: - $ref: '#/components/schemas/Note' - $ref: '#/components/schemas/ExtendedNote' /api/v1/notes/{instance_id}/: get: tags: [notes] summary: Заметка по id parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Note' } '404': description: Не найдено put: tags: [notes] summary: Обновить заметку parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/BaseNote' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Note' } '400': { description: Некорректный company_id } '404': { description: Не найдено } delete: tags: [notes] summary: Удалить заметку parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } responses: '200': description: Удалено (возвращает удалённый объект) content: application/json: schema: { $ref: '#/components/schemas/Note' } '404': { description: Не найдено } /api/v1/notes/{instance_id}/documents/: post: tags: [notes] summary: Добавить документы к заметке parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: array items: { $ref: '#/components/schemas/BaseDocument' } responses: '201': description: Создано content: application/json: schema: type: array items: { $ref: '#/components/schemas/Document' } '404': { description: Заметка не найдена } /api/v1/notes/{instance_id}/attachments/: post: tags: [notes] summary: Загрузить файлы-вложения к заметке parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } requestBody: required: true content: multipart/form-data: schema: type: object properties: files: type: array items: { type: string, format: binary } responses: '200': description: OK content: application/json: schema: type: array items: type: object properties: name: { type: string } link: { type: string } id: { type: integer } attachment_type: { type: string } '404': { description: Заметка не найдена } get: tags: [notes] summary: Вложения заметки parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } responses: '200': description: OK content: application/json: schema: { type: array, items: { type: object } } '404': { description: Заметка не найдена } /api/v1/notes/{instance_id}/bound_attachments/: post: tags: [notes] summary: Привязать существующее вложение к заметке parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/BaseAttachment' } responses: '201': description: Создано content: application/json: schema: { $ref: '#/components/schemas/Attachment' } '404': { description: Заметка не найдена } /api/v1/notes/{instance_id}/generate_document/: post: tags: [notes] summary: Сгенерировать документ по заметке (через workflow) parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/DocumentCreation' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/WFResponse' } '404': { description: Заметка не найдена } /api/v1/links/: post: tags: [links] summary: Создать ссылку requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/BaseLink' } responses: '201': description: Создано content: application/json: schema: { $ref: '#/components/schemas/Link' } '400': { description: Некорректный note_id } get: tags: [links] summary: Список ссылок parameters: - { name: note, in: query, description: "Фильтр по note_id", schema: { type: integer } } - { name: limit, in: query, schema: { type: integer, default: 1000 } } - { name: offset, in: query, schema: { type: integer, default: 0 } } responses: '200': description: OK content: application/json: schema: { type: array, items: { $ref: '#/components/schemas/Link' } } /api/v1/links/{instance_id}/: get: tags: [links] summary: Ссылка по id parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Link' } '404': { description: Не найдено } put: tags: [links] summary: Обновить ссылку parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/BaseLink' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Link' } '404': { description: Не найдено } delete: tags: [links] summary: Удалить ссылку parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } responses: '200': description: Удалено content: application/json: schema: { $ref: '#/components/schemas/Link' } '404': { description: Не найдено } /api/v1/documents/{instance_id}/: delete: tags: [documents] summary: Удалить документ заметки parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } responses: '200': description: Удалено content: application/json: schema: { $ref: '#/components/schemas/Document' } '404': { description: Не найдено } /api/v1/attachments/{instance_id}/: delete: tags: [attachments] summary: Удалить вложение parameters: - { name: instance_id, in: path, required: true, description: "attachment_id", schema: { type: integer } } responses: '200': description: Удалено content: application/json: schema: { $ref: '#/components/schemas/Attachment' } '404': { description: Не найдено } /api/v1/nd/nd_proxy/: post: tags: [nd_service] summary: Создать НД-прокси security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/NDProxyCreate' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/NDProxySchema' } '400': { description: Нарушение целостности } get: tags: [nd_service] summary: Список НД-прокси security: [{ bearerAuth: [] }] parameters: - { name: is_bound, in: query, schema: { type: boolean } } - { name: limit, in: query, schema: { type: integer, default: 1000 } } - { name: offset, in: query, schema: { type: integer, default: 0 } } responses: '200': description: OK content: application/json: schema: { type: array, items: { $ref: '#/components/schemas/NDProxySchema' } } /api/v1/nd/nd_proxy/{instance_id}/: get: tags: [nd_service] summary: НД-прокси по коду parameters: - { name: instance_id, in: path, required: true, description: "nd_code", schema: { type: string } } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/NDProxySchema' } '404': { description: Не найдено } put: tags: [nd_service] summary: Обновить НД-прокси parameters: - { name: instance_id, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/NDProxyCreate' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/NDProxySchema' } '404': { description: Не найдено } patch: tags: [nd_service] summary: Частично обновить НД-прокси parameters: - { name: instance_id, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/NDProxyUpdate' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/NDProxySchema' } '404': { description: Не найдено } delete: tags: [nd_service] summary: Удалить НД-прокси parameters: - { name: instance_id, in: path, required: true, schema: { type: string } } responses: '200': description: Удалено content: application/json: schema: { $ref: '#/components/schemas/NDProxySchema' } '404': { description: Не найдено } /api/v1/nd/nd_proxy/{instance_id}/bound/: post: tags: [nd_service] summary: Привязать заметку и файл к НД-прокси parameters: - { name: instance_id, in: path, required: true, description: "nd_code", schema: { type: string } } requestBody: required: true content: multipart/form-data: schema: type: object properties: note_id: { type: integer } name: { type: string } file: { type: string, format: binary } responses: '200': description: OK '404': { description: Не найдено } /api/v1/nd/notes/{instance_id}/: get: tags: [nd_service] summary: Заметка НД с изображением parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } responses: '200': description: OK content: application/json: schema: { type: object } '404': { description: Не найдено } patch: tags: [nd_service] summary: Обновить время заметки (НД) parameters: - { name: instance_id, in: path, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/NDUpdateTimeNote' } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/Note' } '404': { description: Не найдено } /api/v1/nd/users/: post: tags: [nd_service] summary: Проверить существование пользователя по username requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/User' } responses: '200': description: OK content: application/json: schema: type: object properties: exist: { type: boolean } '400': { description: Ошибка запроса к Django } /api/v1/nd/token/: get: tags: [nd_service] summary: Сгенерировать JWT для НД-сервиса responses: '200': description: OK content: application/json: schema: type: object properties: token: { type: string } components: securitySchemes: bearerAuth: type: http scheme: bearer description: | Заголовок `Authorization` (проверяется через Django `/client/settings/`). Опционально заголовок `Identity` для режима Zitadel. Эндпоинты `/api/v1/nd/*` на запись используют собственный JWT (`JWTBearer`, флаг ND_JWT_ENABLE). schemas: Service: type: string enum: [workspace] Entity: type: string enum: [workspace] BaseNote: type: object required: [name, service, entity, instance_id, company_id] properties: name: { type: string } body: { type: string, nullable: true } time_start: { type: string, format: date-time, nullable: true } time_end: { type: string, format: date-time, nullable: true } service: { $ref: '#/components/schemas/Service' } entity: { $ref: '#/components/schemas/Entity' } instance_id: { type: string } meta_data: { type: object, nullable: true } height_base_plane: { type: number, format: float, nullable: true } height_geom_prmtv: { type: number, format: float, nullable: true } created_by: { type: integer, nullable: true, minimum: 1 } company_id: { type: integer, minimum: 1 } resource_id: { type: string, format: uuid, nullable: true } Note: allOf: - $ref: '#/components/schemas/BaseNote' - type: object required: [id, created_at, updated_at] properties: id: { type: integer } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } ExtendedNote: allOf: - $ref: '#/components/schemas/Note' - type: object properties: links: type: array items: { type: integer } documents: type: array items: { $ref: '#/components/schemas/Document' } DocumentCreation: type: object required: [path, values, file_name, template] properties: path: { type: string } values: { type: object, additionalProperties: true } file_name: { type: string } template: { type: string } WFResponse: type: object required: [context] properties: context: { type: object } BaseLink: type: object required: [link_id, note_id] properties: link_id: { type: integer, minimum: 1 } note_id: { type: integer, minimum: 1 } Link: allOf: - $ref: '#/components/schemas/BaseLink' - type: object required: [id] properties: id: { type: integer } BaseDocument: type: object required: [document_id, bundle_id] properties: document_id: { type: integer } bundle_id: { type: string } note_id: { type: integer, nullable: true } Document: allOf: - $ref: '#/components/schemas/BaseDocument' - type: object required: [id] properties: id: { type: integer } BaseAttachment: type: object required: [attachment_id, object_name, attachment_type, is_state] properties: attachment_id: { type: integer, minimum: 1 } note_id: { type: integer, nullable: true, minimum: 1 } object_name: { type: string } attachment_type: { type: string } is_state: { type: boolean } Attachment: allOf: - $ref: '#/components/schemas/BaseAttachment' - type: object required: [id] properties: id: { type: integer } NDProxyCreate: type: object required: [nd_code, creator, is_locked] properties: nd_code: { type: string } work_description: { type: string, nullable: true } equipment_description: { type: string, nullable: true } description: { type: string, nullable: true } creator: { type: string } is_locked: { type: boolean } note_id: { type: integer, nullable: true } NDProxyUpdate: type: object properties: nd_code: { type: string, nullable: true } work_description: { type: string, nullable: true } equipment_description: { type: string, nullable: true } description: { type: string, nullable: true } creator: { type: string, nullable: true } is_locked: { type: boolean, nullable: true } note_id: { type: integer, nullable: true } NDProxySchema: allOf: - $ref: '#/components/schemas/NDProxyCreate' - type: object required: [id] properties: id: { type: integer } NDUpdateTimeNote: type: object properties: time_start: { type: string, format: date-time, nullable: true } time_end: { type: string, format: date-time, nullable: true } User: type: object required: [username] properties: username: { type: string }