instance_settings.media_limits (publish_quality_cap: off/720p/360p/180p, stage_max_tiles: 4/9/16/25) — новая вкладка «Нагрузка» в админке, дефолты (off/25) сохраняют текущее поведение существующих инсталляций. Настройка отдаётся не только GET /admin/settings, но и в join-ответе (JoinOut) — участнику нужно иметь её на руках ДО публикации трека, а /admin/settings доступен только администратору. Потолок качества применяется через RoomOptions.publishDefaults (videoEncoding + videoSimulcastLayers на пресетах VideoPresets LiveKit) — режет битрейт верхнего слоя симулкаста, реальное разрешение WebRTC подстраивает сам. Лимит плиток — фильтрация STAGE_GRID_LAYOUTS по columns*rows в StageGrid, лишние участники уходят на страницу пагинации вместо подписки. Значение приезжает в joinState вместе с токеном ДО первого рендера LiveKitRoom (RoomPage не рендерит его, пока joinState не заполнен целиком), поэтому смена настройки не переподключает уже вошедшего участника — roomOptions пересчитывается по стабильной ссылке на joinState, которая после подключения не меняется. Проверено вживую на локальном стенде (docker compose --profile media): сохранение/персист настроек, join отдаёт актуальные значения, уже подключённый участник не разрывается при смене настройки в другом окне.
583 lines
25 KiB
Markdown
583 lines
25 KiB
Markdown
# Администраторский 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,
|
||
"publish_quality_cap": "off",
|
||
"stage_max_tiles": 25
|
||
}
|
||
```
|
||
|
||
**Поля:**
|
||
- `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` = предупреждение в админке (см. ниже)
|
||
- `publish_quality_cap` — потолок качества исходящего видео публикующего: `off` (без ограничения, дефолт), `720p`, `360p` или `180p`; отдаётся участнику ещё и в join-ответе (`JoinOut`, `POST /api/v1/conferences/{id}/join`/`guest-join`) — до входа в LiveKit-комнату
|
||
- `stage_max_tiles` — максимум одновременно видимых плиток сетки конференции: `4`, `9`, `16` или `25` (дефолт, совпадает с текущим потолком сетки 5×5, то есть без ограничения); участники сверх лимита уходят на следующую страницу пагинации
|
||
|
||
---
|
||
|
||
#### 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 (без админа)
|