| |
| use axum::{ |
| extract::Request, |
| http::{header, StatusCode}, |
| middleware::Next, |
| response::{IntoResponse, Response, Html}, |
| }; |
| use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; |
| use once_cell::sync::Lazy; |
|
|
| |
| struct AdminCredentials { |
| username: String, |
| password: String, |
| enabled: bool, |
| } |
|
|
| |
| fn load_admin_credentials() -> AdminCredentials { |
| let username = std::env::var("ADMIN_USER") |
| .or_else(|_| std::env::var("ADMIN_USERNAME")) |
| .unwrap_or_default(); |
| let password = std::env::var("ADMIN_PASS") |
| .or_else(|_| std::env::var("ADMIN_PASSWORD")) |
| .unwrap_or_default(); |
|
|
| let enabled = !username.is_empty() && !password.is_empty(); |
|
|
| if enabled { |
| tracing::info!("Admin authentication enabled for user: {}", username); |
| } else { |
| tracing::warn!("Admin authentication DISABLED - set ADMIN_USER and ADMIN_PASS environment variables"); |
| } |
|
|
| AdminCredentials { |
| username, |
| password, |
| enabled, |
| } |
| } |
|
|
| |
| static ADMIN_CREDS: Lazy<AdminCredentials> = Lazy::new(load_admin_credentials); |
|
|
| |
| pub fn is_admin_auth_enabled() -> bool { |
| ADMIN_CREDS.enabled |
| } |
|
|
| |
| fn validate_basic_auth(auth_header: &str) -> bool { |
| if !ADMIN_CREDS.enabled { |
| return true; |
| } |
|
|
| |
| let encoded = match auth_header.strip_prefix("Basic ") { |
| Some(e) => e, |
| None => return false, |
| }; |
|
|
| |
| let decoded = match BASE64.decode(encoded) { |
| Ok(d) => d, |
| Err(_) => return false, |
| }; |
|
|
| |
| let decoded_str = match String::from_utf8(decoded) { |
| Ok(s) => s, |
| Err(_) => return false, |
| }; |
|
|
| let parts: Vec<&str> = decoded_str.splitn(2, ':').collect(); |
| if parts.len() != 2 { |
| return false; |
| } |
|
|
| parts[0] == ADMIN_CREDS.username && parts[1] == ADMIN_CREDS.password |
| } |
|
|
| |
| pub async fn basic_auth_middleware(request: Request, next: Next) -> Result<Response, Response> { |
| let path = request.uri().path(); |
|
|
| |
| if path.starts_with("/v1") { |
| return Ok(next.run(request).await); |
| } |
|
|
| |
| if path == "/healthz" { |
| return Ok(next.run(request).await); |
| } |
|
|
| |
| if !ADMIN_CREDS.enabled { |
| return Ok(next.run(request).await); |
| } |
|
|
| |
| let auth_header = request |
| .headers() |
| .get(header::AUTHORIZATION) |
| .and_then(|h| h.to_str().ok()); |
|
|
| match auth_header { |
| Some(header) if validate_basic_auth(header) => { |
| Ok(next.run(request).await) |
| } |
| _ => { |
| |
| Err(unauthorized_response(path)) |
| } |
| } |
| } |
|
|
| |
| fn unauthorized_response(path: &str) -> Response { |
| |
| if path.starts_with("/api/") { |
| let body = serde_json::json!({ |
| "success": false, |
| "error": "Authentication required. Set ADMIN_USER and ADMIN_PASS in HuggingFace Secrets." |
| }); |
|
|
| return ( |
| StatusCode::UNAUTHORIZED, |
| [ |
| (header::CONTENT_TYPE, "application/json"), |
| (header::WWW_AUTHENTICATE, "Basic realm=\"Antigravity Admin\""), |
| ], |
| serde_json::to_string(&body).unwrap(), |
| ) |
| .into_response(); |
| } |
|
|
| |
| ( |
| StatusCode::UNAUTHORIZED, |
| [ |
| (header::CONTENT_TYPE, "text/html; charset=utf-8"), |
| (header::WWW_AUTHENTICATE, "Basic realm=\"Antigravity Admin\""), |
| ], |
| Html(r#" |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>Login Required</title> |
| <style> |
| body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5; } |
| .box { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center; } |
| h1 { color: #333; margin-bottom: 1rem; } |
| p { color: #666; } |
| </style> |
| </head> |
| <body> |
| <div class="box"> |
| <h1>Login Required</h1> |
| <p>Please enter your credentials to access Antigravity Manager.</p> |
| <p style="font-size: 0.8rem; color: #999;">Configure ADMIN_USER and ADMIN_PASS in HuggingFace Secrets</p> |
| </div> |
| </body> |
| </html> |
| "#.to_string()), |
| ) |
| .into_response() |
| } |
|
|