meraproject/openspec/changes/archive/2026-07-14-service-project-members/design.md
keboss-m a7f3df859a Add service API for assigning employees to project teams.
Expose PUT /api/service/project-members for external apps that authenticate with only the API key, reusing portal validation while keeping acting-based writes unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 15:30:59 +03:00

6.1 KiB
Raw Permalink Blame History

Context

User-reader уже умеет upsert участника проекта через PUT /api/project-members (project_members_write.py): валидация раздела/роли/сотрудника и запись в tMerakomisTeamMember. Авторизация завязана на X-Acting-Emp-Id и can_write_project_member (паритет PHP Rules::isRwMember).

Внешнее приложение знает project_id, emp_id, section_id, role и имеет только service API-ключ. Impersonation РП/ГИП для него неудобна и нежелательна.

Справочник разделов проекта (GET /api/project-sections) уже отдаёт section_id; отдельного поля «раздел + подраздел» в модели нет — в TeamMember хранится один section.

Goals / Non-Goals

Goals:

  • Дать внешним сервисам upsert участника проекта только по API-ключу.
  • Переиспользовать существующую бизнес-валидацию и upsert-логику без дублирования SQL.
  • Явно отделить service-контракт от портального acting-потока (отдельный URL).
  • Зафиксировать контракт в docs и тестах.

Non-Goals:

  • Создание/редактирование сотрудника в tMerakomisEmp.
  • Изменение поведения PUT /api/project-members (acting остаётся обязательным).
  • Расширение GET /api/project-sections (parent / full_name) — не требуется для этого сценария.
  • Отдельный API-ключ или scopes на эндпоинт (пока один USER_READER_API_KEY).
  • Запись разделов проекта (tMerakomisProjectSection) или создание команды проекта.

Decisions

1. Отдельный путь PUT /api/service/project-members (вариант B)

Выбор: новый URL под префиксом /api/service/…, а не опциональный acting на существующем PUT.

Почему:

  • Контракт для интеграций явный: «сервис доверяет ключу», не «человек с правами».
  • Не ослабляем портальный эндпоинт случайным вызовом без acting.
  • Проще документировать и аудировать.

Альтернатива (A): тот же PUT, acting опционален → отвергнута: смешивает модели доверия.

2. Переиспользование ProjectMemberWrite + validate_member_write + upsert_team_member

Выбор: тот же JSON-контракт и те же helper-функции; новый handler только пропускает acting/permissions.

Почему: один источник правил (раздел ∈ проекта, роль валидна, emp не архивный, у проекта есть team).

Реализация: тонкий роут в новом модуле (например project_members_service.py) или второй handler в project_members_write.py с Depends(require_api_key) без parse_acting_emp_id. Предпочтительно отдельный файл/роутер, чтобы граница service vs portal была видна в main.py.

3. Авторизация = только API-ключ

Выбор: при включённом USER_READER_API_KEY — обязателен ключ; при пустом ключе (локальная отладка) — как у остальных защищённых эндпоинтов. Проверки isRwMember нет.

Почему: соответствует заявленному сценарию внешнего приложения. Риск компенсируется контролем ключа на стороне деплоя.

4. Ответ и ошибки

Выбор: тот же shape, что у portal PUT: ok: true + поля item GET /api/project-members. Те же коды валидации (400/404), без 403 forbidden из-за прав acting (acting нет). 401 при неверном/отсутствующем ключе.

5. Документация

Обновить матрицу доступа и раздел записи в docs/user-reader-api.md (+ краткая строка в DEVELOPERS.md). В сценариях интеграции указать: внешний сервис → service PUT; UI/табель от имени сотрудника → portal PUT.

Risks / Trade-offs

Risk Mitigation
Утечка API-ключа = полный write состава любых проектов Ключ только на сервере; ротация; в будущем — scopes / отдельный ключ для write
Клиенты путают portal и service PUT Разные URL + явная секция в docs; portal по-прежнему требует Acting
Дублирование логики при копировании кода Обязательно вызывать существующие validate/upsert, не копировать SQL
Нет audit «кто добавил» (нет acting) Принять для v1; при необходимости позже писать в History с маркером service

Migration Plan

  1. Задеплоить user-reader с новым роутом (обратно совместимо).
  2. Внешнее приложение переключить на PUT /api/service/project-members.
  3. Rollback: убрать роут / откатить образ — portal PUT не затронут.

Open Questions

  • Нужен ли позже audit/history для service-записей? (вне scope v1)
  • Нужен ли отдельный write-ключ или scope? (вне scope v1; один общий ключ)