From 262390274dd4cc879ab809ca45a87c95cece4286 Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Tue, 31 Mar 2026 20:18:54 -0700 Subject: [PATCH] =?UTF-8?q?feat(auth):=20Telegram=20bot=20registration=20f?= =?UTF-8?q?low=20=E2=80=94=20/auth/telegram=20endpoint=20(#150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(auth): add /auth/telegram registration endpoint for bot-initiated login When a user sends /start register to the Telegram bot, the bot sends an inline button pointing to localhost:7788/auth/telegram?token=. This new GET handler consumes the one-time login token via the backend, stores the resulting JWT as the app session, and returns a styled HTML success/error page. Co-Authored-By: Claude Opus 4.6 (1M context) * style: apply cargo fmt to telegram auth handler Co-Authored-By: Claude Opus 4.6 (1M context) * fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit * update format --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit --- src/core/jsonrpc.rs | 171 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/src/core/jsonrpc.rs b/src/core/jsonrpc.rs index 0f86fe9fe..70f3c918c 100644 --- a/src/core/jsonrpc.rs +++ b/src/core/jsonrpc.rs @@ -87,6 +87,176 @@ pub fn default_state() -> AppState { // --- HTTP server (Axum) ---------------------------------------------------- +#[derive(Debug, serde::Deserialize)] +struct TelegramAuthQuery { + token: Option, +} + +fn success_html() -> String { + r#" + + + + + OpenHuman — Connected + + + +
+
+

Connected!

+

Your Telegram account has been connected to OpenHuman. You can close this tab.

+
+ +"# + .to_string() +} + +fn escape_html(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +fn error_html(message: &str) -> String { + let escaped_message = escape_html(message); + format!( + r#" + + + + + OpenHuman — Error + + + +
+
+

Something went wrong

+

{escaped_message}

+
+ +"# + ) +} + +async fn telegram_auth_handler(Query(query): Query) -> impl IntoResponse { + let html_response = |status: StatusCode, body: String| -> Response { + ( + status, + [(header::CONTENT_TYPE, "text/html; charset=utf-8")], + body, + ) + .into_response() + }; + + let token = match query + .token + .as_deref() + .map(str::trim) + .filter(|s| !s.is_empty()) + { + Some(t) => t.to_string(), + None => { + return html_response( + StatusCode::BAD_REQUEST, + error_html("Missing token parameter. Send /start register to the bot again."), + ) + } + }; + + log::info!("[auth:telegram] Received registration callback with token"); + + let config = match crate::openhuman::config::Config::load_or_init().await { + Ok(c) => c, + Err(e) => { + log::error!("[auth:telegram] Failed to load config: {e}"); + return html_response( + StatusCode::INTERNAL_SERVER_ERROR, + error_html("Internal error. Please try again."), + ); + } + }; + + let api_url = crate::api::config::effective_api_url(&config.api_url); + + let client = match crate::api::rest::BackendOAuthClient::new(&api_url) { + Ok(c) => c, + Err(e) => { + log::error!("[auth:telegram] Failed to create API client: {e}"); + return html_response( + StatusCode::INTERNAL_SERVER_ERROR, + error_html("Internal error. Please try again."), + ); + } + }; + + let jwt_token = match client.consume_login_token(&token).await { + Ok(jwt) => jwt, + Err(e) => { + let error_str = e.to_string(); + // Check if this is a client-side error (token validation) or server-side error + let is_client_error = error_str.contains("expired") + || error_str.contains("invalid") + || error_str.contains("not found") + || error_str.contains("already used") + || error_str.contains("401") + || error_str.contains("400") + || error_str.contains("404"); + + if is_client_error { + log::warn!("[auth:telegram] Token consumption failed (client error): {e}"); + return html_response( + StatusCode::BAD_REQUEST, + error_html( + "This link has expired or was already used. Send /start register to the bot again.", + ), + ); + } else { + log::error!("[auth:telegram] Token consumption failed (server error): {e}"); + return html_response( + StatusCode::INTERNAL_SERVER_ERROR, + error_html("Internal server error, please try again later."), + ); + } + } + }; + + match crate::openhuman::credentials::ops::store_session(&config, &jwt_token, None, None).await { + Ok(outcome) => { + for msg in &outcome.logs { + log::info!("[auth:telegram] {msg}"); + } + log::info!("[auth:telegram] Session stored successfully"); + } + Err(e) => { + log::error!("[auth:telegram] Failed to store session: {e}"); + return html_response( + StatusCode::INTERNAL_SERVER_ERROR, + error_html("Connected to Telegram but failed to save session. Please try again."), + ); + } + } + + html_response(StatusCode::OK, success_html()) +} + pub fn build_core_http_router(socketio_enabled: bool) -> Router { let router = Router::new() .route("/", get(root_handler)) @@ -94,6 +264,7 @@ pub fn build_core_http_router(socketio_enabled: bool) -> Router { .route("/schema", get(schema_handler)) .route("/events", get(events_handler)) .route("/rpc", post(rpc_handler)) + .route("/auth/telegram", get(telegram_auth_handler)) .fallback(not_found_handler) .layer(middleware::from_fn(http_request_log_middleware)) .layer(middleware::from_fn(cors_middleware))