Compare commits

..

6 Commits

Author SHA1 Message Date
fa9ea95657 Don't check for /favicon.ico path in require_auth_mw
All checks were successful
Push Workflows / rustfmt (push) Successful in 5s
Push Workflows / tailwind-build (push) Successful in 5s
Push Workflows / clippy (push) Successful in 28s
Push Workflows / test (push) Successful in 40s
Push Workflows / docs (push) Successful in 41s
Push Workflows / build (push) Successful in 1m50s
Push Workflows / nix-build (push) Successful in 5m17s
The server returns the icon before this middleware is reached
2026-06-30 21:17:37 -04:00
3ec07c0469 Serve favicon on /favicon.ico 2026-06-30 21:16:52 -04:00
f41f4f8d80 Set DIOXUS_PUBLIC_PATH to configured value 2026-06-30 21:11:26 -04:00
c9b7bbfece Enable fs feature on tower-http 2026-06-30 21:10:29 -04:00
9de0b13ee8 Add tower-http 2026-06-30 21:09:02 -04:00
8b00db60eb Add public_path config option 2026-06-30 21:08:05 -04:00
5 changed files with 83 additions and 6 deletions

32
Cargo.lock generated
View File

@@ -1096,7 +1096,7 @@ dependencies = [
"tokio-tungstenite 0.28.0",
"tokio-util",
"tower",
"tower-http",
"tower-http 0.6.11",
"tower-layer",
"tracing",
"tungstenite 0.27.0",
@@ -1374,7 +1374,7 @@ dependencies = [
"tokio-tungstenite 0.28.0",
"tokio-util",
"tower",
"tower-http",
"tower-http 0.6.11",
"tracing",
"tracing-futures",
"url",
@@ -2427,6 +2427,7 @@ dependencies = [
"rand 0.10.1",
"serde",
"thiserror 2.0.18",
"tower-http 0.7.0",
"tower-sessions-redis-store",
"tracing",
]
@@ -3293,7 +3294,7 @@ dependencies = [
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
"tower-http 0.6.11",
"tower-service",
"url",
"wasm-bindgen",
@@ -4182,6 +4183,31 @@ dependencies = [
"url",
]
[[package]]
name = "tower-http"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233"
dependencies = [
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"http-range-header",
"httpdate",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"

View File

@@ -24,6 +24,7 @@ pbkdf2 = { version = "0.13.0", optional = true, features = ["getrandom", "phc"]
rand = "0.10.1"
serde = { version = "1.0.228", features = ["derive"] }
thiserror = "2.0.18"
tower-http = { version = "0.7.0", optional = true, features = ["fs"] }
tower-sessions-redis-store = { version = "0.16.0", optional = true }
tracing = "0.1.44"
@@ -40,6 +41,7 @@ server = [
"dep:dotenvy",
"dep:fred",
"dep:pbkdf2",
"dep:tower-http",
"dep:tower-sessions-redis-store",
]

View File

@@ -1,3 +1,5 @@
use std::path::PathBuf;
use serde::Deserialize;
/// Enable secure cookies by default only in release mode
@@ -141,12 +143,18 @@ impl KeyValStoreConfig {
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ServerConfig {
pub public_path: PathBuf,
}
#[derive(Debug, Clone, Deserialize)]
/// Top-level application configuration
pub struct Config {
pub auth: AuthConfig,
pub database: DatabaseConfig,
pub key_val_store: KeyValStoreConfig,
pub server: ServerConfig,
}
/// Parse configuration from the expected files and environment variables
@@ -159,6 +167,7 @@ pub fn load_config() -> Result<Config, config::ConfigError> {
.set_default("server.port", 8080)?
.set_default("auth.open_signup", false)?
.set_default("auth.cookies_secure", DEFAULT_COOKIES_SECURE)?
.set_default("server.public_path", default_public_dir())?
.add_source(File::with_name(&format!("/etc/{pkg_name}/config")).required(false))
.add_source(File::with_name(&format!("/etc/{pkg_name}")).required(false))
.add_source(File::with_name("config").required(false))
@@ -166,3 +175,19 @@ pub fn load_config() -> Result<Config, config::ConfigError> {
.build()?
.try_deserialize()
}
/// Provide a sane default for the public path, using the same sources as Dioxus does internally.
/// Checks the `DIOXUS_PUBLIC_PATH` environment variable, then tries relative to the path of this
/// executable
fn default_public_dir() -> Option<String> {
std::env::var("DIOXUS_PUBLIC_PATH").ok().or_else(|| {
std::env::current_exe().ok().and_then(|path| {
path.parent()
.expect("current executable path must have a parent")
.join("public")
.into_os_string()
.into_string()
.ok()
})
})
}

View File

@@ -2,8 +2,10 @@ use dioxus::{
fullstack::axum::{Router, middleware::from_fn},
server::axum::Extension,
};
use tower_http::services::ServeFile;
use crate::App;
use crate::app::LOGO_ICO;
use crate::server::{
auth::build_auth_layer,
config::{self, Config},
@@ -22,6 +24,14 @@ pub fn main() -> Result<std::convert::Infallible> {
.map_err(|e| Error::message_here(e.to_string()))
.err_context("Failed to load config")?;
// Dioxus doesn't expose a way to configure the public path other than this environment
// variable, and also doesn't provide a way to read what the public path is. As a workaround we
// expose a config option and set this variable that Dioxus expects.
// SAFETY: "This function is safe to call in a single-threaded program."
unsafe {
std::env::set_var("DIOXUS_PUBLIC_PATH", config.server.public_path.clone());
}
// `Ok(...?)` is because `dioxus::serve` expects an `anyhow::Result`
dioxus::serve(move || {
let config = config.clone();
@@ -39,13 +49,28 @@ async fn router_setup(config: Config) -> Result<Router> {
.await
.err_context("Failed key-value store setup")?;
let favicon_path = {
// Resolve the favicon path
let asset_path = LOGO_ICO.resolve();
// If the asset path starts with "/", strip it. Otherwise it behaves as an "absolute path"
// and replaces the base path in the join operation. This is necessary because Dioxus will
// produce a path like "/assets/" in the call to `resolve`
let asset_path_rel = asset_path.strip_prefix("/").unwrap_or(&asset_path);
config.server.public_path.join(asset_path_rel)
};
tracing::debug!("Favicon path: {}", favicon_path.display());
let auth_layer = build_auth_layer(db_pool.clone(), key_val_pool, config.auth.cookies_secure);
let router = dioxus::server::router(App)
.layer(from_fn(require_auth_middleware))
.layer(Extension(config))
.layer(Extension(db_pool))
.layer(auth_layer);
.layer(auth_layer)
.nest_service("/favicon.ico", ServeFile::new(favicon_path));
tracing::info!("Setup complete, returning Router...");
Ok(router)

View File

@@ -13,13 +13,12 @@ const ALLOWED_PATH_PREFIX: [&str; 1] = ["/assets/"];
#[cfg(debug_assertions)]
const ALLOWED_PATH_PREFIX: [&str; 2] = ["/assets/", "/wasm/"];
const ALLOWED_PATHS: [&str; 6] = [
const ALLOWED_PATHS: [&str; 5] = [
"/login",
"/signup",
"/api/v1/auth/login",
"/api/v1/auth/signup",
"/api/v1/auth/user",
"/favicon.ico",
];
/// Axum middleware to redirect an unauthenticated request to /login unless it matches one of the allowed paths