# Администраторский API Эндпоинты управления инстансом VidConf — конференции, пользователи, команды, настройки. **Все эндпоинты требуют роль `admin` (403 иначе).** ## Авторизация Все запросы должны включать JWT Bearer токен администратора в заголовке `Authorization`: ```bash Authorization: Bearer ``` Попытка обращения без роли `admin` → **403 Forbidden** с `detail="Forbidden"`. ## Эндпоинты ### Конференции #### GET /api/v1/admin/conferences **Список всех конференций инстанса с фильтрацией и пагинацией.** **Query параметры:** - `status` (опционально) — фильтр по статусу: `scheduled`, `active`, `ended` - `q` (опционально) — текстовый поиск по названию/номеру/slug - `limit` (опционально, default=50, max=200) — строк на странице - `offset` (опционально, default=0) — смещение (пагинация) **Response (200 OK):** ```json { "items": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "number": "123456789", "slug": "abc123456", "title": "Встреча Q3", "status": "scheduled", "is_pinned": true, "is_closed": false, "scheduled_at": "2026-07-20T14:00:00Z", "duration_minutes": 60, "recurrence": { "type": "weekly", "weekdays": [0, 2, 4] }, "summary_recipients": "all", "created_at": "2026-07-16T12:00:00Z", "owner_name": "Иван Иванов", "owner_email": "ivan@example.com" } ], "total": 42 } ``` **Ключевые поля:** - `owner_name` / `owner_email` — имя и email владельца конференции (null если владельца нет) - `summary_recipients` — текущий режим рассылки (null = дефолт инстанса) --- #### PATCH /api/v1/admin/conferences/{conference_id} **Изменить конференцию (реюз `ConferenceService.update`, те же правила валидации).** **Path параметры:** - `conference_id` (UUID) **Request body:** (все поля опциональны) ```json { "title": "Новое название", "scheduled_at": "2026-07-25T15:00:00Z", "duration_minutes": 90, "is_pinned": true, "recurrence": null, "is_closed": false, "password": null, "summary_recipients": "owner" } ``` **Response (200 OK):** обновленная конференция (как GET /conferences) **Коды ошибок:** - `404` — conference_not_found - `422` — invalid_conference_state (попытка добавить recurrence к неживой конференции и т.п.) **Примечание:** админ проходит проверку владения как "владелец ИЛИ админ" — можно менять чужие конференции. --- #### DELETE /api/v1/admin/conferences/{conference_id} **Удалить конференцию (409 для активной).** **Path параметры:** - `conference_id` (UUID) **Response (204 No Content)** **Коды ошибок:** - `404` — conference_not_found - `409` — conference_active (активную конференцию удалить нельзя) --- #### POST /api/v1/admin/conferences/{conference_id}/invitations **Поставить в очередь ручную рассылку .ics-приглашений (202 Accepted, отправка идёт в Celery).** **Path параметры:** - `conference_id` (UUID) **Request body:** (опционально) ```json { "emails": ["alice@example.com", "bob@example.com"] } ``` **Response (202 Accepted)** — без тела (задача поставлена в очередь) **Логика:** - Если `emails` опущена/null — получатели по умолчанию: владелец конференции + для закреплённых — участники прошлых сеансов - Если `emails` передан — явный список адресов - Каждое приглашение добавляется в `email_deliveries` с `kind='invitation'` (журнал без unique, дубликаты допускаются) **Коды ошибок:** - `404` — conference_not_found --- ### Пользователи #### GET /api/v1/admin/users **Список всех пользователей инстанса с пагинацией.** **Query параметры:** - `q` (опционально) — текстовый поиск по email/имени - `limit` (опционально, default=50, max=200) - `offset` (опционально, default=0) **Response (200 OK):** ```json { "items": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "admin@example.com", "name_user": "Администратор", "role": "admin", "email_verified": true, "is_blocked": false, "team_id": null, "created_at": "2026-07-16T12:00:00Z" } ], "total": 15 } ``` --- #### POST /api/v1/admin/users **Создать пользователя от имени администратора.** Email сразу считается подтверждённым (письмо на верификацию не отправляется); роль по умолчанию — `user`. Дубль email → **409** `email_already_registered`. Несуществующая команда → **404** `team_not_found`. **Request body:** ```json { "name_user": "Иван Петров", "email": "ivan@example.com", "password": "securePassword123", "team_id": "550e8400-e29b-41d4-a716-446655440001" } ``` **Поля:** - `name_user` (str, 1..255) — отображаемое имя (обязательно) - `email` (EmailStr) — адрес электронной почты (обязательно, уникален) - `password` (str, ≥8) — пароль пользователя (политика как при регистрации) - `team_id` (UUID или null) — опциональная привязка к команде (не обязана быть выбрана, в отличие от публичной регистрации) **Response (201 Created):** ```json { "id": "550e8400-e29b-41d4-a716-446655440002", "email": "ivan@example.com", "name_user": "Иван Петров", "role": "user", "is_blocked": false, "email_verified": true, "created_at": "2026-07-21T10:30:00Z", "team_id": "550e8400-e29b-41d4-a716-446655440001", "avatar_url": null, "team_name": "Backend" } ``` **Коды ошибок:** - `409` — email_already_registered (email уже зарегистрирован) - `404` — team_not_found (передан несуществующий `team_id`) - `422` — Unprocessable Entity (пароль короче 8 символов, валидация email и т.д.) **Примечание:** В отличие от самостоятельной регистрации (`POST /auth/register`), пользователь не проходит верификацию email — роль назначается как `user`, а `email_verified` сразу `true`. --- #### GET /api/v1/admin/users/{user_id} **Карточка профиля пользователя — те же данные, что в пользовательском профиле + роль и статус блокировки.** **Path параметры:** - `user_id` (UUID) **Response (200 OK):** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "user@example.com", "name_user": "Иван Петров", "role": "user", "is_blocked": false, "team_id": "550e8400-e29b-41d4-a716-446655440001", "team_name": "Backend", "avatar_path": "avatars/550e8400-e29b-41d4-a716-446655440000.jpg", "avatar_url": "/media/avatars/550e8400-e29b-41d4-a716-446655440000.jpg", "created_at": "2026-07-18T12:00:00Z" } ``` **Поля:** - `avatar_path` — путь к файлу аватара (относительно `MEDIA_ROOT`), `null` если нет - `avatar_url` — готовый URL для отображения (`/media/avatars/...`) - `team_name` — имя команды (null если не привязан) **Коды ошибок:** - `404` — user_not_found --- #### PATCH /api/v1/admin/users/{user_id} **Изменить роль, статус блокировки, ФИО, команду пользователя.** **Path параметры:** - `user_id` (UUID) **Request body:** (все поля опциональны) ```json { "role": "admin", "is_blocked": true, "name_user": "Новое имя", "team_id": "550e8400-e29b-41d4-a716-446655440000" } ``` **Response (200 OK):** обновленный пользователь (как GET /admin/users/{id}) **Коды ошибок:** - `404` — user_not_found (пользователь) или team_not_found (несуществующий `team_id`) - `409` — cannot_modify_self (попытка изменить свою `role`/`is_blocked`; своё `name_user` и `team_id` менять можно) **Семантика:** - `is_blocked=true` — пользователь получит 401 на любом защищённом эндпоинте (проверка в `api/deps.py::_user_from_token`), немедленно, без ожидания истечения access-токена - `role` может быть `"admin"` или `"user"` - `name_user` — новое ФИО пользователя - `team_id` — явное `null` снимает привязку к команде; отсутствие поля в теле запроса команду не трогает (различается через `model_fields_set`); запрет самоизменения (409) на `team_id` **не распространяется** --- #### POST /api/v1/admin/users/{user_id}/avatar **Загрузить аватар пользователю.** **Path параметры:** - `user_id` (UUID) **Request body:** `multipart/form-data` - `file` — файл изображения (обязателен) **Требования:** - Формат: JPEG, PNG или WebP (проверяется по magic bytes) - Размер: максимум 2 МБ **Response (200 OK):** обновленный пользователь (как GET /admin/users/{id}, с новым `avatar_url`) **Коды ошибок:** - `404` — user_not_found - `413` — avatar_too_large (файл больше 2 МБ) - `415` — avatar_invalid_type (не JPEG/PNG/WebP) **Примечание:** Старый аватар автоматически удаляется при загрузке нового. --- ### Команды #### GET /api/v1/admin/teams **Список всех команд, отсортированный по названию.** **Response (200 OK):** ```json { "items": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Backend", "created_at": "2026-07-18T12:00:00Z" } ], "total": 1 } ``` --- #### POST /api/v1/admin/teams **Создать команду.** **Request body:** ```json { "name": "Backend" } ``` `name` обрезается от пробелов (1..255 символов после обрезки). **Response (201 Created):** созданная команда **Коды ошибок:** - `409` — team_name_taken (команда с таким названием уже существует) --- #### PATCH /api/v1/admin/teams/{team_id} **Переименовать команду.** **Path параметры:** - `team_id` (UUID) **Request body:** ```json { "name": "Backend & Platform" } ``` **Response (200 OK):** обновлённая команда **Коды ошибок:** - `404` — team_not_found - `409` — team_name_taken --- #### DELETE /api/v1/admin/teams/{team_id} **Удалить команду.** **Path параметры:** - `team_id` (UUID) **Response (204 No Content)** У пользователей, состоявших в удалённой команде, `team_id` автоматически обнуляется (`ON DELETE SET NULL`) — сами пользователи не удаляются. **Коды ошибок:** - `404` — team_not_found --- ### Настройки инстанса #### GET /api/v1/admin/settings **Текущие эффективные настройки инстанса (собрань из БД + дефолты).** **Response (200 OK):** ```json { "chat_enabled": false, "transcription_enabled": true, "ai_level": "min", "ai_levels": [ {"level": "min", "available": true, "reason": null}, {"level": "medium", "available": false, "reason": "Недостаточно памяти"}, {"level": "max", "available": false, "reason": "Нет GPU"} ], "summary_recipients": "all", "display_timezone": "Europe/Moscow", "registration_team_choice": false, "registration_email_domain_enabled": false, "registration_email_domain": null, "transcription_queue_served": true } ``` **Поля:** - `chat_enabled` — включен ли чат в конференциях - `transcription_enabled` — включены ли транскрибация и суммаризация (единый переключатель) - `ai_level` — текущий уровень AI (`min`, `medium`, `max`) - `ai_levels` — доступность всех уровней с причинами (например, `medium` может быть недоступен при недостатке памяти, `max` требует GPU) - `summary_recipients` — режим рассылки саммри по умолчанию (`all` или `owner`) - `display_timezone` — таймзона отображения времени в письмах и .ics (IANA, например `Europe/Moscow`) - `registration_team_choice` — разрешён ли выбор команды на публичной форме регистрации (дефолт `false`); см. `GET /api/v1/auth/registration-options` - `registration_email_domain_enabled` — включена ли верификация регистрирующихся по домену email (дефолт `false`) - `registration_email_domain` — эталонный домен email (нормализован: без ведущего `@`, в нижнем регистре); `null`, пока верификация не настроена - `transcription_queue_served` — `true`, если хотя бы один Celery-воркер `transcriber` активно обслуживает очередь транскрибации; `false` = предупреждение в админке (см. ниже) --- #### PUT /api/v1/admin/settings **Частично обновить настройки инстанса (недоступный уровень AI/неверная таймзона → 400).** **Request body:** (все поля опциональны, PATCH-семантика) ```json { "chat_enabled": true, "transcription_enabled": false, "ai_level": "min", "summary_recipients": "owner", "display_timezone": "Europe/London", "registration_team_choice": true, "registration_email_domain_enabled": true, "registration_email_domain": "example.com" } ``` **Response (200 OK):** обновленные настройки (как GET /settings) **Коды ошибок:** - `400` — invalid AI level (попытка установить недоступный уровень, например `medium` без нужного железа), неверная таймзона (не IANA) или некорректная настройка верификации домена email (включение без домена либо домен не проходит валидацию паттерном) **Логика:** - `chat_enabled`: пишет `enabled` в хранилище настроек (`instance_settings.chat`) - `transcription_enabled`: пишет `enabled` сразу в обе секции (`transcriber`, `summarizer`) — единый переключатель; **guard:** если установлено `true`, но `transcription_queue_served=false` в ответе `GET /settings`, админка отображает предупреждение «Очередь транскрибации не обслуживается ни одним Celery-воркером» (воркеры не запущены, обслуживают другую очередь или недоступны) - `ai_level`: проверяется `detect_ai_levels()` на доступность перед сохранением - `display_timezone`: валидируется как IANA (через `ZoneInfo`) - `summary_recipients`: может быть `"all"` или `"owner"` (дефолт для новых конференций) - `registration_team_choice`: включает/выключает выбор команды на публичной форме регистрации (`instance_settings.registration_team_choice`); влияет на `GET /api/v1/auth/registration-options` и допустимость `team_id` в `POST /api/v1/auth/register` - `registration_email_domain_enabled` / `registration_email_domain`: включают верификацию регистрирующихся по домену email (`instance_settings.registration_email_domain`); включение (`enabled=true`) при пустом/отсутствующем домене → `400`; домен нормализуется (`strip`, без ведущей `@`, нижний регистр) и валидируется простым паттерном доменного имени, иначе `400`; влияет на `GET /api/v1/auth/registration-options` (`email_domain`) и `POST /api/v1/auth/register` (`400 invalid_email_domain` при несовпадении домена) **Примечание:** Изменение настроек **не требует рестарта** ни backend'а, ни воркеров Celery (они читают конфиг на старте каждой задачи). ### Предупреждение об очереди транскрибации При включённой транскрибации (`transcription_enabled=true`) админка проверяет доступность очереди через поле `transcription_queue_served` в ответе `GET /api/v1/admin/settings`: - **`transcription_queue_served=true`:** хотя бы один Celery-воркер `transcriber` активен и обслуживает очередь — новые конференции будут обработаны - **`transcription_queue_served=false`:** ни один воркер не подключён к очереди — **все** сеансы конференций по завершении зависнут в статусе `transcribing` (задачи накопятся в Redis, никто их не возьмёт) **Решение:** запустить хотя бы один воркер транскрибации: ```bash docker compose up -d transcriber # или cd workers && celery -A transcriber worker -q ``` После запуска воркера поле `transcription_queue_served` обновится на `true` (проверяется при каждом запросе к `/api/v1/admin/settings`). --- ## Примеры запросов ### Получить все конференции с фильтром по статусу ```bash curl -X GET \ 'http://localhost:8000/api/v1/admin/conferences?status=scheduled&limit=20&offset=0' \ -H "Authorization: Bearer $TOKEN" ``` ### Изменить режим рассылки саммри конференции ```bash curl -X PATCH \ 'http://localhost:8000/api/v1/admin/conferences/550e8400-e29b-41d4-a716-446655440000' \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"summary_recipients": "owner"}' ``` ### Поставить ручную рассылку приглашений ```bash curl -X POST \ 'http://localhost:8000/api/v1/admin/conferences/550e8400-e29b-41d4-a716-446655440000/invitations' \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"emails": ["alice@example.com", "bob@example.com"]}' ``` ### Создать пользователя ```bash curl -X POST \ 'http://localhost:8000/api/v1/admin/users' \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name_user": "Иван Петров", "email": "ivan@example.com", "password": "securePassword123", "team_id": "550e8400-e29b-41d4-a716-446655440001" }' ``` ### Заблокировать пользователя ```bash curl -X PATCH \ 'http://localhost:8000/api/v1/admin/users/550e8400-e29b-41d4-a716-446655440000' \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"is_blocked": true}' ``` ### Назначить пользователю команду ```bash curl -X PATCH \ 'http://localhost:8000/api/v1/admin/users/550e8400-e29b-41d4-a716-446655440000' \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"team_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"}' ``` ### Изменить настройки инстанса ```bash curl -X PUT \ 'http://localhost:8000/api/v1/admin/settings' \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "chat_enabled": true, "ai_level": "min", "display_timezone": "Europe/London", "summary_recipients": "all" }' ``` --- ## Безопасность и ограничения - **Только для админов:** все эндпоинты блокируют 403 для роли `user` - **Самоизменение роли/блокировки запрещено:** PATCH собственных `role`/`is_blocked` → 409 (`cannot_modify_self`); свою команду (`team_id`) администратор менять может - **Секреты SMTP не видны:** почтовые реквизиты хранятся только в `.env`, в API не попадают - **Блокировка немедленна:** `is_blocked=true` действует на любом защищённом эндпоинте без ожидания истечения токена - **Рассылка в очереди:** отправка писем идёт асинхронно через Celery (202, не 200), ошибки логируются в воркере --- ## Примечания 1. **Изменение расписания конференции** (PATCH `scheduled_at`/`duration_minutes`/`recurrence`/`title`) инкрементирует `ics_sequence` и ставит в очередь `send_invitations` — календарные клиенты получат обновление по UID события. 2. **Переопределение рассылки саммри** (`summary_recipients` в конференции) сильнее дефолта инстанса — если в конференции установлено, оно используется; иначе берётся настройка из `instance_settings.summary_recipients`. 3. **AI-уровни:** три уровня качества `min`/`medium`/`max` (Qwen3.5-4B/9B/35B-A3B, faster-whisper small/medium/large-v3). Доступность уровня определяется детектом железа (HW_CPUS/HW_RAM_MB/HW_GPU_NAME/HW_VRAM_MB в `.env`). Недоступный уровень → 400 Bad Request с объяснением. ADR-004: [docs/architecture/adr/004-ai-tier-matrix.md](../architecture/adr/004-ai-tier-matrix.md) --- ## Ссылки - [Конфигурация инстанса](../architecture/README.md#Компоненты) — общее описание системы - [Схема БД](../db/schema.md) — таблицы `instance_settings`, `email_deliveries` - [API конференций](./conferences.md) — пользовательский API (без админа)