use axum::{extract::ConnectInfo, http::HeaderMap, routing::get, Json, Router}; use serde_json::json; use std::net::SocketAddr; use crate::state::AppState; pub fn routes() -> Router { Router::new() .route("/health", get(health)) .route("/whoami", get(whoami)) } async fn health() -> Json { Json(json!({ "status": "ok", "version": env!("CARGO_PKG_VERSION") })) } async fn whoami( headers: HeaderMap, connect_info: Option>, ) -> Json { // Prefer X-Forwarded-For (set by Caddy/reverse proxy), then X-Real-Ip, then direct let forwarded = headers .get("x-forwarded-for") .and_then(|v| v.to_str().ok()) .map(|v| v.split(',').next().unwrap_or("").trim().to_string()); let real_ip = headers .get("x-real-ip") .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()); let direct = connect_info.map(|ci| ci.0.ip().to_string()); let ip = forwarded.clone() .or(real_ip.clone()) .or(direct.clone()) .unwrap_or_else(|| "unknown".to_string()); // Classify as IPv4 or IPv6 let is_v6 = ip.contains(':'); let via = headers.get("via").and_then(|v| v.to_str().ok()).map(|s| s.to_string()); let proto = headers.get("x-forwarded-proto").and_then(|v| v.to_str().ok()).map(|s| s.to_string()); let host = headers.get("x-forwarded-host").and_then(|v| v.to_str().ok()).map(|s| s.to_string()); let behind_proxy = forwarded.is_some() || real_ip.is_some() || via.is_some(); let mut result = json!({ "ip": ip, "version": if is_v6 { "IPv6" } else { "IPv4" }, "direct": direct, "behind_proxy": behind_proxy, }); if behind_proxy { let proxy = json!({ "x_forwarded_for": forwarded, "x_real_ip": real_ip, "x_forwarded_proto": proto, "x_forwarded_host": host, "via": via, }); result.as_object_mut().unwrap().insert("proxy".to_string(), proxy); } Json(result) }