mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 01:23:47 +00:00
fix(auth): различать устаревшего бота и ненастроенного на экране логина
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
# Bedolaga Cabinet - Web Interface
|
# 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) | Адаптивный дизайн
|
React + Vite + TypeScript | Авторизация через Telegram | Мультиязычность (EN/RU) | Адаптивный дизайн
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
- Docker и Docker Compose
|
- 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)
|
- Обратный прокси (Caddy / Nginx / Traefik)
|
||||||
|
|
||||||
## Архитектура
|
## Архитектура
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import apiClient from './client';
|
import apiClient from './client';
|
||||||
|
import { isEndpointMissingError } from '../utils/api-error';
|
||||||
import type { AnimationConfig } from '@/components/ui/backgrounds/types';
|
import type { AnimationConfig } from '@/components/ui/backgrounds/types';
|
||||||
import { DEFAULT_ANIMATION_CONFIG } from '@/components/ui/backgrounds/types';
|
import { DEFAULT_ANIMATION_CONFIG } from '@/components/ui/backgrounds/types';
|
||||||
|
|
||||||
@@ -36,6 +37,8 @@ export interface TelegramWidgetConfig {
|
|||||||
request_access: boolean;
|
request_access: boolean;
|
||||||
oidc_enabled: boolean;
|
oidc_enabled: boolean;
|
||||||
oidc_client_id: string;
|
oidc_client_id: string;
|
||||||
|
/** The bot answered 404 on the config route — build older than v3.24.0. */
|
||||||
|
endpoint_missing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OfflineConvGoal {
|
export interface OfflineConvGoal {
|
||||||
@@ -321,7 +324,7 @@ export const brandingApi = {
|
|||||||
'/cabinet/branding/telegram-widget',
|
'/cabinet/branding/telegram-widget',
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch {
|
} catch (err) {
|
||||||
return {
|
return {
|
||||||
bot_username: import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '',
|
bot_username: import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '',
|
||||||
size: 'large',
|
size: 'large',
|
||||||
@@ -330,6 +333,7 @@ export const brandingApi = {
|
|||||||
request_access: true,
|
request_access: true,
|
||||||
oidc_enabled: false,
|
oidc_enabled: false,
|
||||||
oidc_client_id: '',
|
oidc_client_id: '',
|
||||||
|
endpoint_missing: isEndpointMissingError(err),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useAuthStore } from '../store/auth';
|
|||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { getPendingCampaignSlug } from '../utils/campaign';
|
import { getPendingCampaignSlug } from '../utils/campaign';
|
||||||
import { copyToClipboard } from '../utils/clipboard';
|
import { copyToClipboard } from '../utils/clipboard';
|
||||||
|
import { isEndpointMissingError } from '../utils/api-error';
|
||||||
|
|
||||||
interface TelegramLoginButtonProps {
|
interface TelegramLoginButtonProps {
|
||||||
referralCode?: string;
|
referralCode?: string;
|
||||||
@@ -159,7 +160,6 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
isOIDC,
|
isOIDC,
|
||||||
widgetConfig?.oidc_client_id,
|
widgetConfig?.oidc_client_id,
|
||||||
widgetConfig?.request_access,
|
widgetConfig?.request_access,
|
||||||
t,
|
|
||||||
scriptLoaded,
|
scriptLoaded,
|
||||||
handleScriptFailed,
|
handleScriptFailed,
|
||||||
]);
|
]);
|
||||||
@@ -330,8 +330,9 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
},
|
},
|
||||||
(expires_in || 300) * 1000,
|
(expires_in || 300) * 1000,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch (err) {
|
||||||
setDeepLinkError(t('common.error'));
|
// 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]);
|
}, [botUsername, loginWithDeepLink, navigate, t]);
|
||||||
|
|
||||||
@@ -421,9 +422,11 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
}, [deepLinkPolling, deepLinkToken, loginWithDeepLink, navigate, t]);
|
}, [deepLinkPolling, deepLinkToken, loginWithDeepLink, navigate, t]);
|
||||||
|
|
||||||
if (!botUsername || botUsername === 'your_bot') {
|
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 (
|
return (
|
||||||
<div className="py-4 text-center text-sm text-dark-400">
|
<div className="py-4 text-center text-sm text-dark-400">
|
||||||
{t('auth.telegramNotConfigured')}
|
{t(widgetConfig?.endpoint_missing ? 'auth.botOutdated' : 'auth.telegramNotConfigured')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,6 +189,7 @@
|
|||||||
"telegramRetryFailed": "Authorization failed. Close the app and try again.",
|
"telegramRetryFailed": "Authorization failed. Close the app and try again.",
|
||||||
"telegramReopenHint": "If the problem persists, close and reopen the app",
|
"telegramReopenHint": "If the problem persists, close and reopen the app",
|
||||||
"telegramNotConfigured": "Telegram bot is not configured",
|
"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",
|
"telegramUnavailable": "Telegram login is temporarily unavailable",
|
||||||
"authenticating": "Authenticating...",
|
"authenticating": "Authenticating...",
|
||||||
"orOpenInApp": "Or open the bot in the app",
|
"orOpenInApp": "Or open the bot in the app",
|
||||||
|
|||||||
@@ -181,6 +181,7 @@
|
|||||||
"telegramRetryFailed": "تایید هویت ناموفق بود. برنامه را ببندید و دوباره باز کنید.",
|
"telegramRetryFailed": "تایید هویت ناموفق بود. برنامه را ببندید و دوباره باز کنید.",
|
||||||
"telegramReopenHint": "اگر مشکل ادامه دارد، برنامه را ببندید و دوباره باز کنید",
|
"telegramReopenHint": "اگر مشکل ادامه دارد، برنامه را ببندید و دوباره باز کنید",
|
||||||
"telegramNotConfigured": "ربات تلگرام پیکربندی نشده",
|
"telegramNotConfigured": "ربات تلگرام پیکربندی نشده",
|
||||||
|
"botOutdated": "نسخهٔ ربات با کابینت سازگار نیست. ربات را به v3.33.0 یا جدیدتر بهروزرسانی کنید.",
|
||||||
"telegramUnavailable": "ورود از طریق تلگرام موقتاً در دسترس نیست",
|
"telegramUnavailable": "ورود از طریق تلگرام موقتاً در دسترس نیست",
|
||||||
"authenticating": "در حال تایید هویت...",
|
"authenticating": "در حال تایید هویت...",
|
||||||
"orOpenInApp": "یا ربات را در برنامه باز کنید",
|
"orOpenInApp": "یا ربات را در برنامه باز کنید",
|
||||||
|
|||||||
@@ -192,6 +192,7 @@
|
|||||||
"telegramRetryFailed": "Авторизация не удалась. Закройте приложение и откройте заново.",
|
"telegramRetryFailed": "Авторизация не удалась. Закройте приложение и откройте заново.",
|
||||||
"telegramReopenHint": "Если проблема повторяется, закройте и откройте приложение заново",
|
"telegramReopenHint": "Если проблема повторяется, закройте и откройте приложение заново",
|
||||||
"telegramNotConfigured": "Telegram бот не настроен",
|
"telegramNotConfigured": "Telegram бот не настроен",
|
||||||
|
"botOutdated": "Версия бота несовместима с кабинетом. Обновите бота до v3.33.0 или новее.",
|
||||||
"telegramUnavailable": "Вход через Telegram временно недоступен",
|
"telegramUnavailable": "Вход через Telegram временно недоступен",
|
||||||
"authenticating": "Авторизация...",
|
"authenticating": "Авторизация...",
|
||||||
"orOpenInApp": "Или откройте бота в приложении",
|
"orOpenInApp": "Или откройте бота в приложении",
|
||||||
|
|||||||
@@ -181,6 +181,7 @@
|
|||||||
"telegramRetryFailed": "授权失败。请关闭应用后重新打开。",
|
"telegramRetryFailed": "授权失败。请关闭应用后重新打开。",
|
||||||
"telegramReopenHint": "如果问题持续存在,请关闭并重新打开应用",
|
"telegramReopenHint": "如果问题持续存在,请关闭并重新打开应用",
|
||||||
"telegramNotConfigured": "Telegram机器人未配置",
|
"telegramNotConfigured": "Telegram机器人未配置",
|
||||||
|
"botOutdated": "机器人版本与后台不兼容,请将机器人更新至 v3.33.0 或更高版本。",
|
||||||
"telegramUnavailable": "Telegram登录暂时不可用",
|
"telegramUnavailable": "Telegram登录暂时不可用",
|
||||||
"authenticating": "正在验证...",
|
"authenticating": "正在验证...",
|
||||||
"orOpenInApp": "或在应用中打开机器人",
|
"orOpenInApp": "或在应用中打开机器人",
|
||||||
|
|||||||
59
src/utils/api-error.test.ts
Normal file
59
src/utils/api-error.test.ts
Normal file
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,15 @@
|
|||||||
import axios from 'axios';
|
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 {
|
export function getApiErrorMessage(err: unknown, fallback: string): string {
|
||||||
if (axios.isAxiosError(err)) {
|
if (axios.isAxiosError(err)) {
|
||||||
const detail = err.response?.data?.detail;
|
const detail = err.response?.data?.detail;
|
||||||
|
|||||||
Reference in New Issue
Block a user