Files
vidconf/docs/api/admin.md

579 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Администраторский API
Эндпоинты управления инстансом VidConf — конференции, пользователи, команды, настройки. **Все эндпоинты требуют роль `admin` (403 иначе).**
## Авторизация
Все запросы должны включать JWT Bearer токен администратора в заголовке `Authorization`:
```bash
Authorization: Bearer <access_token>
```
Попытка обращения без роли `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 (без админа)