Skip to main content

assay_workflow/api/
mod.rs

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