# Эндпоинты сервиса iams-v2

Документ описывает все HTTP-эндпоинты, которые **предоставляет** сервис `iams` (IAM: пользователи, ресурсы/проекты, права доступа). В отличие от фронтенд-модулей, здесь описан контракт самого сервиса.

## Как устроено взаимодействие

Сервер — Fiber (`internal/controller/http/server.go`). Маршруты регистрируются двумя наборами:

- **Внутренний контур** (`RegisterAPIRoutes`) — без аутентификации на уровне приложения, доступ ограничивается сетевым слоем (не публикуется через ingress). Базовые группы: `/api/v0`, `/api/admin/v0`, `/api/v1`, `/api/v2`.
- **Внешний контур** (`RegisterExternalAPIRoutes`) — регистрируется только при `AUTH_ENABLED=true`. Перед маршрутами выполняется разбор JWT (`AuthMiddleware`) и пометка контекста (`ExternalAPIMiddleware`, для админ-группы дополнительно `ExternalUserAdminAPIMiddleware`). Базовые группы: `/external/api/v0`, `/external/api/admin/v0`, `/external/api/v1`, `/external/api/v2`. Внешний контур публикует **подмножество** внутренних маршрутов, часть — только на чтение.

Глобальные middleware: `requestid`, `recover`, OTel (при `OTEL_ENABLED` и заданном `OTEL_SERVICE_NAME`), логирование.

### Аутентификация (внешний контур)

Токен передаётся заголовком `Authorization: Bearer <jwt>` (`internal/controller/http/jwt/parser.go`). **Подпись токена не проверяется** — доверие делегируется вышестоящему gateway/virtual service. Схема разбора выбирается по заголовку `Identity`:

- заголовок `Identity` отсутствует/пуст → legacy-формат (Django SimpleJWT);
- заголовок `Identity` задан → Zitadel (клейм `urn:zitadel:iam:user:metadata`, значения полей — base64).

### Формат ответов и ошибок

Успешные ответы — JSON (списки: `{ count, results }`; часть эндпоинтов возвращает объект напрямую). Ошибки — JSON `{ "error": "...", "id": "..." }` (`httpx.Response`). Доменный `Kind` маппится на HTTP-статус (`httpx/error_resolve.go`): `InvalidArgument/OutOfRange`→`400`, `Unauthenticated`→`401`, `PermissionDenied`→`403`, `NotFound`→`404`, `Aborted/AlreadyExists`→`409`, `PreconditionFailed`→`412`, `ResourceExhausted`→`429`, `Unavailable`→`503`, `Internal/Unknown/DataLoss`→`500`. Идентификатор запроса — заголовок из `requestid`.

### Пагинация и фильтры

Списки — через query `limit`/`offset` (по умолчанию `limit=1000`). Дополнительные фильтры задаются query-параметрами (напр. для `/resource`: `parent_id`, `type`, `tenant_id`, `name`, `code`, `public_id`, `target_id`, `service_accounts`; для `/users`: `username`, `search`, `is_active`, `is_staff`, `is_superuser`, `service_account_id`, `id`, `company_id`, `departments`, `positions`, `groups`).

## Служебные эндпоинты

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/health` | Health-check: проверка БД и (при включении) Kafka. `200` — ok, `503` — недоступность зависимости |

## Внутренний контур

### Users (`/api/v0/users`, `/api/admin/v0`, `/api/v1`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/users` | Список пользователей (фильтры, пагинация) |
| POST | `/api/v0/users` | Создать пользователя (если включён use case) |
| PATCH | `/api/v0/users` | Массовое обновление пользователей (legacy bulk_update) |
| GET | `/api/v0/users/:id` | Пользователь по id |
| PUT | `/api/v0/users/:id` | Полное обновление пользователя |
| PATCH | `/api/v0/users/:id` | Частичное обновление пользователя |
| DELETE | `/api/v0/users/:id` | Удалить пользователя |
| POST | `/api/admin/v0/users/activation` | Активация/деактивация пользователей компании |
| POST | `/api/v1/users-with-resources` | Пользователи с их ресурсами (тело: `tenant_id`, `id[]`) |
| GET | `/api/v1/users-grouped-by-resource/` | Пользователи, сгруппированные по ресурсу (если включён use case) |

