mod api;
mod board;
use std::sync::OnceLock;
use axum::Json;
use axum::extract::Request;
use axum::http::{StatusCode, Uri, header};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use enwiro_sdk::capability::CapabilitySet;
use rust_embed::RustEmbed;
use utoipa_axum::router::OpenApiRouter;
#[derive(Clone)]
pub struct AppState {
pub workspaces_directory: String,
}
#[derive(RustEmbed)]
#[folder = "web/dist"]
struct Assets;
async fn static_handler(uri: Uri) -> Response {
let path = uri.path().trim_start_matches('/');
let path = if path.is_empty() { "index.html" } else { path };
match Assets::get(path) {
Some(file) => {
let mime = mime_guess::from_path(path).first_or_octet_stream();
([(header::CONTENT_TYPE, mime.as_ref())], file.data).into_response()
}
None => match Assets::get("index.html") {
Some(index) => ([(header::CONTENT_TYPE, "text/html")], index.data).into_response(),
None => StatusCode::NOT_FOUND.into_response(),
},
}
}
async fn guard_host(req: Request, next: Next) -> Result<Response, StatusCode> {
let host = req
.headers()
.get(header::HOST)
.and_then(|h| h.to_str().ok())
.unwrap_or("");
let hostname = host.split(':').next().unwrap_or("");
if matches!(hostname, "127.0.0.1" | "localhost") {
Ok(next.run(req).await)
} else {
Err(StatusCode::FORBIDDEN)
}
}
fn capability() -> &'static CapabilitySet {
static CAPABILITY: OnceLock<CapabilitySet> = OnceLock::new();
CAPABILITY.get_or_init(CapabilitySet::new)
}
async fn guard_capability(req: Request, next: Next) -> Result<Response, StatusCode> {
if !req.uri().path().starts_with("/api") {
return Ok(next.run(req).await);
}
let query_token = token_from_query(req.uri());
let authorized = capability().is_authorized(req.headers())
|| query_token.is_some_and(|token| capability().contains(token));
if authorized {
Ok(next.run(req).await)
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
fn token_from_query(uri: &Uri) -> Option<&str> {
uri.query()?
.split('&')
.find_map(|pair| pair.strip_prefix("token="))
}
fn load_workspaces_directory() -> String {
let config: enwiro_daemon::ConfigurationValues = enwiro_sdk::config::load_user_config("enwiro")
.ok()
.and_then(|v| serde_json::from_value(v).ok())
.unwrap_or_default();
config.workspaces_directory
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let state = AppState {
workspaces_directory: load_workspaces_directory(),
};
let (api_router, mut openapi) = OpenApiRouter::new()
.nest("/api", api::router(state))
.split_for_parts();
openapi.info.title = "enw-gui".to_string();
if std::env::args().any(|a| a == "--dump-openapi") {
println!("{}", openapi.to_pretty_json()?);
return Ok(());
}
let app = api_router
.route(
"/api/openapi.json",
axum::routing::get(move || async move { Json(openapi) }),
)
.fallback(static_handler)
.layer(axum::middleware::from_fn(guard_capability))
.layer(axum::middleware::from_fn(guard_host));
let token = capability().mint();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let url = format!("http://{}/?token={token}", listener.local_addr()?);
println!("enw-gui serving on {url}");
let _ = open::that(&url);
axum::serve(listener, app).await?;
Ok(())
}