use axum::Router;
use axum::response::Redirect;
use axum::routing::get;
use std::sync::Arc;
use tracing::info;
use assay_domain::events::EngineEventBus;
use assay_workflow::events::WorkflowEventBus;
use assay_workflow::{WorkflowCtx, WorkflowStore};
use crate::state::EngineState;
const WORKFLOW_PUBLIC_PATHS: &[&str] = &[
"/api/v1/engine/workflow/health",
"/api/v1/engine/workflow/version",
"/api/v1/engine/workflow/openapi.json",
"/api/v1/engine/workflow/docs",
];
pub fn build_app<S: WorkflowStore + Clone + 'static>(state: EngineState<S>) -> Router {
let workflow_router = assay_workflow::api::router(Arc::clone(&state.workflow));
let workflow_router = if state.auth.is_some() {
workflow_router.layer(axum::middleware::from_fn_with_state(
state.clone(),
workflow_gate_middleware::<S>,
))
} else {
workflow_router
};
let dashboard_router =
assay_dashboard::workflow_router().with_state(Arc::clone(&state.dashboard));
let healthz = Router::new().route(
"/healthz",
get(|| async { Redirect::permanent("/api/v1/engine/core/health") }),
);
let engine_api_router = crate::engine_api::router::<S>().with_state(state.clone());
let engine_console_router = assay_dashboard::engine_router();
let mut app = workflow_router
.merge(dashboard_router)
.merge(healthz)
.merge(engine_api_router)
.merge(engine_console_router);
if state.auth.is_some() {
let spec_router =
assay_auth::oidc_spec_router::<EngineState<S>>().with_state(state.clone());
app = app.nest("/auth", spec_router);
let engine_auth_router =
assay_auth::engine_auth_router::<EngineState<S>>().with_state(state.clone());
app = app.nest("/api/v1/engine/auth", engine_auth_router);
let asset_router = assay_dashboard::auth_router();
app = app.merge(asset_router);
}
#[cfg(feature = "vault")]
if state.vault.is_some() {
let vault = assay_vault::router::vault_router::<EngineState<S>>().with_state(state.clone());
app = app.nest("/api/v1/vault", vault);
}
#[cfg(all(feature = "vault", feature = "vault-bitwarden-compat"))]
if state.vault.is_some() && state.auth.is_some() {
let bw =
assay_vault::bitwarden_compat::router::<EngineState<S>>().with_state(state.clone());
app = app.merge(bw);
}
#[cfg(feature = "vault")]
{
app = app.merge(assay_dashboard::vault_router());
}
app
}
async fn workflow_gate_middleware<S: WorkflowStore + Clone + 'static>(
axum::extract::State(state): axum::extract::State<EngineState<S>>,
request: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
let path = request.uri().path();
if WORKFLOW_PUBLIC_PATHS
.iter()
.any(|p| path == *p || path.starts_with(&format!("{p}/")))
{
return next.run(request).await;
}
let Some(auth) = state.auth.as_ref() else {
return (
axum::http::StatusCode::SERVICE_UNAVAILABLE,
axum::Json(serde_json::json!({
"error": "service_unavailable",
"error_description": "auth not configured on this engine instance",
})),
)
.into_response();
};
let namespace = request
.uri()
.query()
.and_then(|q| {
url::form_urlencoded::parse(q.as_bytes())
.find(|(k, _)| k == "namespace")
.map(|(_, v)| v.into_owned())
})
.unwrap_or_else(|| "main".to_string());
let keys = crate::state::AdminApiKeys(Arc::clone(&state.admin_api_keys));
let headers = request.headers().clone();
if let Err(r) =
assay_auth::gate::require_role_for(&headers, auth, &keys, "workflow", &namespace, "access")
.await
{
return *r;
}
next.run(request).await
}
use axum::response::IntoResponse;
pub async fn serve<S: WorkflowStore + Clone + 'static>(
bind_addr: &str,
state: EngineState<S>,
) -> anyhow::Result<()> {
let app = build_app(state);
bind_and_serve(bind_addr, app).await
}
pub async fn bind_and_serve(bind_addr: &str, app: axum::Router) -> anyhow::Result<()> {
let listener = tokio::net::TcpListener::bind(bind_addr)
.await
.map_err(|e| anyhow::anyhow!("bind {bind_addr}: {e}"))?;
let actual = listener.local_addr()?;
info!(target: "assay-engine", %actual, "listening");
axum::serve(listener, app).await?;
Ok(())
}
pub fn build_workflow_ctx<S: WorkflowStore + 'static>(store: S) -> Arc<WorkflowCtx<S>> {
let ctx = WorkflowCtx::start(Arc::new(store)).with_binary_version(env!("CARGO_PKG_VERSION"));
Arc::new(ctx)
}
pub fn build_workflow_ctx_with_bus<S: WorkflowStore + 'static>(
store: S,
bus: Arc<dyn EngineEventBus>,
) -> Arc<WorkflowCtx<S>> {
let ctx = WorkflowCtx::start(Arc::new(store))
.with_binary_version(env!("CARGO_PKG_VERSION"))
.with_event_bus(WorkflowEventBus::new(bus));
Arc::new(ctx)
}