use crate::auth::AuthClaims;
use crate::did_ops::{self, LogEntryInfo, LogMetadata};
use crate::error::AppError;
use crate::server::AppState;
use crate::watcher_push::{self, WatcherSyncStatus};
use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use did_hosting_common::DidListEntry;
use serde::{Deserialize, Serialize};
fn clean_mnemonic(m: &str) -> &str {
m.trim_start_matches('/')
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DidDetailResponse {
pub mnemonic: String,
pub created_at: u64,
pub updated_at: u64,
pub version_count: u64,
pub did_id: Option<String>,
pub owner: String,
pub disabled: bool,
pub log: Option<LogMetadata>,
pub watcher_sync: Option<Vec<WatcherSyncStatus>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
}
impl DidDetailResponse {
fn from_record(
record: did_ops::DidRecord,
log: Option<LogMetadata>,
watcher_sync: Option<Vec<WatcherSyncStatus>>,
) -> Self {
let method = (!record.method.is_empty()).then(|| record.method.clone());
let domain = (!record.domain.is_empty()).then(|| record.domain.clone());
Self {
mnemonic: record.mnemonic,
created_at: record.created_at,
updated_at: record.updated_at,
version_count: record.version_count,
did_id: record.did_id,
owner: record.owner,
disabled: record.disabled,
log,
watcher_sync,
method,
domain,
}
}
}
#[cfg_attr(feature = "openapi", utoipa::path(
get,
path = "/api/dids/{mnemonic}",
tag = "dids",
params(("mnemonic" = String, Path, description = "Slot path (mnemonic); may contain '/'")),
responses(
(status = 200, description = "DID metadata + log summary", content_type = "application/json"),
(status = 401, description = "Missing/invalid bearer token"),
(status = 403, description = "Caller not authorized for this slot"),
(status = 404, description = "No such DID slot"),
),
security(("bearer" = [])),
))]
pub async fn get_did(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<Json<DidDetailResponse>, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
let result = did_ops::get_did_info(&auth, &state, mnemonic).await?;
let watcher_sync: Option<Vec<WatcherSyncStatus>> = state
.dids_ks
.get(did_ops::watcher_sync_key(mnemonic))
.await?;
Ok(Json(DidDetailResponse::from_record(
result.record,
result.log_metadata,
watcher_sync,
)))
}
#[cfg_attr(feature = "openapi", utoipa::path(
get,
path = "/api/log/{mnemonic}",
tag = "dids",
params(("mnemonic" = String, Path, description = "Slot path (mnemonic); may contain '/'")),
responses(
(status = 200, description = "Parsed log entries (versionId, versionTime, state, parameters)", content_type = "application/json"),
(status = 401, description = "Missing/invalid bearer token"),
(status = 403, description = "Caller not authorized for this slot"),
(status = 404, description = "No such DID slot"),
),
security(("bearer" = [])),
))]
pub async fn get_did_log(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<Json<Vec<LogEntryInfo>>, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
let entries = did_ops::get_did_log(&auth, &state, mnemonic).await?;
Ok(Json(entries))
}
#[cfg_attr(feature = "openapi", utoipa::path(
put,
path = "/api/dids/{mnemonic}",
tag = "dids",
params(("mnemonic" = String, Path, description = "Slot path (mnemonic); may contain '/'")),
request_body(
content = String,
description = "did:webvh log — one JSON LogEntry per line (JSONL)",
content_type = "application/jsonl+json",
),
responses(
(status = 204, description = "Published; content stored and watchers notified"),
(status = 400, description = "Invalid did.jsonl, failed proof verification, or DID id/host mismatch"),
(status = 401, description = "Missing/invalid bearer token"),
(status = 403, description = "Caller not authorized for this slot"),
),
security(("bearer" = [])),
))]
pub async fn upload_did(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
body: String,
) -> Result<StatusCode, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
did_ops::publish_did(&auth, &state, mnemonic, &body).await?;
watcher_push::notify_watchers_did(
&state.config,
&state.http_client,
&state.dids_ks,
mnemonic.to_string(),
);
Ok(StatusCode::NO_CONTENT)
}
#[cfg_attr(feature = "openapi", utoipa::path(
put,
path = "/api/witness/{mnemonic}",
tag = "dids",
params(("mnemonic" = String, Path, description = "Slot path (mnemonic); may contain '/'")),
request_body(
content = String,
description = "Witness-proof document (did-witness.json) for the slot's current log",
content_type = "application/json",
),
responses(
(status = 204, description = "Witness proofs stored"),
(status = 400, description = "Invalid witness document"),
(status = 401, description = "Missing/invalid bearer token"),
(status = 403, description = "Caller not authorized for this slot"),
),
security(("bearer" = [])),
))]
pub async fn upload_witness(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
body: String,
) -> Result<StatusCode, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
did_ops::upload_witness(&auth, &state, mnemonic, &body).await?;
watcher_push::notify_watchers_did(
&state.config,
&state.http_client,
&state.dids_ks,
mnemonic.to_string(),
);
Ok(StatusCode::NO_CONTENT)
}
#[cfg_attr(feature = "openapi", utoipa::path(
delete,
path = "/api/dids/{mnemonic}",
tag = "dids",
params(("mnemonic" = String, Path, description = "Slot path (mnemonic); may contain '/'")),
responses(
(status = 204, description = "Soft-deleted; content retained for the recovery window"),
(status = 401, description = "Missing/invalid bearer token"),
(status = 403, description = "Caller not authorized for this slot"),
(status = 404, description = "No such DID slot"),
),
security(("bearer" = [])),
))]
pub async fn delete_did(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<StatusCode, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
did_ops::delete_did(&auth, &state, mnemonic).await?;
watcher_push::notify_watchers_delete(
&state.config,
&state.http_client,
&state.dids_ks,
mnemonic.to_string(),
);
Ok(StatusCode::NO_CONTENT)
}
pub async fn disable_did(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<StatusCode, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
did_ops::set_did_disabled(&auth, &state, mnemonic, true).await?;
watcher_push::notify_watchers_did(
&state.config,
&state.http_client,
&state.dids_ks,
mnemonic.to_string(),
);
Ok(StatusCode::NO_CONTENT)
}
pub async fn enable_did(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<StatusCode, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
did_ops::set_did_disabled(&auth, &state, mnemonic, false).await?;
watcher_push::notify_watchers_did(
&state.config,
&state.http_client,
&state.dids_ks,
mnemonic.to_string(),
);
Ok(StatusCode::NO_CONTENT)
}
pub async fn get_raw_log(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<
(
StatusCode,
[(axum::http::HeaderName, &'static str); 1],
String,
),
AppError,
> {
let mnemonic = clean_mnemonic(&mnemonic);
let content = did_ops::get_raw_log(&auth, &state, mnemonic).await?;
Ok((
StatusCode::OK,
[(
axum::http::header::CONTENT_TYPE,
"text/plain; charset=utf-8",
)],
content,
))
}
#[derive(Debug, Deserialize)]
pub struct ListDidsQuery {
pub owner: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[cfg_attr(feature = "openapi", utoipa::path(
get,
path = "/api/dids",
tag = "dids",
params(
("owner" = Option<String>, Query, description = "Filter by owner DID (admin only)"),
("limit" = Option<usize>, Query, description = "Page size"),
("offset" = Option<usize>, Query, description = "Page offset"),
),
responses(
(status = 200, description = "List of hosted DID slots", content_type = "application/json"),
(status = 401, description = "Missing/invalid bearer token"),
),
security(("bearer" = [])),
))]
pub async fn list_dids(
auth: AuthClaims,
State(state): State<AppState>,
Query(query): Query<ListDidsQuery>,
) -> Result<Json<Vec<DidListEntry>>, AppError> {
let entries = did_ops::list_dids(
&auth,
&state,
query.owner.as_deref(),
query.limit,
query.offset,
)
.await?;
Ok(Json(entries))
}