1pub mod cache;
2pub mod callback_router;
3pub mod error;
4pub mod handlers;
5pub mod state;
6
7pub use callback_router::{
8 callback_router, CallbackAuth, CallbackReceiverConfig, CallbackRouterBuildError,
9};
10
11use std::time::Duration;
12
13use axum::http::header;
14use axum::response::{Html, IntoResponse, Response};
15use axum::routing::{delete, get, patch, post};
16use axum::Router;
17use rust_embed::Embed;
18use sqlx::PgPool;
19use tower_http::cors::CorsLayer;
20
21use crate::state::{AppState, ReadOnlyMode};
22
23#[derive(Embed)]
24#[folder = "static/"]
25struct StaticAssets;
26
27pub async fn router(pool: PgPool, cache_ttl: Duration) -> Result<Router, sqlx::Error> {
32 router_with(pool, cache_ttl, None, ReadOnlyMode::Auto).await
33}
34
35pub async fn router_with_callback_secret(
40 pool: PgPool,
41 cache_ttl: Duration,
42 callback_hmac_secret: Option<[u8; 32]>,
43) -> Result<Router, sqlx::Error> {
44 router_with(pool, cache_ttl, callback_hmac_secret, ReadOnlyMode::Auto).await
45}
46
47pub async fn router_with(
57 pool: PgPool,
58 cache_ttl: Duration,
59 callback_hmac_secret: Option<[u8; 32]>,
60 read_only_mode: ReadOnlyMode,
61) -> Result<Router, sqlx::Error> {
62 let read_only = read_only_mode.resolve(&pool).await?;
63 let state = AppState::new(pool, read_only, cache_ttl, callback_hmac_secret);
64 let api = Router::new()
65 .route("/jobs", get(handlers::jobs::list_jobs))
67 .route("/jobs/{id}", get(handlers::jobs::get_job))
68 .route("/jobs/{id}/retry", post(handlers::jobs::retry_job))
69 .route("/jobs/{id}/cancel", post(handlers::jobs::cancel_job))
70 .route("/jobs/bulk-retry", post(handlers::jobs::bulk_retry))
71 .route("/jobs/bulk-cancel", post(handlers::jobs::bulk_cancel))
72 .route(
74 "/batch-ops",
75 get(handlers::batch_ops::list_batch_operations),
76 )
77 .route(
78 "/batch-ops",
79 post(handlers::batch_ops::submit_batch_operation),
80 )
81 .route(
82 "/batch-ops/preview",
83 post(handlers::batch_ops::preview_batch_operation),
84 )
85 .route(
86 "/batch-ops/{id}",
87 get(handlers::batch_ops::get_batch_operation),
88 )
89 .route(
90 "/batch-ops/{id}",
91 patch(handlers::batch_ops::patch_batch_operation),
92 )
93 .route("/queues", get(handlers::queues::list_queues))
95 .route(
96 "/queues/runtime",
97 get(handlers::runtime::list_queue_runtime),
98 )
99 .route("/queues/{queue}", get(handlers::queues::get_queue))
100 .route("/queues/{queue}/pause", post(handlers::queues::pause_queue))
101 .route(
102 "/queues/{queue}/resume",
103 post(handlers::queues::resume_queue),
104 )
105 .route("/queues/{queue}/drain", post(handlers::queues::drain_queue))
106 .route("/kinds", get(handlers::kinds::list_kinds))
108 .route("/cron", get(handlers::cron::list_cron_jobs))
110 .route(
111 "/cron/{name}/trigger",
112 post(handlers::cron::trigger_cron_job),
113 )
114 .route("/cron/{name}/pause", post(handlers::cron::pause_cron_job))
115 .route("/cron/{name}/resume", post(handlers::cron::resume_cron_job))
116 .route("/stats", get(handlers::stats::get_stats))
118 .route("/stats/timeseries", get(handlers::stats::get_timeseries))
119 .route("/stats/kinds", get(handlers::stats::get_distinct_kinds))
120 .route("/stats/queues", get(handlers::stats::get_distinct_queues))
121 .route("/capabilities", get(handlers::stats::get_capabilities))
122 .route("/dlq", get(handlers::dlq::list_dlq))
124 .route("/dlq/depth", get(handlers::dlq::dlq_depth))
125 .route("/dlq/{id}", get(handlers::dlq::get_dlq_job))
126 .route("/dlq/{id}", delete(handlers::dlq::purge_dlq_job))
127 .route("/dlq/{id}/retry", post(handlers::dlq::retry_dlq_job))
128 .route("/dlq/bulk-retry", post(handlers::dlq::bulk_retry_dlq))
129 .route("/dlq/bulk-purge", post(handlers::dlq::bulk_purge_dlq))
130 .route("/dlq/bulk-move", post(handlers::dlq::bulk_move_failed))
131 .route("/runtime", get(handlers::runtime::get_runtime))
133 .route("/storage", get(handlers::runtime::get_storage))
134 .route(
136 "/callbacks/{callback_id}/complete",
137 post(handlers::callbacks::complete_callback),
138 )
139 .route(
140 "/callbacks/{callback_id}/fail",
141 post(handlers::callbacks::fail_callback),
142 )
143 .route(
144 "/callbacks/{callback_id}/heartbeat",
145 post(handlers::callbacks::heartbeat_callback),
146 );
147
148 Ok(Router::new()
149 .nest("/api", api)
150 .fallback(static_handler)
151 .layer(CorsLayer::permissive())
152 .with_state(state))
153}
154
155async fn static_handler(uri: axum::http::Uri) -> Response {
157 let path = uri.path().trim_start_matches('/');
158
159 if let Some(file) = StaticAssets::get(path) {
161 let mime = mime_guess::from_path(path).first_or_octet_stream();
162 return ([(header::CONTENT_TYPE, mime.as_ref())], file.data.to_vec()).into_response();
163 }
164
165 if let Some(index) = StaticAssets::get("index.html") {
167 return Html(index.data.to_vec()).into_response();
168 }
169
170 Html(PLACEHOLDER_HTML).into_response()
172}
173
174const PLACEHOLDER_HTML: &str = r#"<!DOCTYPE html>
175<html>
176<head><title>AWA</title></head>
177<body>
178 <h1>AWA Job Queue</h1>
179 <p>API available at <a href="/api/stats">/api/stats</a></p>
180 <p>Frontend not built. Run <code>cd awa-ui/frontend && npm install && npm run build</code> to build the UI.</p>
181</body>
182</html>"#;