use axum::body::Bytes;
use axum::extract::{Extension, State};
use axum::http::{HeaderMap, StatusCode, header};
use axum::response::{IntoResponse, Response};
use crate::admin_events::{StreamSlot, accepts_event_stream};
use crate::member_portal::oob::HttpContext;
use crate::routing::trust_task_admission::{Admission, ClientAddress, TrustTaskLimits};
use crate::server::AppState;
use crate::trust_tasks::{JoinAuthCtx, dispatch_trust_task_core_admitted};
pub const USER_ACTIVITY_HEADER: &str = "x-vtc-user-activity";
#[utoipa::path(
post, path = "/trust-tasks", tag = "trust-tasks",
request_body(
content = String,
description = "A Trust Task document (trust_tasks_rs::TrustTask JSON)",
),
responses(
(status = 200, description = "Trust Task #response document"),
(status = 400, description = "Malformed document / payload, or a document larger than its type accepts (trust-task-error)"),
(status = 403, description = "Holder auth / VIC verification failed (trust-task-error)"),
(status = 422, description = "Task failed, e.g. duplicate request (trust-task-error)"),
),
)]
pub async fn dispatch(
State(state): State<AppState>,
Extension(ClientAddress(address)): Extension<ClientAddress>,
Extension(limits): Extension<TrustTaskLimits>,
headers: HeaderMap,
body: Bytes,
) -> Response {
let admission = match Admission::begin(&state, &limits, address, &body).await {
Ok(admission) => admission,
Err(limited) => return limited.into_response(),
};
let slot = StreamSlot::new(
accepts_event_stream(headers.get(header::ACCEPT).and_then(|v| v.to_str().ok())),
headers
.get("last-event-id")
.and_then(|v| v.to_str().ok())
.map(str::to_string),
);
let header = |name: header::HeaderName| {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.map(str::to_string)
};
let http = HttpContext::new(
address,
header(header::USER_AGENT),
header(header::ORIGIN),
header(header::HOST),
);
let outcome = HttpContext::scope(
http.clone(),
slot
.scope(Box::pin(admission.scope(
dispatch_trust_task_core_admitted(
&state,
&JoinAuthCtx::rest(),
&body,
&admission,
address,
),
))),
)
.await;
let set_cookies = http.take_cookies();
if outcome.status == StatusCode::OK
&& headers.contains_key(USER_ACTIVITY_HEADER)
&& let Some(principal) = admission.verified_principal()
{
vti_common::auth::touch_cookie_session_for(&headers, &state, principal).await;
}
match admission.finish() {
Ok(()) => match slot.take() {
Some(grant) if outcome.status == StatusCode::OK => {
crate::admin_events::respond(state, grant, outcome.body)
}
_ => {
let mut response = outcome.into_response();
if is_oob_document(&body) {
response.headers_mut().insert(
header::CACHE_CONTROL,
header::HeaderValue::from_static("no-store"),
);
}
if !set_cookies.is_empty()
&& let Err(e) =
crate::member_portal::cookies::append(response.headers_mut(), set_cookies)
{
return e.into_response();
}
response
}
},
Err(limited) => limited.into_response(),
}
}
fn is_oob_document(body: &[u8]) -> bool {
serde_json::from_slice::<serde_json::Value>(body)
.ok()
.and_then(|v| v.get("type").and_then(|t| t.as_str()).map(str::to_string))
.is_some_and(|t| t.starts_with("https://trusttasks.org/spec/auth/oob/"))
}