Skip to main content

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}