24 KiB
Администраторский API
Эндпоинты управления инстансом VidConf — конференции, пользователи, команды, настройки. Все эндпоинты требуют роль admin (403 иначе).
Авторизация
Все запросы должны включать JWT Bearer токен администратора в заголовке Authorization:
Authorization: Bearer <access_token>
Попытка обращения без роли admin → 403 Forbidden с detail="Forbidden".
Эндпоинты
Конференции
GET /api/v1/admin/conferences
Список всех конференций инстанса с фильтрацией и пагинацией.
Query параметры:
status(опционально) — фильтр по статусу:scheduled,active,endedq(опционально) — текстовый поиск по названию/номеру/sluglimit(опционально, default=50, max=200) — строк на страницеoffset(опционально, default=0) — смещение (пагинация)
Response (200 OK):
{
"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: (все поля опциональны)
{
"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_found422— invalid_conference_state (попытка добавить recurrence к неживой конференции и т.п.)
Примечание: админ проходит проверку владения как "владелец ИЛИ админ" — можно менять чужие конференции.
DELETE /api/v1/admin/conferences/{conference_id}
Удалить конференцию (409 для активной).
Path параметры:
conference_id(UUID)
Response (204 No Content)
Коды ошибок:
404— conference_not_found409— conference_active (активную конференцию удалить нельзя)
POST /api/v1/admin/conferences/{conference_id}/invitations
Поставить в очередь ручную рассылку .ics-приглашений (202 Accepted, отправка идёт в Celery).
Path параметры:
conference_id(UUID)
Request body: (опционально)
{
"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):
{
"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:
{
"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):
{
"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):
{
"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: (все поля опциональны)
{
"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_found413— avatar_too_large (файл больше 2 МБ)415— avatar_invalid_type (не JPEG/PNG/WebP)
Примечание: Старый аватар автоматически удаляется при загрузке нового.
Команды
GET /api/v1/admin/teams
Список всех команд, отсортированный по названию.
Response (200 OK):
{
"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:
{ "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:
{ "name": "Backend & Platform" }
Response (200 OK): обновлённая команда
Коды ошибок:
404— team_not_found409— 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):
{
"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-optionsregistration_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-семантика)
{
"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/registerregistration_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, никто их не возьмёт)
Решение: запустить хотя бы один воркер транскрибации:
docker compose up -d transcriber
# или
cd workers && celery -A transcriber worker -q
После запуска воркера поле transcription_queue_served обновится на true
(проверяется при каждом запросе к /api/v1/admin/settings).
Примеры запросов
Получить все конференции с фильтром по статусу
curl -X GET \
'http://localhost:8000/api/v1/admin/conferences?status=scheduled&limit=20&offset=0' \
-H "Authorization: Bearer $TOKEN"
Изменить режим рассылки саммри конференции
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"}'
Поставить ручную рассылку приглашений
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"]}'
Создать пользователя
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"
}'
Заблокировать пользователя
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}'
Назначить пользователю команду
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"}'
Изменить настройки инстанса
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), ошибки логируются в воркере
Примечания
-
Изменение расписания конференции (PATCH
scheduled_at/duration_minutes/recurrence/title) инкрементируетics_sequenceи ставит в очередьsend_invitations— календарные клиенты получат обновление по UID события. -
Переопределение рассылки саммри (
summary_recipientsв конференции) сильнее дефолта инстанса — если в конференции установлено, оно используется; иначе берётся настройка изinstance_settings.summary_recipients. -
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
Ссылки
- Конфигурация инстанса — общее описание системы
- Схема БД — таблицы
instance_settings,email_deliveries - API конференций — пользовательский API (без админа)