use axum::Json;
use axum::extract::{Path, State};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use vta_sdk::protocols::join_requests::JoinRequestStatusResponseBody;
use vti_common::error::AppError;
use crate::ceremony::Verdict;
use crate::join::{JoinStatus, get_join_request};
use crate::server::AppState;
pub const JOIN_STATUS_DOMAIN_TAG: &[u8] = b"vtc-join-status/v1\0";
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(utoipa::ToSchema)]
pub struct StatusRequestBody {
pub applicant_did: String,
pub signature: String,
}
#[utoipa::path(
post, path = "/join-requests/{id}/status", tag = "join-requests",
params(("id" = String, Path, description = "Join request id")),
request_body = StatusRequestBody,
responses(
(status = 200, description = "Join request lifecycle status", body = JoinRequestStatusResponseBody),
(status = 400, description = "Holder-binding validation failed"),
(status = 404, description = "Join request not found"),
),
)]
pub async fn status(
State(state): State<AppState>,
Path(id): Path<Uuid>,
Json(body): Json<StatusRequestBody>,
) -> Result<Json<JoinRequestStatusResponseBody>, AppError> {
let resp = status_inner(&state, id, body.applicant_did, Some(&body.signature)).await?;
Ok(Json(resp))
}
pub async fn status_inner(
state: &AppState,
id: Uuid,
applicant_did: String,
signature_hex: Option<&str>,
) -> Result<JoinRequestStatusResponseBody, AppError> {
if let Some(hex_sig) = signature_hex {
verify_holder_signature(&applicant_did, id, hex_sig)?;
}
let req = get_join_request(&state.join_requests_ks, id)
.await?
.ok_or_else(|| AppError::NotFound(format!("join request not found: {id}")))?;
if req.applicant_did != applicant_did {
return Err(AppError::Validation(
"applicantDid does not match the join request applicant".into(),
));
}
let (needs, presentation_definition) = if req.status == JoinStatus::Deferred {
match req
.policy_decision
.and_then(|pd| serde_json::from_value::<Verdict>(pd).ok())
{
Some(Verdict::RequestMore(rm)) => (rm.needs, Some(rm.presentation_definition)),
_ => (Vec::new(), None),
}
} else {
(Vec::new(), None)
};
Ok(JoinRequestStatusResponseBody {
request_id: id,
status: req.status.to_string(),
needs,
presentation_definition,
})
}
fn verify_holder_signature(
applicant_did: &str,
request_id: Uuid,
signature_hex: &str,
) -> Result<(), AppError> {
let payload = canonical_payload(applicant_did, request_id)?;
crate::holder_signature::verify_domain_signed(
applicant_did,
JOIN_STATUS_DOMAIN_TAG,
&payload,
signature_hex,
)
.map_err(AppError::Validation)
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CanonicalPayload<'a> {
applicant_did: &'a str,
request_id: String,
}
fn canonical_payload(applicant_did: &str, request_id: Uuid) -> Result<Vec<u8>, AppError> {
serde_json::to_vec(&CanonicalPayload {
applicant_did,
request_id: request_id.to_string(),
})
.map_err(|e| AppError::Internal(format!("canonical payload serialize: {e}")))
}