Skip to main content

assay_workflow/api/
mod.rs

1pub mod activities;
2pub mod api_keys;
3pub mod auth;
4pub mod dashboard;
5pub mod events;
6pub mod meta;
7pub mod namespaces;
8pub mod openapi;
9pub mod queues;
10pub mod schedules;
11pub mod tasks;
12pub mod workers;
13pub mod workflow_tasks;
14pub mod workflows;
15
16use std::sync::Arc;
17
18use axum::middleware;
19use axum::Router;
20use tokio::sync::broadcast;
21use tracing::info;
22
23use crate::api::auth::AuthMode;
24use crate::engine::Engine;
25use crate::store::WorkflowStore;
26
27/// Shared state for all API handlers.
28pub struct AppState<S: WorkflowStore> {
29    pub engine: Arc<Engine<S>>,
30    pub event_tx: broadcast::Sender<events::BroadcastEvent>,
31    pub auth_mode: AuthMode,
32    /// Version of the containing binary (e.g. the `assay-lua` CLI) — set
33    /// by embedders so `/api/v1/version` reflects the user-facing
34    /// binary, not this internal engine-crate version. Defaults to the
35    /// `assay-workflow` crate version, which is what the dashboard
36    /// falls back to when an embedder doesn't override.
37    pub binary_version: Option<&'static str>,
38}
39
40/// Build the full API router.
41pub fn router<S: WorkflowStore + 'static>(state: Arc<AppState<S>>) -> Router {
42    let needs_auth = state.auth_mode.is_enabled();
43
44    let api = Router::new()
45        .nest("/api/v1", api_v1_router())
46        .nest("/api/v1", events::router());
47
48    let api = if needs_auth {
49        api.layer(middleware::from_fn_with_state(
50            Arc::clone(&state),
51            auth::auth_middleware,
52        ))
53    } else {
54        api
55    };
56
57    // Dashboard + OpenAPI docs (no auth)
58    let app = api
59        .merge(dashboard::router())
60        .merge(openapi::router());
61
62    app.with_state(state)
63}
64
65fn api_v1_router<S: WorkflowStore + 'static>() -> Router<Arc<AppState<S>>> {
66    Router::new()
67        .merge(workflows::router())
68        .merge(activities::router())
69        .merge(workflow_tasks::router())
70        .merge(tasks::router())
71        .merge(schedules::router())
72        .merge(workers::router())
73        .merge(namespaces::router())
74        .merge(queues::router())
75        .merge(api_keys::router())
76        .merge(meta::router())
77}
78
79/// Start the HTTP server on the given port.
80pub async fn serve<S: WorkflowStore + 'static>(
81    engine: Engine<S>,
82    port: u16,
83    auth_mode: AuthMode,
84) -> anyhow::Result<()> {
85    serve_with_version(engine, port, auth_mode, None).await
86}
87
88/// Like `serve`, but lets the embedder (e.g. the `assay` binary) pass
89/// its own semver so `/api/v1/version` reflects the binary users are
90/// actually running instead of the internal `assay-workflow` crate
91/// version. Without this, the dashboard would show a misleading
92/// "engine crate" version to operators.
93pub async fn serve_with_version<S: WorkflowStore + 'static>(
94    engine: Engine<S>,
95    port: u16,
96    auth_mode: AuthMode,
97    binary_version: Option<&'static str>,
98) -> anyhow::Result<()> {
99    let (event_tx, _) = broadcast::channel(1024);
100
101    let mode_desc = auth_mode.describe();
102
103    let state = Arc::new(AppState {
104        engine: Arc::new(engine),
105        event_tx,
106        auth_mode,
107        binary_version,
108    });
109
110    let app = router(state);
111    let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?;
112    info!("Workflow API listening on 0.0.0.0:{port} (auth: {mode_desc})");
113
114    axum::serve(listener, app).await?;
115    Ok(())
116}