use axum::http::Uri;
use axum::http::{HeaderMap, Method, StatusCode, header};
use axum::response::{IntoResponse, Response};
const DEV_HINT: &str = r#"
dovenest
dovenest
The frontend has not been built yet.
In development, run just dev-web in a second terminal and open
http://localhost:8080 (Trunk serves the app and proxies /api to this backend).
If you hit this page directly from the backend, make sure Trunk's output
directory is visible — the server looks in target/web or $DOVENEST_DIST.
For the production single binary: just build.
"#;
/// Serve the frontend: static assets, with a fallback to index.html so that
/// client-side routes (e.g. `/s/`) work.
pub(super) async fn fallback(method: Method, uri: Uri, headers: HeaderMap) -> Response {
let path = uri.path();
if path.starts_with("/api/") {
return (StatusCode::NOT_FOUND, "unknown endpoint").into_response();
}
// CalDAV and CardDAV clients given only the server's address ask it for
// the principal (RFC 6764, 6).
if path == "/" {
match method.as_str() {
"PROPFIND" => {
return (
StatusCode::TEMPORARY_REDIRECT,
[(header::LOCATION, format!("{}/", api_types::PIM))],
)
.into_response();
}
"OPTIONS" => {
return (
StatusCode::OK,
[
(header::ALLOW, "OPTIONS, GET, HEAD, PROPFIND"),
(
header::HeaderName::from_static("dav"),
super::pim::COMPLIANCE,
),
],
)
.into_response();
}
_ => {}
}
}
if method != Method::GET && method != Method::HEAD {
return StatusCode::METHOD_NOT_ALLOWED.into_response();
}
let rel = path.trim_start_matches('/');
let asset = if rel.is_empty() {
crate::assets::get_asset("index.html")
} else {
crate::assets::get_asset(rel)
};
match asset {
Some((bytes, mime, cache, etag)) => {
// The embedded assets carry a content hash, so a repeat visit can
// be answered without sending the body again.
if let Some(tag) = &etag
&& headers
.get(header::IF_NONE_MATCH)
.and_then(|v| v.to_str().ok())
.is_some_and(|v| v.split(',').any(|t| t.trim() == tag || t.trim() == "*"))
{
return (
StatusCode::NOT_MODIFIED,
[(header::CACHE_CONTROL, cache), (header::ETAG, tag.clone())],
)
.into_response();
}
let mut res = (
[(header::CONTENT_TYPE, mime), (header::CACHE_CONTROL, cache)],
bytes,
)
.into_response();
if let Some(tag) = etag
&& let Ok(v) = tag.parse()
{
res.headers_mut().insert(header::ETAG, v);
}
res
}
None => match crate::assets::get_asset("index.html") {
Some((bytes, mime, _, _)) => (
[
(header::CONTENT_TYPE, mime),
(header::CACHE_CONTROL, "no-cache".to_string()),
],
bytes,
)
.into_response(),
None => DEV_HINT.into_response(),
},
}
}