diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 0182fda6..ac407675 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -29,6 +29,8 @@ dirs = "5.0" git2 = "0.18" async-trait = "0.1" dissimilar = "1.0" +rust-embed = "8.2" +mime_guess = "2.0" [build-dependencies] ts-rs = { version = "9.0", features = ["uuid-impl", "chrono-impl"] } diff --git a/backend/src/main.rs b/backend/src/main.rs index 9bb5076d..4789c0e9 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,10 +1,13 @@ use axum::{ + body::Body, extract::Extension, + http::{header, HeaderValue, StatusCode}, middleware, - response::Json as ResponseJson, + response::{IntoResponse, Json as ResponseJson, Response}, routing::{get, post}, Json, Router, }; +use rust_embed::RustEmbed; use sqlx::postgres::PgPoolOptions; use std::{collections::HashMap, env, sync::Arc}; use tokio::sync::Mutex; @@ -22,6 +25,10 @@ use execution_monitor::{execution_monitor, AppState}; use models::{user::User, ApiResponse}; use routes::{filesystem, health, projects, tasks, users}; +#[derive(RustEmbed)] +#[folder = "../frontend/dist"] +struct Assets; + async fn echo_handler( Json(payload): Json, ) -> ResponseJson> { @@ -32,6 +39,49 @@ async fn echo_handler( }) } +async fn static_handler(uri: axum::extract::Path) -> impl IntoResponse { + let path = uri.trim_start_matches('/'); + serve_file(path).await +} + +async fn index_handler() -> impl IntoResponse { + serve_file("index.html").await +} + +async fn serve_file(path: &str) -> impl IntoResponse { + let file = Assets::get(path); + + match file { + Some(content) => { + let mime = mime_guess::from_path(path).first_or_octet_stream(); + + Response::builder() + .status(StatusCode::OK) + .header( + header::CONTENT_TYPE, + HeaderValue::from_str(mime.as_ref()).unwrap(), + ) + .body(Body::from(content.data.into_owned())) + .unwrap() + } + None => { + // For SPA routing, serve index.html for unknown routes + if let Some(index) = Assets::get("index.html") { + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, HeaderValue::from_static("text/html")) + .body(Body::from(index.data.into_owned())) + .unwrap() + } else { + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("404 Not Found")) + .unwrap() + } + } + } +} + #[tokio::main] async fn main() -> anyhow::Result<()> { // Load environment variables from .env file @@ -74,9 +124,8 @@ async fn main() -> anyhow::Result<()> { // Public routes (no auth required) let public_routes = Router::new() - .route("/", get(|| async { "Bloop API" })) - .route("/health", get(health::health_check)) - .route("/echo", post(echo_handler)) + .route("/api/health", get(health::health_check)) + .route("/api/echo", post(echo_handler)) .merge(users::public_users_router()); // Protected routes (auth required) @@ -91,6 +140,9 @@ async fn main() -> anyhow::Result<()> { let app = Router::new() .merge(public_routes) .merge(protected_routes) + // Static file serving routes + .route("/", get(index_handler)) + .route("/*path", get(static_handler)) .layer(Extension(pool)) .layer(Extension(app_state)) .layer(CorsLayer::permissive()); diff --git a/package.json b/package.json index 13d8e94f..f420a5bd 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "scripts": { "dev": "concurrently \"cargo watch -x 'run --manifest-path backend/Cargo.toml'\" \"npm run frontend:dev\"", "build": "npm run frontend:build && cargo build --release --manifest-path backend/Cargo.toml", + "build:single": "npm run frontend:build && cargo build --release --manifest-path backend/Cargo.toml", "frontend:dev": "cd frontend && npm run dev", "frontend:build": "cd frontend && npm run build", "backend:dev": "cargo watch -x 'run --manifest-path backend/Cargo.toml'",