diff --git a/README.md b/README.md index cc89889..b366a9e 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # Bedolaga Cabinet - Web Interface -Веб-интерфейс личного кабинета для VPN бота на базе [Remnawave Bedolaga Telegram Bot V3.0.0+](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot). +Веб-интерфейс личного кабинета для VPN бота на базе [Remnawave Bedolaga Telegram Bot v3.33.0+](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot). React + Vite + TypeScript | Авторизация через Telegram | Мультиязычность (EN/RU) | Адаптивный дизайн ## Требования - Docker и Docker Compose -- Запущенный backend бота с включенным Cabinet API +- Запущенный backend бота с включенным Cabinet API, версия **v3.33.0 или новее** — авторизация кабинета использует deep-link эндпоинты (`/cabinet/auth/deeplink/*`), которых нет в более старых версиях. На старом боте кабинет не сможет авторизовать пользователей (залоченные теги вроде `:3` не подходят — используйте `:latest` или конкретный тег ≥ 3.33.0) - Обратный прокси (Caddy / Nginx / Traefik) ## Архитектура diff --git a/src/api/branding.ts b/src/api/branding.ts index e57a130..72d6ffc 100644 --- a/src/api/branding.ts +++ b/src/api/branding.ts @@ -1,4 +1,5 @@ import apiClient from './client'; +import { isEndpointMissingError } from '../utils/api-error'; import type { AnimationConfig } from '@/components/ui/backgrounds/types'; import { DEFAULT_ANIMATION_CONFIG } from '@/components/ui/backgrounds/types'; @@ -36,6 +37,8 @@ export interface TelegramWidgetConfig { request_access: boolean; oidc_enabled: boolean; oidc_client_id: string; + /** The bot answered 404 on the config route — build older than v3.24.0. */ + endpoint_missing?: boolean; } export interface OfflineConvGoal { @@ -321,7 +324,7 @@ export const brandingApi = { '/cabinet/branding/telegram-widget', ); return response.data; - } catch { + } catch (err) { return { bot_username: import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '', size: 'large', @@ -330,6 +333,7 @@ export const brandingApi = { request_access: true, oidc_enabled: false, oidc_client_id: '', + endpoint_missing: isEndpointMissingError(err), }; } }, diff --git a/src/components/TelegramLoginButton.tsx b/src/components/TelegramLoginButton.tsx index c31df2c..15dc748 100644 --- a/src/components/TelegramLoginButton.tsx +++ b/src/components/TelegramLoginButton.tsx @@ -9,6 +9,7 @@ import { useAuthStore } from '../store/auth'; import { useNavigate } from 'react-router'; import { getPendingCampaignSlug } from '../utils/campaign'; import { copyToClipboard } from '../utils/clipboard'; +import { isEndpointMissingError } from '../utils/api-error'; interface TelegramLoginButtonProps { referralCode?: string; @@ -159,7 +160,6 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto isOIDC, widgetConfig?.oidc_client_id, widgetConfig?.request_access, - t, scriptLoaded, handleScriptFailed, ]); @@ -330,8 +330,9 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto }, (expires_in || 300) * 1000, ); - } catch { - setDeepLinkError(t('common.error')); + } catch (err) { + // 404 = the deep-link auth routes don't exist on this bot build (< v3.33.0) + setDeepLinkError(t(isEndpointMissingError(err) ? 'auth.botOutdated' : 'common.error')); } }, [botUsername, loginWithDeepLink, navigate, t]); @@ -421,9 +422,11 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto }, [deepLinkPolling, deepLinkToken, loginWithDeepLink, navigate, t]); if (!botUsername || botUsername === 'your_bot') { + // 404 on the config route = the bot build predates the cabinet endpoints, + // which reads as "not configured" but is actually a version mismatch (#345). return (
- {t('auth.telegramNotConfigured')} + {t(widgetConfig?.endpoint_missing ? 'auth.botOutdated' : 'auth.telegramNotConfigured')}
); } diff --git a/src/locales/en.json b/src/locales/en.json index 6221861..92b8a93 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -201,6 +201,7 @@ "telegramRetryFailed": "Authorization failed. Close the app and try again.", "telegramReopenHint": "If the problem persists, close and reopen the app", "telegramNotConfigured": "Telegram bot is not configured", + "botOutdated": "The bot version is not compatible with the cabinet. Update the bot to v3.33.0 or newer.", "telegramUnavailable": "Telegram login is temporarily unavailable", "authenticating": "Authenticating...", "orOpenInApp": "Or open the bot in the app", diff --git a/src/locales/fa.json b/src/locales/fa.json index 5142049..33372f7 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -193,6 +193,7 @@ "telegramRetryFailed": "تایید هویت ناموفق بود. برنامه را ببندید و دوباره باز کنید.", "telegramReopenHint": "اگر مشکل ادامه دارد، برنامه را ببندید و دوباره باز کنید", "telegramNotConfigured": "ربات تلگرام پیکربندی نشده", + "botOutdated": "نسخهٔ ربات با کابینت سازگار نیست. ربات را به v3.33.0 یا جدیدتر به‌روزرسانی کنید.", "telegramUnavailable": "ورود از طریق تلگرام موقتاً در دسترس نیست", "authenticating": "در حال تایید هویت...", "orOpenInApp": "یا ربات را در برنامه باز کنید", diff --git a/src/locales/ru.json b/src/locales/ru.json index 32fc373..038aaa1 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -204,6 +204,7 @@ "telegramRetryFailed": "Авторизация не удалась. Закройте приложение и откройте заново.", "telegramReopenHint": "Если проблема повторяется, закройте и откройте приложение заново", "telegramNotConfigured": "Telegram бот не настроен", + "botOutdated": "Версия бота несовместима с кабинетом. Обновите бота до v3.33.0 или новее.", "telegramUnavailable": "Вход через Telegram временно недоступен", "authenticating": "Авторизация...", "orOpenInApp": "Или откройте бота в приложении", diff --git a/src/locales/zh.json b/src/locales/zh.json index 356a5c3..c9cfb4a 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -193,6 +193,7 @@ "telegramRetryFailed": "授权失败。请关闭应用后重新打开。", "telegramReopenHint": "如果问题持续存在,请关闭并重新打开应用", "telegramNotConfigured": "Telegram机器人未配置", + "botOutdated": "机器人版本与后台不兼容,请将机器人更新至 v3.33.0 或更高版本。", "telegramUnavailable": "Telegram登录暂时不可用", "authenticating": "正在验证...", "orOpenInApp": "或在应用中打开机器人", diff --git a/src/utils/api-error.test.ts b/src/utils/api-error.test.ts new file mode 100644 index 0000000..bb6c5bb --- /dev/null +++ b/src/utils/api-error.test.ts @@ -0,0 +1,59 @@ +import { AxiosError, AxiosHeaders } from 'axios'; +import { describe, expect, it } from 'vitest'; +import { getApiErrorMessage, isEndpointMissingError } from './api-error'; + +function axiosErrorWithStatus(status: number, detail?: unknown): AxiosError { + const headers = new AxiosHeaders(); + const config = { headers }; + return new AxiosError( + 'Request failed', + 'ERR_BAD_REQUEST', + config, + {}, + { + status, + statusText: '', + headers, + config, + data: detail === undefined ? {} : { detail }, + }, + ); +} + +describe('isEndpointMissingError', () => { + it('returns true for an axios 404', () => { + expect(isEndpointMissingError(axiosErrorWithStatus(404))).toBe(true); + }); + + it('returns false for other axios statuses', () => { + expect(isEndpointMissingError(axiosErrorWithStatus(403))).toBe(false); + expect(isEndpointMissingError(axiosErrorWithStatus(500))).toBe(false); + }); + + it('returns false for non-axios errors', () => { + expect(isEndpointMissingError(new Error('boom'))).toBe(false); + expect(isEndpointMissingError(undefined)).toBe(false); + expect(isEndpointMissingError(null)).toBe(false); + }); +}); + +describe('getApiErrorMessage', () => { + it('returns string detail from an axios error', () => { + expect(getApiErrorMessage(axiosErrorWithStatus(400, 'Code must not be empty'), 'fb')).toBe( + 'Code must not be empty', + ); + }); + + it('joins pydantic validation errors', () => { + const err = axiosErrorWithStatus(422, [ + { loc: ['body', 'name'], msg: 'field required' }, + { loc: ['body', 'days'], msg: 'must be positive' }, + ]); + expect(getApiErrorMessage(err, 'fb')).toBe('name: field required; days: must be positive'); + }); + + it('falls back for non-axios errors and missing detail', () => { + expect(getApiErrorMessage(new Error('boom'), 'fb')).toBe('fb'); + expect(getApiErrorMessage(axiosErrorWithStatus(500), 'fb')).toBe('fb'); + }); +}); diff --git a/src/utils/api-error.ts b/src/utils/api-error.ts index b33e2ff..8e9f850 100644 --- a/src/utils/api-error.ts +++ b/src/utils/api-error.ts @@ -1,5 +1,15 @@ import axios from 'axios'; +/** + * True when the backend answered 404 on the route itself — for cabinet + * endpoints that means the bot build is too old to have them (e.g. the + * deep-link auth routes exist only since bot v3.33.0), as opposed to a + * missing entity inside a handler. + */ +export function isEndpointMissingError(err: unknown): boolean { + return axios.isAxiosError(err) && err.response?.status === 404; +} + export function getApiErrorMessage(err: unknown, fallback: string): string { if (axios.isAxiosError(err)) { const detail = err.response?.data?.detail;