use std::sync::Arc;
use axum::{
extract::DefaultBodyLimit,
http::{header, HeaderValue, Method, StatusCode},
response::{Html, IntoResponse},
routing::{delete, get, patch, post, put},
Router,
};
use sqlx::PgPool;
use tokio::sync::mpsc;
use tower_http::{
cors::{AllowOrigin, CorsLayer},
set_header::SetResponseHeaderLayer,
trace::TraceLayer,
};
use uuid::Uuid;
use scalar_api_reference::scalar_html_default;
use super::auth::{require_admin, AdminAuth};
use super::ws::ConnectionManager;
use super::{
api_keys, artifacts, compilations, data_planes, health, init, operations, plugins,
project_plugins, projects, specs, ws,
};
const OPENAPI_SPEC: &str = include_str!("../../openapi.yaml");
const API_VERSION: &str = "application/vnd.barbacane.v1+json";
const MAX_JSON_BODY: usize = 1024 * 1024;
const MAX_UPLOAD_BODY: usize = 32 * 1024 * 1024;
#[derive(Clone)]
pub struct AppState {
pub pool: PgPool,
pub compilation_tx: Option<mpsc::Sender<Uuid>>,
pub connection_manager: Arc<ConnectionManager>,
}
async fn openapi_spec() -> impl IntoResponse {
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/yaml")],
OPENAPI_SPEC,
)
}
async fn api_docs() -> Html<String> {
let config = serde_json::json!({
"spec": {
"url": "/api/openapi"
},
"theme": "purple",
"layout": "modern",
"hideModels": false,
"hideDownloadButton": false
});
Html(scalar_html_default(&config))
}
fn cors_layer() -> CorsLayer {
let origins: Vec<HeaderValue> = std::env::var("BARBACANE_CONTROL_ALLOWED_ORIGINS")
.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.filter_map(|s| HeaderValue::from_str(s).ok())
.collect();
CorsLayer::new()
.allow_origin(AllowOrigin::list(origins))
.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::PATCH,
Method::DELETE,
])
.allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE])
}
pub fn create_router(
pool: PgPool,
compilation_tx: Option<mpsc::Sender<Uuid>>,
connection_manager: Arc<ConnectionManager>,
admin_auth: AdminAuth,
) -> Router {
let state = AppState {
pool,
compilation_tx,
connection_manager,
};
let public = Router::new()
.route("/health", get(health::health_check))
.route("/ws/data-plane", get(ws::ws_handler));
let protected = Router::new()
.route("/openapi", get(openapi_spec))
.route("/docs", get(api_docs))
.route("/init", post(init::init_project))
.route(
"/specs",
post(specs::upload_spec).layer(DefaultBodyLimit::max(MAX_UPLOAD_BODY)),
)
.route("/specs", get(specs::list_specs))
.route("/specs/{id}", get(specs::get_spec))
.route("/specs/{id}", delete(specs::delete_spec))
.route("/specs/{id}/history", get(specs::get_spec_history))
.route("/specs/{id}/content", get(specs::download_spec_content))
.route("/specs/{id}/compliance", get(specs::get_spec_compliance))
.route("/specs/{id}/compile", post(compilations::start_compilation))
.route(
"/specs/{id}/compilations",
get(compilations::list_spec_compilations),
)
.route(
"/plugins",
post(plugins::register_plugin).layer(DefaultBodyLimit::max(MAX_UPLOAD_BODY)),
)
.route("/plugins", get(plugins::list_plugins))
.route("/plugins/{name}", get(plugins::list_plugin_versions))
.route("/plugins/{name}/{version}", get(plugins::get_plugin))
.route("/plugins/{name}/{version}", delete(plugins::delete_plugin))
.route(
"/plugins/{name}/{version}/download",
get(plugins::download_plugin),
)
.route("/artifacts", get(artifacts::list_artifacts))
.route("/artifacts/{id}", get(artifacts::get_artifact))
.route("/artifacts/{id}", delete(artifacts::delete_artifact))
.route(
"/artifacts/{id}/download",
get(artifacts::download_artifact),
)
.route(
"/specs/{id}/operations",
patch(operations::patch_spec_operations),
)
.route("/compilations/{id}", get(compilations::get_compilation))
.route(
"/compilations/{id}",
delete(compilations::delete_compilation),
)
.route("/projects", post(projects::create_project))
.route("/projects", get(projects::list_projects))
.route("/projects/{id}", get(projects::get_project))
.route("/projects/{id}", put(projects::update_project))
.route("/projects/{id}", delete(projects::delete_project))
.route("/projects/{id}/specs", get(projects::list_project_specs))
.route(
"/projects/{id}/specs",
post(projects::upload_spec_to_project).layer(DefaultBodyLimit::max(MAX_UPLOAD_BODY)),
)
.route(
"/projects/{id}/plugins",
get(project_plugins::list_project_plugins),
)
.route(
"/projects/{id}/plugins",
post(project_plugins::add_plugin_to_project),
)
.route(
"/projects/{id}/plugins/{name}",
put(project_plugins::update_project_plugin),
)
.route(
"/projects/{id}/plugins/{name}",
delete(project_plugins::remove_plugin_from_project),
)
.route(
"/projects/{id}/operations",
get(operations::get_project_operations),
)
.route(
"/projects/{id}/compilations",
get(projects::list_project_compilations),
)
.route(
"/projects/{id}/artifacts",
get(projects::list_project_artifacts),
)
.route("/projects/{id}/api-keys", post(api_keys::create_api_key))
.route("/projects/{id}/api-keys", get(api_keys::list_api_keys))
.route(
"/projects/{id}/api-keys/{key_id}",
delete(api_keys::revoke_api_key),
)
.route(
"/projects/{id}/data-planes",
get(data_planes::list_data_planes),
)
.route(
"/projects/{id}/data-planes/{dp_id}",
get(data_planes::get_data_plane),
)
.route(
"/projects/{id}/data-planes/{dp_id}",
delete(data_planes::disconnect_data_plane),
)
.route(
"/projects/{id}/deploy",
post(data_planes::deploy_to_data_planes),
)
.layer(axum::middleware::from_fn_with_state(
admin_auth,
require_admin,
));
Router::new()
.merge(public)
.merge(protected)
.layer(DefaultBodyLimit::max(MAX_JSON_BODY))
.layer(TraceLayer::new_for_http())
.layer(cors_layer())
.layer(SetResponseHeaderLayer::if_not_present(
axum::http::header::CONTENT_TYPE,
HeaderValue::from_static(API_VERSION),
))
.with_state(state)
}