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))