diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 171a0ce..0e814f6 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -3,6 +3,10 @@ name = "bichon-server" version.workspace = true edition.workspace = true +[features] +default = ["embed-web"] +embed-web = ["dep:rust-embed"] + [dependencies] bichon-core = { path = "../core", features = ["web-api"] } @@ -17,7 +21,7 @@ poem-openapi = { version = "5.1.16", features = [ "swagger-ui", "email", ] } -rust-embed = "8.11.0" +rust-embed = { version = "8.11.0", optional = true } email_address.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/server/src/rest/assets.rs b/crates/server/src/rest/assets.rs index 7675629..bb9590e 100644 --- a/crates/server/src/rest/assets.rs +++ b/crates/server/src/rest/assets.rs @@ -1,24 +1,11 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . +#[cfg(feature = "embed-web")] +mod inner { + use rust_embed::RustEmbed; + #[derive(RustEmbed)] + #[folder = "../../web/dist/"] + pub struct FrontEndAssets; +} -use rust_embed::RustEmbed; - -#[derive(RustEmbed)] -#[folder = "../../web/dist/"] -pub struct FrontEndAssets; +#[cfg(feature = "embed-web")] +pub use inner::FrontEndAssets; diff --git a/crates/server/src/rest/mod.rs b/crates/server/src/rest/mod.rs index 1349665..468dac3 100644 --- a/crates/server/src/rest/mod.rs +++ b/crates/server/src/rest/mod.rs @@ -16,6 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use crate::common::auth::ApiGuard; use crate::common::error::ErrorCapture; use crate::common::log::Tracing; use crate::common::tls::rustls_config; @@ -29,25 +30,31 @@ use bichon_core::error::BichonResult; use bichon_core::raise_error; use bichon_core::settings::cli::SETTINGS; -use super::error::ApiErrorResponse; -use crate::common::auth::ApiGuard; use api::create_openapi_service; -use assets::FrontEndAssets; -use http::{HeaderValue, Method}; -use poem::endpoint::EmbeddedFilesEndpoint; +use http::Method; use poem::listener::{Listener, TcpListener}; -use poem::middleware::{CatchPanic, Compression, Cors, SetHeader}; -use poem::{get, handler, post, Endpoint, EndpointExt, IntoResponse, Route, Server}; +use poem::middleware::{CatchPanic, Compression, Cors}; +use poem::{get, post, Endpoint, EndpointExt, Route, Server}; use public::oauth2::oauth2_callback; use std::collections::HashSet; use std::time::Duration; +#[cfg(feature = "embed-web")] +use { + assets::FrontEndAssets, + http::HeaderValue, + poem::{handler, endpoint::EmbeddedFilesEndpoint, IntoResponse}, + poem::middleware::SetHeader, +}; + pub mod api; pub mod assets; pub mod public; pub type ApiResult = std::result::Result; +use super::error::ApiErrorResponse; + /// Build the community route tree. Pro/Enterprise servers can call this /// and then add their own routes before passing the tree to the server. pub fn build_routes() -> impl Endpoint { @@ -95,13 +102,6 @@ pub fn build_routes() -> impl Endpoint { .expose_headers(vec!["Accept"]) .max_age(SETTINGS.bichon_cors_max_age); - let cache_static = || { - SetHeader::new().overriding( - http::header::CACHE_CONTROL, - HeaderValue::from_static("max-age=86400"), - ) - }; - let app_logic = Route::new() .nest("/api-docs/swagger", swagger) .nest("/api-docs/redoc", redoc) @@ -112,12 +112,9 @@ pub fn build_routes() -> impl Endpoint { .nest("/oauth2/callback", get(oauth2_callback)) .nest("/api/status", get(get_status)) .nest("/api/login", post(login)) - .nest_no_strip("/api/v1", open_api_route) - .nest_no_strip( - "/assets", - EmbeddedFilesEndpoint::::new().with(cache_static()), - ) - .at("/*", serve_index_with_base); + .nest_no_strip("/api/v1", open_api_route); + + let app_logic = add_web_assets(app_logic); Route::new() .nest(&SETTINGS.bichon_base_url, app_logic) @@ -126,6 +123,52 @@ pub fn build_routes() -> impl Endpoint { .with(CatchPanic::new()) } +#[cfg(feature = "embed-web")] +fn add_web_assets(route: Route) -> impl Endpoint { + let cache_static = || { + SetHeader::new().overriding( + http::header::CACHE_CONTROL, + HeaderValue::from_static("max-age=86400"), + ) + }; + + route + .nest_no_strip( + "/assets", + EmbeddedFilesEndpoint::::new().with(cache_static()), + ) + .at("/*", serve_index_with_base) +} + +#[cfg(not(feature = "embed-web"))] +fn add_web_assets(route: Route) -> Route { + route +} + +#[cfg(feature = "embed-web")] +#[handler] +async fn serve_index_with_base() -> impl IntoResponse { + let mut html = + String::from_utf8_lossy(&FrontEndAssets::get("index.html").unwrap().data).to_string(); + + let raw_base = &SETTINGS.bichon_base_url; + let base_href = if raw_base.ends_with('/') { + raw_base.clone() + } else { + format!("{}/", raw_base) + }; + + let inject_content = format!( + r#""#, + base_href, raw_base + ); + + html = html.replace("", &format!("{}", inject_content)); + poem::Response::builder() + .content_type("text/html; charset=utf-8") + .body(html) +} + pub async fn start_http_server() -> BichonResult<()> { let listener = TcpListener::bind(( SETTINGS.bichon_bind_ip.clone().unwrap_or("0.0.0.0".into()), @@ -160,26 +203,3 @@ pub async fn start_http_server() -> BichonResult<()> { .await .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)) } - -#[handler] -async fn serve_index_with_base() -> impl IntoResponse { - let mut html = - String::from_utf8_lossy(&FrontEndAssets::get("index.html").unwrap().data).to_string(); - - let raw_base = &SETTINGS.bichon_base_url; - let base_href = if raw_base.ends_with('/') { - raw_base.clone() - } else { - format!("{}/", raw_base) - }; - - let inject_content = format!( - r#""#, - base_href, raw_base - ); - - html = html.replace("", &format!("{}", inject_content)); - poem::Response::builder() - .content_type("text/html; charset=utf-8") - .body(html) -}