fraiseql_server/routes/studio/admin.rs
1//! Admin API endpoints for the Studio dashboard.
2//!
3//! All routes are grouped under `/admin/v1/*` and protected by the existing
4//! `bearer_auth_middleware` (reusing the same admin token from `ServerConfig`).
5
6use axum::{Json, extract::State};
7use fraiseql_core::db::traits::DatabaseAdapter;
8use serde::{Deserialize, Serialize};
9
10use crate::routes::graphql::app_state::AppState;
11
12// ---------------------------------------------------------------------------
13// Response shapes (agreed with Luxen UI author per phase spec)
14// ---------------------------------------------------------------------------
15
16/// Response from `GET /admin/v1/health/detailed`.
17#[derive(Debug, Serialize, Deserialize, Clone)]
18pub struct AdminHealthResponse {
19 /// Server uptime in seconds since startup.
20 pub uptime_secs: u64,
21 /// Binary version string (e.g. `"2.2.0"`).
22 pub version: String,
23 /// Number of active database connections in the pool.
24 pub pool_active: u32,
25 /// Number of idle database connections in the pool.
26 pub pool_idle: u32,
27 /// Maximum pool size.
28 pub pool_max: u32,
29 /// Query cache hit rate (0–1), or `None` if cache is disabled.
30 pub cache_hit_rate: Option<f64>,
31 /// Current cache entry count, or `None` if cache is disabled.
32 pub cache_entries: Option<u64>,
33}
34
35/// Response from `GET /admin/v1/schema`.
36#[derive(Debug, Serialize, Deserialize, Clone)]
37pub struct AdminSchemaResponse {
38 /// Compiled schema as raw JSON value.
39 pub schema: serde_json::Value,
40}
41
42// ---------------------------------------------------------------------------
43// Token extraction helper (public for testing)
44// ---------------------------------------------------------------------------
45
46/// Extract the bearer token from an `Authorization` header value.
47///
48/// Returns `Some(token)` for `"Bearer <token>"` headers; `None` otherwise.
49#[must_use]
50pub fn extract_bearer_token(auth_header: Option<&str>) -> Option<&str> {
51 let header = auth_header?;
52 header.strip_prefix("Bearer ")
53}
54
55// ---------------------------------------------------------------------------
56// Handlers
57// ---------------------------------------------------------------------------
58
59/// `GET /admin/v1/schema` — compiled schema as JSON.
60///
61/// Protected by `bearer_auth_middleware` applied in the router layer.
62///
63/// # Errors
64///
65/// Returns `401` without valid admin credentials (enforced by middleware).
66pub async fn schema_handler<A>(
67 State(state): State<AppState<A>>,
68) -> impl axum::response::IntoResponse
69where
70 A: DatabaseAdapter + Clone + Send + Sync + 'static,
71{
72 let schema = state.executor.load().schema().clone();
73 let value = serde_json::to_value(&schema).unwrap_or(serde_json::Value::Null);
74 Json(AdminSchemaResponse { schema: value })
75}
76
77/// `GET /admin/v1/health/detailed` — pool stats, cache stats, uptime, version.
78///
79/// Protected by `bearer_auth_middleware` applied in the router layer.
80///
81/// # Errors
82///
83/// Returns `401` without valid admin credentials (enforced by middleware).
84pub async fn health_handler<A>(
85 State(_state): State<AppState<A>>,
86) -> impl axum::response::IntoResponse
87where
88 A: DatabaseAdapter + Clone + Send + Sync + 'static,
89{
90 let uptime_secs = std::time::SystemTime::now()
91 .duration_since(std::time::UNIX_EPOCH)
92 .map(|d| d.as_secs())
93 .unwrap_or(0);
94
95 Json(AdminHealthResponse {
96 uptime_secs,
97 version: env!("CARGO_PKG_VERSION").to_string(),
98 pool_active: 0,
99 pool_idle: 0,
100 pool_max: 0,
101 cache_hit_rate: None,
102 cache_entries: None,
103 })
104}