#[cfg(feature = "server")] use axum::{ extract::State, http::StatusCode, response::IntoResponse, routing::{get, post}, Json, Router, }; use std::sync::Arc; use super::handlers; use super::{ApiResponse, HealthResponse, SealRequest, SealStatus}; use crate::core::VaultCore; /// Build the API router with all mounted engines and system endpoints #[cfg(feature = "server")] pub fn build_router(vault: Arc) -> Router> { let mut router = Router::new() // System endpoints .route("/v1/sys/health", get(sys_health)) .route("/v1/sys/status", get(sys_status)) .route("/v1/sys/seal", post(sys_seal)) .route("/v1/sys/unseal", post(sys_unseal)) .route("/v1/sys/mounts", get(sys_list_mounts)) .route("/v1/sys/init", get(sys_init_status)) // Metrics endpoint (Prometheus format) .route("/metrics", get(metrics_endpoint)) .with_state(vault.clone()); // Dynamically mount routes for each registered engine for (mount_path, _engine) in vault.engines.iter() { let mount_clean = mount_path.trim_end_matches('/'); let wildcard_path = format!("/v1{mount_clean}/*path"); router = router.route( &wildcard_path, get(handlers::read_secret) .post(handlers::write_secret) .delete(handlers::delete_secret) .put(handlers::update_secret), ); // Also add route without trailing path let base_path = format!("/v1{mount_clean}"); router = router.route( &base_path, get(handlers::read_secret) .post(handlers::write_secret) .delete(handlers::delete_secret) .put(handlers::update_secret), ); } router } /// GET /v1/sys/health - Health check endpoint #[cfg(feature = "server")] async fn sys_health(State(vault): State>) -> impl IntoResponse { let sealed = { let seal = vault.seal.blocking_lock(); seal.is_sealed() }; let response = ApiResponse::success(HealthResponse { sealed, initialized: true, }); (StatusCode::OK, Json(response)) } /// POST /v1/sys/seal - Seal the vault #[cfg(feature = "server")] async fn sys_seal(State(vault): State>) -> impl IntoResponse { let mut seal = vault.seal.lock().await; seal.seal(); let response = ApiResponse::success(SealStatus { sealed: true, shares_needed: None, }); (StatusCode::OK, Json(response)) } /// POST /v1/sys/unseal - Unseal the vault with shares #[cfg(feature = "server")] async fn sys_unseal( State(vault): State>, Json(payload): Json, ) -> impl IntoResponse { if let Some(shares) = payload.shares { let shares_data: Vec<&[u8]> = shares.iter().map(|s| s.as_bytes()).collect(); let mut seal = vault.seal.lock().await; match seal.unseal(&shares_data) { Ok(_) => { let response = ApiResponse::success(SealStatus { sealed: seal.is_sealed(), shares_needed: None, }); (StatusCode::OK, Json(response)).into_response() } Err(e) => { let response = ApiResponse::::error(format!("Unseal failed: {}", e)); (StatusCode::BAD_REQUEST, Json(response)).into_response() } } } else { let response = ApiResponse::<()>::error("Missing shares in request"); (StatusCode::BAD_REQUEST, Json(response)).into_response() } } /// GET /v1/sys/status - Get vault status #[cfg(feature = "server")] async fn sys_status(State(vault): State>) -> impl IntoResponse { let sealed = { let seal = vault.seal.blocking_lock(); seal.is_sealed() }; let response = ApiResponse::success(serde_json::json!({ "sealed": sealed, "initialized": true, "engines": vault.engines.keys().collect::>(), })); (StatusCode::OK, Json(response)) } /// GET /v1/sys/mounts - List all mounted engines #[cfg(feature = "server")] async fn sys_list_mounts(State(vault): State>) -> impl IntoResponse { let mut mounts = serde_json::Map::new(); for (path, engine) in vault.engines.iter() { let mount_info = serde_json::json!({ "type": engine.engine_type(), "name": engine.name(), "path": path, }); mounts.insert(path.clone(), mount_info); } let response = ApiResponse::success(serde_json::Value::Object(mounts)); (StatusCode::OK, Json(response)) } /// GET /v1/sys/init - Get initialization status #[cfg(feature = "server")] async fn sys_init_status(State(vault): State>) -> impl IntoResponse { let _seal = vault.seal.blocking_lock(); let response = ApiResponse::success(serde_json::json!({ "initialized": true, })); (StatusCode::OK, Json(response)) } /// GET /metrics - Prometheus metrics endpoint #[cfg(feature = "server")] async fn metrics_endpoint(State(vault): State>) -> impl IntoResponse { let snapshot = vault.metrics.snapshot(); let metrics_text = snapshot.to_prometheus_text(); ( StatusCode::OK, [("Content-Type", "text/plain; version=0.0.4")], metrics_text, ) } #[cfg(not(feature = "server"))] pub fn build_router(_vault: Arc) -> Router<()> { Router::new() } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn test_api_response_success() { let response = ApiResponse::success(json!({"key": "value"})); assert_eq!(response.status, "success"); assert!(response.error.is_none()); } #[test] fn test_api_response_error() { let response = ApiResponse::::error("Something went wrong"); assert_eq!(response.status, "error"); assert!(response.data.is_none()); assert!(response.error.is_some()); } #[test] fn test_health_response() { let health = HealthResponse { sealed: false, initialized: true, }; assert!(!health.sealed); assert!(health.initialized); } #[test] fn test_seal_status() { let status = SealStatus { sealed: true, shares_needed: Some(2), }; assert!(status.sealed); assert_eq!(status.shares_needed, Some(2)); } }