### Permission check (`/api/v0`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| POST | `/api/v0/permissions-check` | Проверка набора прав пользователя (`user_id`, `checks[]`) |

### Groups (`/api/v0/groups`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/groups` | Список групп |
| POST | `/api/v0/groups` | Создать группу |
| POST | `/api/v0/groups/search` | Поиск групп (пагинация, сортировка, фильтры) |
| GET | `/api/v0/groups/:id` | Группа по id |
| PUT | `/api/v0/groups/:id` | Обновить группу |
| PATCH | `/api/v0/groups/:id` | Частично обновить группу |
| DELETE | `/api/v0/groups/:id` | Удалить группу |

### Positions (`/api/v0/positions`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/positions` | Список должностей |
| POST | `/api/v0/positions` | Создать должность |
| GET | `/api/v0/positions/:id` | Должность по id |
| PUT | `/api/v0/positions/:id` | Обновить должность |
| PATCH | `/api/v0/positions/:id` | Частично обновить должность |
| DELETE | `/api/v0/positions/:id` | Удалить должность |

### Departments (`/api/v0/departments`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/departments` | Список подразделений |
| POST | `/api/v0/departments` | Создать подразделение |
| GET | `/api/v0/departments/:id` | Подразделение по id |
| PUT | `/api/v0/departments/:id` | Обновить подразделение |
| PATCH | `/api/v0/departments/:id` | Частично обновить подразделение |
| DELETE | `/api/v0/departments/:id` | Удалить подразделение |

### Django permissions (`/api/v0/permissions`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/permissions` | Список permission'ов |
| POST | `/api/v0/permissions` | Создать permission |
| POST | `/api/v0/permissions/search` | Поиск permission'ов |
| GET | `/api/v0/permissions/:id` | Permission по id |
| DELETE | `/api/v0/permissions/:id` | Удалить permission |

### Resource types v0 (`/api/v0/resource-types`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/resource-types` | Список типов ресурсов |
| POST | `/api/v0/resource-types` | Создать тип |
| GET | `/api/v0/resource-types/:id` | Тип по id |
| PUT | `/api/v0/resource-types/:id` | Обновить тип |
| PATCH | `/api/v0/resource-types/:id` | Частично обновить тип |
| DELETE | `/api/v0/resource-types/:id` | Удалить тип |

### Resources v0 (`/api/v0/resources`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/resources` | Список ресурсов (legacy v0) |
| POST | `/api/v0/resources` | Создать ресурс |
| GET | `/api/v0/resources/:id` | Ресурс по id |
| PUT | `/api/v0/resources/:id` | Обновить ресурс |
| PATCH | `/api/v0/resources/:id` | Частично обновить ресурс |
| DELETE | `/api/v0/resources/:id` | Удалить ресурс |

### Projects v0 (`/api/v0/projects`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/projects` | Список проектов |
| POST | `/api/v0/projects` | Создать проект |
| GET | `/api/v0/projects/:publicID` | Проект по public id |
| PUT | `/api/v0/projects/:publicID` | Обновить проект |
| PATCH | `/api/v0/projects/:publicID` | Частично обновить проект |
| DELETE | `/api/v0/projects/:publicID` | Удалить проект |

### Widgets v0 (`/api/v0/widgets`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v0/widgets` | Список виджетов |
| POST | `/api/v0/widgets` | Создать виджет |
| GET | `/api/v0/widgets/:publicID` | Виджет по public id |
| PUT | `/api/v0/widgets/:publicID` | Обновить виджет |
| PATCH | `/api/v0/widgets/:publicID` | Частично обновить виджет |
| DELETE | `/api/v0/widgets/:publicID` | Удалить виджет |

