1use std::sync::atomic::Ordering;
16
17use axum::{
18 Router,
19 body::Body,
20 extract::State,
21 http::{StatusCode, header},
22 response::{IntoResponse, Response},
23 routing::get,
24};
25
26use super::state::AppState;
27
28fn json_response(body: String) -> Response {
30 Response::builder()
31 .status(StatusCode::OK)
32 .header(header::CONTENT_TYPE, "application/json; charset=utf-8")
33 .body(Body::from(body))
34 .unwrap_or_else(|_| {
35 Response::builder()
36 .status(StatusCode::INTERNAL_SERVER_ERROR)
37 .body(Body::from("Internal Server Error"))
38 .expect("static 500 response must be valid")
39 })
40}
41
42pub fn create_admin_router(state: AppState) -> Router {
44 Router::new()
45 .route("/live", get(liveness))
46 .route("/ready", get(readiness))
47 .route("/metrics", get(metrics))
48 .route("/config", get(config_handler))
49 .with_state(state)
50}
51
52async fn liveness() -> Response {
54 StatusCode::OK.into_response()
55}
56
57async fn readiness(State(state): State<AppState>) -> Response {
59 let cache_guard = state.schema_cache.load();
60 if cache_guard.is_some() {
61 StatusCode::OK.into_response()
62 } else {
63 StatusCode::SERVICE_UNAVAILABLE.into_response()
64 }
65}
66
67async fn config_handler(State(state): State<AppState>) -> Response {
69 let config = state.config.load();
70 let body = redacted_config(&config);
71 let json = serde_json::to_string_pretty(&body).unwrap_or_else(|_| "{}".to_string());
72 json_response(json)
73}
74
75pub fn redacted_config(config: &crate::config::AppConfig) -> serde_json::Value {
77 serde_json::json!({
78 "db_uri": "***",
79 "db_schemas": config.db_schemas,
80 "db_anon_role": config.db_anon_role,
81 "db_pool_size": config.db_pool_size,
82 "db_channel": config.db_channel,
83 "db_channel_enabled": config.db_channel_enabled,
84 "db_max_rows": config.db_max_rows,
85 "server_host": config.server_host,
86 "server_port": config.server_port,
87 "server_timing_enabled": config.server_timing_enabled,
88 "jwt_secret": if config.jwt_secret.is_some() { "***" } else { "" },
89 "jwt_secret_is_base64": config.jwt_secret_is_base64,
90 "log_level": config.log_level.as_str(),
91 "openapi_mode": config.openapi_mode.as_str(),
92 })
93}
94
95async fn metrics(State(state): State<AppState>) -> Response {
97 let m = &state.metrics;
98 let body = serde_json::json!({
99 "requests_total": m.requests_total.load(Ordering::Relaxed),
100 "requests_success": m.requests_success.load(Ordering::Relaxed),
101 "requests_error": m.requests_error.load(Ordering::Relaxed),
102 "db_queries_total": m.db_queries_total.load(Ordering::Relaxed),
103 "schema_cache_reloads": m.schema_cache_reloads.load(Ordering::Relaxed),
104 "jwt_cache_hits": m.jwt_cache_hits.load(Ordering::Relaxed),
105 "jwt_cache_misses": m.jwt_cache_misses.load(Ordering::Relaxed),
106 "jwt_cache_entries": state.jwt_cache.entry_count(),
107 "pg_version": format!("{}.{}.{}", state.pg_version.major, state.pg_version.minor, state.pg_version.patch),
108 });
109
110 let json = serde_json::to_string_pretty(&body).unwrap_or_else(|_| "{}".to_string());
111 json_response(json)
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::config::AppConfig;
118
119 #[test]
120 fn redacted_config_hides_db_uri() {
121 let config = AppConfig::default();
122 let json = redacted_config(&config);
123 assert_eq!(json["db_uri"], "***");
124 }
125
126 #[test]
127 fn redacted_config_hides_jwt_secret_when_set() {
128 let mut config = AppConfig::default();
129 config.jwt_secret = Some("super-secret-key".to_string());
130 let json = redacted_config(&config);
131 assert_eq!(json["jwt_secret"], "***");
132 }
133
134 #[test]
135 fn redacted_config_empty_jwt_when_none() {
136 let config = AppConfig::default();
137 let json = redacted_config(&config);
138 assert_eq!(json["jwt_secret"], "");
139 }
140
141 #[test]
142 fn redacted_config_preserves_schemas() {
143 let mut config = AppConfig::default();
144 config.db_schemas = vec!["public".to_string(), "api".to_string()];
145 let json = redacted_config(&config);
146 let schemas = json["db_schemas"].as_array().unwrap();
147 assert_eq!(schemas.len(), 2);
148 assert_eq!(schemas[0], "public");
149 assert_eq!(schemas[1], "api");
150 }
151
152 #[test]
153 fn redacted_config_preserves_server_fields() {
154 let mut config = AppConfig::default();
155 config.server_host = "0.0.0.0".to_string();
156 config.server_port = 8080;
157 config.server_timing_enabled = true;
158 let json = redacted_config(&config);
159 assert_eq!(json["server_host"], "0.0.0.0");
160 assert_eq!(json["server_port"], 8080);
161 assert_eq!(json["server_timing_enabled"], true);
162 }
163
164 #[test]
165 fn redacted_config_preserves_db_pool_and_channel() {
166 let mut config = AppConfig::default();
167 config.db_pool_size = 20;
168 config.db_channel = "dbrst".to_string();
169 config.db_channel_enabled = true;
170 config.db_max_rows = Some(1000);
171 let json = redacted_config(&config);
172 assert_eq!(json["db_pool_size"], 20);
173 assert_eq!(json["db_channel"], "dbrst");
174 assert_eq!(json["db_channel_enabled"], true);
175 assert_eq!(json["db_max_rows"], 1000);
176 }
177}