mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
feat(auth): Telegram bot registration flow — /auth/telegram endpoint (#150)
* 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=<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) <noreply@anthropic.com> * style: apply cargo fmt to telegram auth handler Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai> * update format --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
This commit is contained in:
co-authored by
Claude Opus 4.6
CodeRabbit
coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
parent
684e784896
commit
262390274d
@@ -87,6 +87,176 @@ pub fn default_state() -> AppState {
|
||||
|
||||
// --- HTTP server (Axum) ----------------------------------------------------
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct TelegramAuthQuery {
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
fn success_html() -> String {
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>OpenHuman — Connected</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
|
||||
.card { background: #1e293b; border-radius: 16px; padding: 48px; text-align: center; max-width: 420px; box-shadow: 0 20px 25px -5px rgba(0,0,0,0.3); }
|
||||
.icon { font-size: 48px; margin-bottom: 16px; }
|
||||
h1 { font-size: 24px; margin-bottom: 12px; color: #f8fafc; }
|
||||
p { font-size: 16px; color: #94a3b8; line-height: 1.6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon">✔</div>
|
||||
<h1>Connected!</h1>
|
||||
<p>Your Telegram account has been connected to OpenHuman. You can close this tab.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#
|
||||
.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#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>OpenHuman — Error</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; display: flex; align-items: center; justify-content: center; min-height: 100vh; }}
|
||||
.card {{ background: #1e293b; border-radius: 16px; padding: 48px; text-align: center; max-width: 420px; box-shadow: 0 20px 25px -5px rgba(0,0,0,0.3); }}
|
||||
.icon {{ font-size: 48px; margin-bottom: 16px; }}
|
||||
h1 {{ font-size: 24px; margin-bottom: 12px; color: #f8fafc; }}
|
||||
p {{ font-size: 16px; color: #94a3b8; line-height: 1.6; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon">⚠</div>
|
||||
<h1>Something went wrong</h1>
|
||||
<p>{escaped_message}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#
|
||||
)
|
||||
}
|
||||
|
||||
async fn telegram_auth_handler(Query(query): Query<TelegramAuthQuery>) -> 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))
|
||||
|
||||
Reference in New Issue
Block a user