Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

578
docs/api/admin.md Normal file
View File

@@ -0,0 +1,578 @@
# Администраторский 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 (без админа)