Files
vidconf/docs/api/admin.md

24 KiB
Raw Blame History

Администраторский API

Эндпоинты управления инстансом VidConf — конференции, пользователи, команды, настройки. Все эндпоинты требуют роль admin (403 иначе).

Авторизация

Все запросы должны включать JWT Bearer токен администратора в заголовке Authorization:

Authorization: Bearer <access_token>

Попытка обращения без роли admin403 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):

{
  "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_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: (опционально)

{
  "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_found
  • 413 — 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_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):

{
  "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_servedtrue, если хотя бы один 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/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, никто их не возьмёт)

Решение: запустить хотя бы один воркер транскрибации:

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), ошибки логируются в воркере

Примечания

  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


Ссылки