// 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'),
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' })
},
}