Первоначальная версия 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

260
frontend/src/api/admin.ts Normal file
View File

@@ -0,0 +1,260 @@
/**
* API-клиент админ-эндпоинтов (`/api/v1/admin/*`).
* Списковые DTO (`AdminConferenceOut`/`AdminUserOut`) отдаются в стандартном
* конверте пагинации `items`/`total`.
*/
import { apiRequest } from '@/api/client'
import type { ConferenceRecurrence, ConferenceStatus, SummaryRecipientsMode } from '@/api/conferences'
/** Уровень качества AI-обработки (транскрибация + суммаризация). */
export type AiLevel = 'min' | 'medium' | 'max'
/** Доступность одного уровня AI с причиной отказа (если недоступен). */
export interface AiLevelStatus {
level: AiLevel
available: boolean
reason: string | null
}
/** Эффективные настройки инстанса. */
export interface SettingsOut {
chat_enabled: boolean
/** Единый переключатель модуля AI (транскрибация + суммаризация). */
transcription_enabled: boolean
/** Есть ли хотя бы один Celery-воркер, обслуживающий очередь транскрибации. */
transcription_queue_served: boolean
ai_level: AiLevel
ai_levels: AiLevelStatus[]
summary_recipients: SummaryRecipientsMode
/** IANA-таймзона отображения времени в письмах/.ics (напр. `Europe/Moscow`). */
display_timezone: string
/** Разрешён ли выбор команды на экране регистрации. */
registration_team_choice: boolean
/** Включена ли верификация регистрации по домену корпоративной почты. */
registration_email_domain_enabled: boolean
/** Эталонный домен для верификации (напр. `company.ru`) — `null`, если верификация выключена. */
registration_email_domain: string | null
}
/** Тело частичного обновления настроек инстанса — все поля опциональны. */
export interface SettingsUpdateIn {
chat_enabled?: boolean
transcription_enabled?: boolean
/** Недоступный уровень (см. `ai_levels`) — backend отвечает 400. */
ai_level?: AiLevel
summary_recipients?: SummaryRecipientsMode
display_timezone?: string
registration_team_choice?: boolean
/** Включение без домена или невалидный домен — backend отвечает 400. */
registration_email_domain_enabled?: boolean
registration_email_domain?: string | null
}
/** Конференция в ответе админ-списка — `ConferenceOut` + сведения о владельце. */
export interface AdminConferenceOut {
id: string
number: string
slug: string
title: string | null
status: ConferenceStatus
is_pinned: boolean
is_closed: boolean
scheduled_at: string | null
duration_minutes: number | null
recurrence: ConferenceRecurrence | null
next_occurrence: string | null
created_at: string
summary_recipients: SummaryRecipientsMode | null
owner_name: string | null
owner_email: string | null
}
/** Параметры выборки списка конференций. */
export interface AdminConferenceQuery {
status?: ConferenceStatus
q?: string
limit?: number
offset?: number
}
/** Тело правки конференции админом — те же поля, что и в пользовательском API. */
export interface AdminConferenceUpdatePayload {
title?: string
scheduled_at?: string
duration_minutes?: number
is_pinned?: boolean
recurrence?: ConferenceRecurrence
is_closed?: boolean
password?: string
summary_recipients?: SummaryRecipientsMode | null
}
/** Пользователь в ответе админ-списка. */
export interface AdminUserOut {
id: string
email: string
name_user: string
role: 'admin' | 'user'
is_blocked: boolean
email_verified: boolean
created_at: string
/** Команда пользователя — `null`, если не назначена. */
team_id: string | null
/** URL аватара с cache-busting (`?v=`) — `null`, если фото не загружено. */
avatar_url: string | null
team_name: string | null
}
/**
* Карточка профиля пользователя для админки — тот же
* набор полей, что и в списке (backend отдаёт аватар и команду и в списке,
* и в карточке).
*/
export type AdminUserDetailOut = AdminUserOut
/** Параметры выборки списка пользователей. */
export interface AdminUserQuery {
q?: string
limit?: number
offset?: number
}
/** Тело правки пользователя — роль, блокировка, ФИО и/или команда, все поля опциональны. */
export interface AdminUserUpdatePayload {
role?: 'admin' | 'user'
is_blocked?: boolean
name_user?: string
/** `null` — снять пользователя с команды. */
team_id?: string | null
}
/** Тело создания пользователя администратором. */
export interface AdminUserCreateIn {
name_user: string
email: string
/** Та же политика минимальной длины, что и при самостоятельной регистрации. */
password: string
team_id?: string | null
}
/** Команда в ответе админ-списка. */
export interface TeamOut {
id: string
name: string
created_at: string
}
/** Параметры выборки списка команд. */
export interface AdminTeamQuery {
limit?: number
offset?: number
}
/** Стандартный конверт пагинации списковых админ-эндпоинтов. */
export interface PagedResult<T> {
items: T[]
total: number
}
function toQueryString(params: Record<string, string | number | undefined>): string {
const search = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== '') search.set(key, String(value))
}
const qs = search.toString()
return qs ? `?${qs}` : ''
}
/** Список конференций для админки — с фильтром по статусу, поиском и пагинацией. */
export async function listAdminConferences(query: AdminConferenceQuery = {}): Promise<PagedResult<AdminConferenceOut>> {
const qs = toQueryString({ status: query.status, q: query.q, limit: query.limit, offset: query.offset })
return apiRequest<PagedResult<AdminConferenceOut>>(`/admin/conferences${qs}`)
}
/** Частично обновить конференцию от имени администратора (реюз `ConferenceService.update`). */
export async function updateAdminConference(
id: string,
payload: AdminConferenceUpdatePayload,
): Promise<AdminConferenceOut> {
return apiRequest<AdminConferenceOut>(`/admin/conferences/${id}`, { method: 'PATCH', body: payload })
}
/** Удалить конференцию навсегда от имени администратора. 409 — конференция сейчас идёт. */
export async function deleteAdminConference(id: string): Promise<void> {
await apiRequest(`/admin/conferences/${id}`, { method: 'DELETE' })
}
/**
* Ручная рассылка .ics-приглашений по конференции. Без `emails` — получатели
* по умолчанию (владелец + участники прошлых сеансов для закреплённых).
* 202 — задача поставлена в очередь, письма отправляются асинхронно.
*/
export async function sendConferenceInvitations(id: string, emails?: string[]): Promise<void> {
await apiRequest(`/admin/conferences/${id}/invitations`, {
method: 'POST',
body: emails && emails.length > 0 ? { emails } : {},
})
}
/** Список пользователей для админки — с поиском и пагинацией. */
export async function listAdminUsers(query: AdminUserQuery = {}): Promise<PagedResult<AdminUserOut>> {
const qs = toQueryString({ q: query.q, limit: query.limit, offset: query.offset })
return apiRequest<PagedResult<AdminUserOut>>(`/admin/users${qs}`)
}
/** Сменить роль и/или блокировку пользователя. 409 — попытка изменить свою учётную запись. */
export async function updateAdminUser(id: string, payload: AdminUserUpdatePayload): Promise<AdminUserOut> {
return apiRequest<AdminUserOut>(`/admin/users/${id}`, { method: 'PATCH', body: payload })
}
/**
* Создать пользователя от имени администратора. 201 — email
* считается подтверждённым сразу. 409 — email уже зарегистрирован,
* 404 `team_not_found` — указанная команда не существует.
*/
export async function createAdminUser(payload: AdminUserCreateIn): Promise<AdminUserOut> {
return apiRequest<AdminUserOut>('/admin/users', { method: 'POST', body: payload })
}
/** Карточка профиля пользователя — открывается по клику на имя в таблице «Пользователи». */
export async function getAdminUser(id: string): Promise<AdminUserDetailOut> {
return apiRequest<AdminUserDetailOut>(`/admin/users/${id}`)
}
/** Загрузить аватар пользователя от имени администратора. 413/415 — лимиты размера/типа файла. */
export async function uploadAdminUserAvatar(id: string, file: File): Promise<AdminUserDetailOut> {
const form = new FormData()
form.append('file', file)
return apiRequest<AdminUserDetailOut>(`/admin/users/${id}/avatar`, { method: 'POST', body: form })
}
/** Текущие настройки инстанса. */
export async function getAdminSettings(): Promise<SettingsOut> {
return apiRequest<SettingsOut>('/admin/settings')
}
/** Частично обновить настройки инстанса. 400 — недоступный уровень AI/невалидная таймзона. */
export async function updateAdminSettings(payload: SettingsUpdateIn): Promise<SettingsOut> {
return apiRequest<SettingsOut>('/admin/settings', { method: 'PUT', body: payload })
}
/** Список команд для админки — с пагинацией. */
export async function listAdminTeams(query: AdminTeamQuery = {}): Promise<PagedResult<TeamOut>> {
const qs = toQueryString({ limit: query.limit, offset: query.offset })
return apiRequest<PagedResult<TeamOut>>(`/admin/teams${qs}`)
}
/** Создать команду. 409 — имя уже занято. */
export async function createAdminTeam(name: string): Promise<TeamOut> {
return apiRequest<TeamOut>('/admin/teams', { method: 'POST', body: { name } })
}
/** Переименовать команду. 409 — имя уже занято. */
export async function renameAdminTeam(id: string, name: string): Promise<TeamOut> {
return apiRequest<TeamOut>(`/admin/teams/${id}`, { method: 'PATCH', body: { name } })
}
/** Удалить команду навсегда. */
export async function deleteAdminTeam(id: string): Promise<void> {
await apiRequest(`/admin/teams/${id}`, { method: 'DELETE' })
}