From 2dbbdb025264b9138ab4456c789070963414c2cf Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 6 Jul 2026 16:30:16 +0300 Subject: [PATCH] Fixed TG login --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 3 +- src/api/mod.rs | 75 +++++++++++++++++++++++++++++++++++ src/i18n/phrases.rs | 5 ++- src/main.rs | 14 +++++-- templates/client_portal.html | 26 +++++++++++- templates/configs.html | 26 +++++++++++- templates/login.html | 35 ++++++++++++++++ templates/telegram_login.html | 62 +++++++++++++++++++++++++++++ 10 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 templates/telegram_login.html diff --git a/Cargo.lock b/Cargo.lock index c87924b..befe873 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,7 +61,7 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "amnezia-fellow" -version = "0.1.6" +version = "0.1.7" dependencies = [ "async-trait", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 524fd30..f2f011a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "amnezia-fellow" -version = "0.1.7" +version = "0.1.8" edition = "2024" description = "Amnezia VPN client manager with SSO, SQLite/PostgreSQL, and Kubernetes Secret sync" diff --git a/README.md b/README.md index 51deb09..799e815 100644 --- a/README.md +++ b/README.md @@ -156,12 +156,13 @@ delivered. ## API -The JSON API is session-authenticated: +The JSON API is session-authenticated unless noted: - `GET /api/me` - `GET /api/vpn-clients` - `GET /api/vpn-status` - `GET /api/telegram-link/status` +- `POST /api/telegram-login/webapp` (public Telegram `initData` login) - `POST /api/telegram-link/webapp` - `POST /api/telegram-link/start` - `POST /api/telegram-link/decline` diff --git a/src/api/mod.rs b/src/api/mod.rs index 5c6809d..e6d35a6 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -162,6 +162,11 @@ struct TelegramLinkResponse { status: TelegramLinkStatusResponse, } +#[derive(Debug, Serialize, JsonSchema)] +struct TelegramLoginResponse { + redirect_to: String, +} + #[derive(Debug, Deserialize, JsonSchema)] struct TelegramWebAppLinkRequest { init_data: String, @@ -292,6 +297,71 @@ async fn telegram_link_webapp_handler( .into_response() } +async fn telegram_login_webapp_handler( + session: Session, + db: Database, + Json(request): Json, +) -> cot::Result { + let (config, _) = AppConfig::load_with_db(&db).await; + if !config.telegram_bot_enabled { + return Ok(json_error_typed( + cot::http::StatusCode::CONFLICT, + "telegram_disabled", + "Telegram is disabled", + "Telegram integration is disabled by the administrator.", + "", + )); + } + + let telegram_user = match telegram::validate_web_app_init_data( + &request.init_data, + &config.telegram_bot_token, + TELEGRAM_INIT_DATA_MAX_AGE, + ) { + Ok(user) => user, + Err(e) => { + return Ok(json_error_typed( + cot::http::StatusCode::BAD_REQUEST, + "telegram_webapp_auth_failed", + "Telegram verification failed", + "Could not verify Telegram Web App data.", + &e.to_string(), + )); + } + }; + + let telegram_id = telegram_user.id.to_string(); + let Some(user) = User::get_by_telegram_id(&db, &telegram_id) + .await + .map_err(|e| cot::Error::internal(format!("failed to load Telegram user: {e}")))? + else { + return Ok(json_error_typed( + cot::http::StatusCode::UNAUTHORIZED, + "telegram_account_not_linked", + "Telegram is not linked", + "This Telegram account is not connected to any VPN account.", + "", + )); + }; + + if !user.is_active() { + return Ok(json_error_typed( + cot::http::StatusCode::FORBIDDEN, + "user_inactive", + "Account is inactive", + "This account is inactive.", + "", + )); + } + + auth::login(&session, user.id_val()).await?; + + Json(TelegramLoginResponse { + redirect_to: "/configs".to_owned(), + }) + .into_response() +} + async fn telegram_link_start_handler( session: Session, db: Database, @@ -831,6 +901,11 @@ impl App for ApiApp { api_post(telegram_link_webapp_handler), "api_telegram_link_webapp", ), + Route::with_api_handler_and_name( + "/telegram-login/webapp", + api_post(telegram_login_webapp_handler), + "api_telegram_login_webapp", + ), Route::with_api_handler_and_name( "/telegram-link/start", api_post(telegram_link_start_handler), diff --git a/src/i18n/phrases.rs b/src/i18n/phrases.rs index ad46eb2..3bcb498 100644 --- a/src/i18n/phrases.rs +++ b/src/i18n/phrases.rs @@ -42,6 +42,9 @@ translations! { login_submit: "Sign in" , "Войти"; login_disabled: "Login is currently disabled." , "Вход сейчас отключён."; login_invalid: "Invalid username or password." , "Неверное имя пользователя или пароль."; + login_telegram_wait: "Signing in with Telegram..." , "Входим через Telegram..."; + login_telegram_fallback: "Open the regular login page" , "Открыть обычный вход"; + login_telegram_failed: "Telegram login failed. Use regular login." , "Не удалось войти через Telegram. Используйте обычный вход."; // Logout nav_logout: "Logout" , "Выход"; @@ -169,7 +172,7 @@ translations! { telegram_open_bot: "Open bot" , "Открыть бота"; telegram_guide_start: "Open the bot and send /start once if you have not done it before." , "Откройте бота и один раз отправьте /start, если ещё не делали этого."; telegram_guide_get_id: "Send the secret code below to the bot." , "Отправьте боту секретный код ниже."; - telegram_guide_paste: "Return here and refresh the status." , "Вернитесь сюда и обновите статус."; + telegram_guide_paste: "Return here; the status updates automatically." , "Вернитесь сюда; статус обновится автоматически."; telegram_secret_label: "Secret code" , "Секретный код"; telegram_save: "Save Telegram" , "Сохранить Telegram"; telegram_saved: "Telegram connected." , "Telegram подключён."; diff --git a/src/main.rs b/src/main.rs index 97a4403..11c3151 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,14 +67,22 @@ struct ClientPortalTemplate { app_version: &'static str, } +#[derive(Debug, Template)] +#[template(path = "telegram_login.html")] +struct TelegramLoginTemplate { + t: &'static Translations, +} + async fn configs_page( session: Session, db: Database, i18n: I18n, ) -> cot::Result { - let user = match auth::require_user_or_redirect(&session, &db).await { - Ok(user) => user, - Err(response) => return Ok(response), + let user = match auth::get_session_user(&session, &db).await { + Some(user) => user, + None => { + return Html::new(TelegramLoginTemplate { t: i18n.t }.render()?).into_response(); + } }; let is_admin = user.role == auth::Role::Admin; diff --git a/templates/client_portal.html b/templates/client_portal.html index 4845f57..59790ee 100644 --- a/templates/client_portal.html +++ b/templates/client_portal.html @@ -461,12 +461,15 @@ function clientPortal() { telegramModal: null, telegramBusy: false, telegramPromptChecked: false, + telegramStatusTimer: null, + telegramStatusPolling: false, init() { this.initTelegramWebApp(); this.load(); this.loadServerStatus(); this.loadTelegramStatus(); this.serverStatusTimer = setInterval(() => this.loadServerStatus(true), 30000); + this.telegramStatusTimer = setInterval(() => this.pollTelegramStatus(), 2500); }, async request(url, options = {}) { const response = await fetch(url, { @@ -517,11 +520,11 @@ function clientPortal() { this.telegram.isWebApp = Boolean(webApp.initData); this.telegram.initData = webApp.initData || ''; }, - async loadTelegramStatus() { + async loadTelegramStatus(options = {}) { try { const status = await this.request('/api/telegram-link/status'); this.applyTelegramStatus(status); - this.maybePromptTelegram(); + if (options.prompt !== false) this.maybePromptTelegram(); } catch (e) { console.warn('telegram status failed', e); } @@ -596,6 +599,25 @@ function clientPortal() { this.telegramBusy = false; } }, + async pollTelegramStatus() { + if (this.telegramStatusPolling || this.telegramBusy) return; + if (!this.telegram.enabled || !this.telegram.pending) return; + if (this.telegramModal !== 'manualGuide' && this.telegramModal !== 'manage') return; + + this.telegramStatusPolling = true; + const wasPending = this.telegram.pending; + try { + await this.loadTelegramStatus({ prompt: false }); + if (wasPending && this.telegram.linked) { + this.telegramModal = 'manage'; + this.setNotice('success', '{{ t.notice_success_title }}', '{{ t.telegram_saved }}'); + } else if (wasPending && !this.telegram.pending) { + this.telegramModal = 'manage'; + } + } finally { + this.telegramStatusPolling = false; + } + }, async copyTelegramSecret() { if (!this.telegram.pending_secret) return; this.clearNotice(); diff --git a/templates/configs.html b/templates/configs.html index 1e7b801..e29edc8 100644 --- a/templates/configs.html +++ b/templates/configs.html @@ -480,6 +480,8 @@ function configsPage() { telegramModal: null, telegramBusy: false, telegramPromptChecked: false, + telegramStatusTimer: null, + telegramStatusPolling: false, isAdmin: {% if is_admin %}true{% else %}false{% endif %}, init() { this.initTelegramWebApp(); @@ -489,6 +491,7 @@ function configsPage() { this.loadRolloutStatus(); this.rolloutTimer = setInterval(() => this.loadRolloutStatus(), 10000); } + this.telegramStatusTimer = setInterval(() => this.pollTelegramStatus(), 2500); }, async request(url, options = {}) { const response = await fetch(url, { @@ -516,11 +519,11 @@ function configsPage() { this.telegram.isWebApp = Boolean(webApp.initData); this.telegram.initData = webApp.initData || ''; }, - async loadTelegramStatus() { + async loadTelegramStatus(options = {}) { try { const status = await this.request('/api/telegram-link/status'); this.applyTelegramStatus(status); - this.maybePromptTelegram(); + if (options.prompt !== false) this.maybePromptTelegram(); } catch (e) { console.warn('telegram status failed', e); } @@ -595,6 +598,25 @@ function configsPage() { this.telegramBusy = false; } }, + async pollTelegramStatus() { + if (this.telegramStatusPolling || this.telegramBusy) return; + if (!this.telegram.enabled || !this.telegram.pending) return; + if (this.telegramModal !== 'manualGuide' && this.telegramModal !== 'manage') return; + + this.telegramStatusPolling = true; + const wasPending = this.telegram.pending; + try { + await this.loadTelegramStatus({ prompt: false }); + if (wasPending && this.telegram.linked) { + this.telegramModal = 'manage'; + this.setNotice('success', '{{ t.notice_success_title }}', '{{ t.telegram_saved }}'); + } else if (wasPending && !this.telegram.pending) { + this.telegramModal = 'manage'; + } + } finally { + this.telegramStatusPolling = false; + } + }, async copyTelegramSecret() { if (!this.telegram.pending_secret) return; this.clearNotice(); diff --git a/templates/login.html b/templates/login.html index 50ed686..f530984 100644 --- a/templates/login.html +++ b/templates/login.html @@ -3,6 +3,7 @@ {% block title %}{{ t.login_heading }} | {{ t.site_name }}{% endblock title %} {% block head_extra %} + +{% endblock head_extra %} + +{% block body %} + + + +{% endblock body %}