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