Newer
Older
tgclient-mcp / frontend / src / api.js
// API-клиент tgclient. Доступ к API: браузерная cookie-сессия gnexus-auth
// (сервис закрыт SSO) или открытый режим без аутентификации (auth-off);
// TGCLIENT_ADMIN_TOKEN на бэкенде опционален — им подписывают запросы скрипты.

export class ApiError extends Error {
  constructor(status, message) {
    super(message)
    this.status = status
  }
}

async function request(path, { method = 'GET', body } = {}) {
  const response = await fetch('/api/v1' + path, {
    method,
    headers: {
      'Content-Type': 'application/json',
    },
    ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
  })
  if (!response.ok) {
    let detail = response.statusText
    try {
      const data = await response.json()
      detail = data.detail || detail
    } catch { /* не-JSON ответ */ }
    // сессия кончилась, пока SPA открыт — сообщаем гейту в App.vue
    if (response.status === 401) {
      window.dispatchEvent(new CustomEvent('tgclient:unauthenticated'))
    }
    throw new ApiError(response.status, detail)
  }
  if (response.status === 204) return null
  return response.json()
}

export const api = {
  // Профиль текущего пользователя / флаг режима авторизации:
  // { auth_enabled: false, user: null } (сервис открыт) или
  // { auth_enabled: true, user: {...}, locale_effective: 'ru' } | 401 (нужно войти)
  me: () => request('/me'),
  // PATCH /me — только cookie-сессия (у Bearer-токена нет личности для настроек).
  // locale: 'en'|'uk'|'ru' или 'auto' (сброс override)
  updateMe: (fields) => request('/me', { method: 'PATCH', body: fields }),

  // Аккаунты Telegram (владелец — sub текущего SSO-юзера)
  accounts: () => request('/accounts'),
  startLogin: (phone, label = '') =>
    request('/accounts/logins', { method: 'POST', body: { phone, label } }),
  loginState: (loginId) => request(`/accounts/logins/${loginId}`),
  submitLoginCode: (loginId, code) =>
    request(`/accounts/logins/${loginId}/code`, { method: 'POST', body: { code } }),
  submitLoginPassword: (loginId, password) =>
    request(`/accounts/logins/${loginId}/password`, { method: 'POST', body: { password } }),
  cancelLogin: (loginId) => request(`/accounts/logins/${loginId}`, { method: 'DELETE' }),
  connectAccount: (id) => request(`/accounts/${id}/connect`, { method: 'POST' }),
  disconnectAccount: (id) => request(`/accounts/${id}/disconnect`, { method: 'POST' }),
  // логаут Telegram: отзыв MTProto-ключа + удаление строки аккаунта (необратимо)
  logoutAccount: (id) => request(`/accounts/${id}`, { method: 'DELETE' }),

  // Персональные MCP-ключи (только cookie-сессия, как /me PATCH);
  // createMcpToken возвращает plaintext-ключ ровно один раз
  mcpTokens: () => request('/me/mcp_tokens'),
  createMcpToken: (name) => request('/me/mcp_tokens', { method: 'POST', body: { name } }),
  revokeMcpToken: (id) => request(`/me/mcp_tokens/${id}/revoke`, { method: 'POST' }),

  // Админка (роль admin/superadmin)
  adminOverview: () => request('/admin/overview'),
  adminMcpTokens: () => request('/admin/mcp_tokens'),
  adminRevokeSession: (id) => request(`/admin/accounts/${id}/revoke_session`, { method: 'POST' }),

  // Служебное
  health: async () => {
    const r = await fetch('/api/v1/health')
    if (!r.ok) throw new ApiError(r.status, r.statusText)
    return r.json()
  },
  // OAuth не под /api/v1 — служебные пути
  logout: async () => {
    return fetch('/auth/logout', { method: 'POST' })
  },
}