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 namespaces;
7pub mod openapi;
8pub mod public;
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.
41///
42/// Three tiers:
43///   1. **Authenticated `/api/v1/*`** — workflows, schedules, namespaces,
44///      activities, tasks, workers, queues, api-keys, events, meta/version.
45///      Gated by `auth::auth_middleware` when an auth mode is enabled.
46///   2. **Public `/api/v1/*`** — health, version. Always unauthenticated so
47///      Kubernetes probes, load balancers, and third-party monitors can
48///      reach them without a bearer token.
49///   3. **Dashboard + OpenAPI** — HTML/JSON at the root. Always public.
50pub fn router<S: WorkflowStore + 'static>(state: Arc<AppState<S>>) -> Router {
51    let needs_auth = state.auth_mode.is_enabled();
52
53    let authed_api = Router::new()
54        .nest("/api/v1", api_v1_router())
55        .nest("/api/v1", events::router());
56
57    let authed_api = if needs_auth {
58        authed_api.layer(middleware::from_fn_with_state(
59            Arc::clone(&state),
60            auth::auth_middleware,
61        ))
62    } else {
63        authed_api
64    };
65
66    // Public /api/v1/* routes — outside the auth layer by construction.
67    let public_api = Router::new().nest("/api/v1", public::router());
68
69    let app = authed_api
70        .merge(public_api)
71        .merge(dashboard::router())
72        .merge(openapi::router());
73
74    app.with_state(state)
75}
76
77fn api_v1_router<S: WorkflowStore + 'static>() -> Router<Arc<AppState<S>>> {
78    Router::new()
79        .merge(workflows::router())
80        .merge(activities::router())
81        .merge(workflow_tasks::router())
82        .merge(tasks::router())
83        .merge(schedules::router())
84        .merge(workers::router())
85        .merge(namespaces::router())
86        .merge(queues::router())
87        .merge(api_keys::router())
88}
89
90/// Start the HTTP server on the given port.
91pub async fn serve<S: WorkflowStore + 'static>(
92    engine: Engine<S>,
93    port: u16,
94    auth_mode: AuthMode,
95) -> anyhow::Result<()> {
96    serve_with_version(engine, port, auth_mode, None).await
97}
98
99/// Like `serve`, but lets the embedder (e.g. the `assay` binary) pass
100/// its own semver so `/api/v1/version` reflects the binary users are
101/// actually running instead of the internal `assay-workflow` crate
102/// version. Without this, the dashboard would show a misleading
103/// "engine crate" version to operators.
104pub async fn serve_with_version<S: WorkflowStore + 'static>(
105    engine: Engine<S>,
106    port: u16,
107    auth_mode: AuthMode,
108    binary_version: Option<&'static str>,
109) -> anyhow::Result<()> {
110    let (event_tx, _) = broadcast::channel(1024);
111
112    let mode_desc = auth_mode.describe();
113
114    let state = Arc::new(AppState {
115        engine: Arc::new(engine),
116        event_tx,
117        auth_mode,
118        binary_version,
119    });
120
121    let app = router(state);
122    let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?;
123    info!("Workflow API listening on 0.0.0.0:{port} (auth: {mode_desc})");
124
125    axum::serve(listener, app).await?;
126    Ok(())
127}