kipuka/routes/admin/
cas.rs1use std::sync::Arc;
7
8use axum::Json;
9use axum::extract::{Path, State};
10use axum::http::StatusCode;
11use axum::response::{IntoResponse, Response};
12use serde::Serialize;
13
14use super::AdminAuth;
15use crate::state::AppState;
16
17#[derive(Serialize)]
19pub struct CaSummary {
20 pub id: String,
22 pub is_default: bool,
24 pub key_type: String,
26 pub hash_algorithm: String,
28 pub validity_days: u32,
30 pub health: String,
32 pub hsm_backed: bool,
34}
35
36#[derive(Serialize)]
38pub struct CaDetail {
39 #[serde(flatten)]
40 pub summary: CaSummary,
41 pub subject_cn: String,
43 pub crl_url: Option<String>,
45 pub ocsp_url: Option<String>,
47 pub cab_forum_compliant: bool,
49}
50
51pub async fn list_cas(_admin: AdminAuth, State(state): State<Arc<AppState>>) -> Response {
56 let mut cas = Vec::new();
57
58 for ca_config in &state.config.cas {
59 let health = state
61 .ha_manager
62 .as_ref()
63 .and_then(|ha| {
64 let ca_id = crate::ha::CaId(ca_config.id.clone());
65 ha.pool()
66 .status_snapshot()
67 .get(&ca_id)
68 .map(|s| format!("{:?}", s.health))
69 })
70 .unwrap_or_else(|| "unknown".to_string());
71
72 cas.push(CaSummary {
73 id: ca_config.id.clone(),
74 is_default: ca_config.is_default,
75 key_type: ca_config.key_type.clone(),
76 hash_algorithm: ca_config.hash_algorithm.clone(),
77 validity_days: ca_config.validity_days,
78 health,
79 hsm_backed: ca_config.is_hsm_backed(),
80 });
81 }
82
83 (StatusCode::OK, Json(cas)).into_response()
84}
85
86pub async fn get_ca(
91 _admin: AdminAuth,
92 Path(id): Path<String>,
93 State(state): State<Arc<AppState>>,
94) -> Response {
95 let ca_config = match state.config.cas.iter().find(|c| c.id == id) {
96 Some(c) => c,
97 None => return (StatusCode::NOT_FOUND, "CA not found").into_response(),
98 };
99
100 let health = state
101 .ha_manager
102 .as_ref()
103 .and_then(|ha| {
104 ha.pool()
105 .status_snapshot()
106 .get(&id)
107 .map(|s| format!("{:?}", s.health))
108 })
109 .unwrap_or_else(|| "unknown".to_string());
110
111 let detail = CaDetail {
112 summary: CaSummary {
113 id: ca_config.id.clone(),
114 is_default: ca_config.is_default,
115 key_type: ca_config.key_type.clone(),
116 hash_algorithm: ca_config.hash_algorithm.clone(),
117 validity_days: ca_config.validity_days,
118 health,
119 hsm_backed: ca_config.is_hsm_backed(),
120 },
121 subject_cn: ca_config.common_name.clone(),
122 crl_url: ca_config.crl_url.clone(),
123 ocsp_url: ca_config.ocsp_url.clone(),
124 cab_forum_compliant: ca_config.cab_forum_compliant,
125 };
126
127 (StatusCode::OK, Json(detail)).into_response()
128}
129
130pub async fn get_ca_health(
135 _admin: AdminAuth,
136 Path(id): Path<String>,
137 State(state): State<Arc<AppState>>,
138) -> Response {
139 if !state.config.cas.iter().any(|c| c.id == id) {
141 return (StatusCode::NOT_FOUND, "CA not found").into_response();
142 }
143
144 let (health, latency_ms) = match state.ha_manager.as_ref() {
145 Some(ha) => {
146 let ca_id_key = crate::ha::CaId(id.clone());
147 let snapshot = ha.pool().status_snapshot();
148 match snapshot.get(&ca_id_key) {
149 Some(status) => {
150 let health = format!("{:?}", status.health);
151 let latency = status.latency_ema_ms as u64;
152 (health, latency)
153 }
154 None => ("unknown".to_string(), 0),
155 }
156 }
157 None => ("not_monitored".to_string(), 0),
158 };
159
160 (
161 StatusCode::OK,
162 Json(serde_json::json!({
163 "ca_id": id,
164 "health": health,
165 "latency_ms": latency_ms,
166 })),
167 )
168 .into_response()
169}