first commit
This commit is contained in:
260
frontend/src/api/admin.ts
Normal file
260
frontend/src/api/admin.ts
Normal 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' })
|
||||
}
|
||||
Reference in New Issue
Block a user