### Resources v1 (`/api/v1/resource`, `/api/v1/targets`, `/api/v1/service-accounts`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v1/resource` | Список ресурсов (v1) |
| POST | `/api/v1/resource` | Создать ресурс |
| GET | `/api/v1/resource/:publicID` | Ресурс по public id |
| PUT | `/api/v1/resource/:publicID` | Обновить ресурс |
| PATCH | `/api/v1/resource/:publicID` | Частично обновить ресурс |
| DELETE | `/api/v1/resource/:publicID` | Удалить ресурс |
| GET | `/api/v1/targets/:target_id/resource` | Ресурс по target id (только внутренний) |
| GET | `/api/v1/resources-grouped-by-sa` | Ресурсы, сгруппированные по сервисному аккаунту (если включено) |
| GET | `/api/v1/service-accounts` | Список сервисных аккаунтов |

### Widgets v2 (`/api/v2/resource`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v2/resource` | Список виджетов (v2) |
| POST | `/api/v2/resource` | Создать виджет |
| GET | `/api/v2/resource/:publicID` | Виджет по public id |
| PUT | `/api/v2/resource/:publicID` | Обновить виджет |
| PATCH | `/api/v2/resource/:publicID` | Частично обновить виджет |
| DELETE | `/api/v2/resource/:publicID` | Удалить виджет |

### Permissions v1 (`/api/v1`)

| Метод | Путь | Назначение |
| --- | --- | --- |
| GET | `/api/v1/resource_permission` | Список прав на ресурсы |
| POST | `/api/v1/resource_permission` | Создать права (пакет `items[]`) |
| GET | `/api/v1/resource_permission/:id` | Право по id |
| DELETE | `/api/v1/resource_permission/:id` | Удалить право |
| PATCH | `/api/v1/permissions-bulk` | Массовое изменение прав (`tenant_id`, `permissions`) |
| GET | `/api/v1/company_resource_permission` | Список прав компании на ресурсы |
| POST | `/api/v1/company_resource_permission` | Создать права компании (пакет `items[]`) |
| GET | `/api/v1/company_resource_permission/:id` | Право компании по id |
| DELETE | `/api/v1/company_resource_permission/:id` | Удалить право компании |

## Внешний контур (`/external/api/*`, только при `AUTH_ENABLED=true`)

Публикуется подмножество внутренних маршрутов; часть — только на чтение. Пути идентичны внутренним, но с префиксом `/external/api`.

| Группа | Маршруты | Отличия от внутреннего контура |
| --- | --- | --- |
| `/external/api/v0/users` | `GET /`, `GET /:id`, `PATCH /` | Только чтение + массовый PATCH (bulk) |
| `/external/api/admin/v0/users` | `POST /activation`, полный CRUD `/users` | Полный доступ (админ-группа) |
| `/external/api/admin/v0/positions` | `/positions` (CRUD) | Как внутренний |
| `/external/api/admin/v0/departments` | `/departments` (CRUD) | Как внутренний |
| `/external/api/admin/v0/groups` | `/groups` (CRUD + search) | Как внутренний |
| `/external/api/admin/v0/permissions` | `/permissions` (django) | Как внутренний |
| `/external/api/v0/resource-types` | `/resource-types` (CRUD) | Как внутренний |
| `/external/api/v0/resources` | `/resources` (CRUD, v0) | Как внутренний |
| `/external/api/v0/projects` | `GET /`, `GET /:publicID` | Только чтение |
| `/external/api/v0/widgets` | `/widgets` (CRUD) | Как внутренний |
| `/external/api/v1/resource` | `GET /`, `GET /:publicID` | Только чтение |
| `/external/api/v2/resource` | `/resource` (виджеты v2, CRUD) | Как внутренний |
| `/external/api/v1/resource_permission` | CRUD-подмножество | Как внутренний |
| `/external/api/v1/permissions-bulk` | `PATCH /` | Как внутренний |
| `/external/api/v1/company_resource_permission` | CRUD-подмножество | Как внутренний |

> Часть маршрутов регистрируется условно — только если соответствующий хендлер/use case подключён при инициализации сервера (проверки `Has*` в хендлерах). При отсутствии сервиса группа не регистрируется.
