use axum::Extension;
use axum::Json;
use axum::http::HeaderMap;
use axum::response::{IntoResponse, Response};
use serde::Serialize;
use crate::writer::{epoch_ms_now, rfc3339_from_epoch_ms};
use super::common::{AdminError, AdminState, verify_and_authorize_admin_only};
const LXM: &str = "tools.cairn.admin.getTrustChain";
#[derive(Debug, Serialize)]
struct Output {
#[serde(rename = "serviceDid")]
service_did: String,
#[serde(rename = "signingKeys")]
signing_keys: Vec<SigningKeyEntry>,
maintainers: Vec<MaintainerEntry>,
#[serde(skip_serializing_if = "Option::is_none", rename = "serviceRecord")]
service_record: Option<ServiceRecordSummary>,
instance: InstanceInfo,
}
#[derive(Debug, Serialize)]
struct SigningKeyEntry {
#[serde(rename = "publicKeyMultibase")]
public_key_multibase: String,
#[serde(rename = "validFrom")]
valid_from: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "validTo")]
valid_to: Option<String>,
#[serde(rename = "createdAt")]
created_at: String,
#[serde(rename = "isActive")]
is_active: bool,
}
#[derive(Debug, Serialize)]
struct MaintainerEntry {
did: String,
role: String,
#[serde(rename = "addedAt")]
added_at: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "addedBy")]
added_by: Option<String>,
#[serde(rename = "provenanceAttested")]
provenance_attested: bool,
}
#[derive(Debug, Serialize)]
struct ServiceRecordSummary {
#[serde(rename = "contentHash")]
content_hash: String,
#[serde(rename = "labelValues")]
label_values: Vec<String>,
}
#[derive(Debug, Serialize)]
struct InstanceInfo {
version: String,
#[serde(rename = "serviceEndpoint")]
service_endpoint: String,
}
pub(super) async fn handler(
Extension(state): Extension<AdminState>,
headers: HeaderMap,
) -> Response {
if let Err(e) = verify_and_authorize_admin_only(&state, &headers, LXM).await {
return e.into_response();
}
let signing_keys = match load_signing_keys(&state).await {
Ok(k) => k,
Err(e) => return e.into_response(),
};
let maintainers = match load_maintainers(&state).await {
Ok(m) => m,
Err(e) => return e.into_response(),
};
let service_record = match load_service_record(&state).await {
Ok(r) => r,
Err(e) => return e.into_response(),
};
Json(Output {
service_did: state.config.service_did.clone(),
signing_keys,
maintainers,
service_record,
instance: InstanceInfo {
version: env!("CARGO_PKG_VERSION").to_string(),
service_endpoint: state.config.service_endpoint.clone(),
},
})
.into_response()
}
async fn load_signing_keys(state: &AdminState) -> Result<Vec<SigningKeyEntry>, AdminError> {
let rows = sqlx::query!(
r#"SELECT public_key_multibase, valid_from, valid_to, created_at AS "created_at!: i64"
FROM signing_keys
ORDER BY valid_from ASC"#,
)
.fetch_all(&state.pool)
.await
.map_err(|_| AdminError::Internal)?;
let now_ms = epoch_ms_now();
let now_rfc = rfc3339_from_epoch_ms(now_ms).map_err(|_| AdminError::Internal)?;
let mut out = Vec::with_capacity(rows.len());
for r in rows {
let created_at = rfc3339_from_epoch_ms(r.created_at).map_err(|_| AdminError::Internal)?;
let is_active = match r.valid_to.as_deref() {
None => true,
Some(v) => v.as_bytes() > now_rfc.as_bytes(),
};
out.push(SigningKeyEntry {
public_key_multibase: r.public_key_multibase,
valid_from: r.valid_from,
valid_to: r.valid_to,
created_at,
is_active,
});
}
Ok(out)
}
async fn load_maintainers(state: &AdminState) -> Result<Vec<MaintainerEntry>, AdminError> {
let rows = sqlx::query!(
r#"SELECT did, role, added_by, added_at AS "added_at!: i64"
FROM moderators
ORDER BY added_at ASC, did ASC"#,
)
.fetch_all(&state.pool)
.await
.map_err(|_| AdminError::Internal)?;
let mut out = Vec::with_capacity(rows.len());
for r in rows {
let added_at = rfc3339_from_epoch_ms(r.added_at).map_err(|_| AdminError::Internal)?;
let provenance_attested = r.added_by.is_some();
out.push(MaintainerEntry {
did: r.did,
role: r.role,
added_at,
added_by: r.added_by,
provenance_attested,
});
}
Ok(out)
}
async fn load_service_record(
state: &AdminState,
) -> Result<Option<ServiceRecordSummary>, AdminError> {
let Some(label_values) = state.config.declared_label_values.clone() else {
return Ok(None);
};
let row =
sqlx::query!("SELECT value FROM labeler_config WHERE key = 'service_record_content_hash'",)
.fetch_optional(&state.pool)
.await
.map_err(|_| AdminError::Internal)?;
let Some(row) = row else {
return Ok(None);
};
Ok(Some(ServiceRecordSummary {
content_hash: row.value,
label_values,
}))
}