use axum::{ extract::{Path, Request, State}, http::{header, StatusCode}, middleware::{self, Next}, response::{IntoResponse, Redirect, Response}, routing::get, Router, }; use mime_guess::Mime; use rust_embed::EmbeddedFile; use crate::{app::App, channel, error::Internal, login::Login}; #[derive(rust_embed::Embed)] #[folder = "target/ui"] struct Assets; impl Assets { fn load(path: impl AsRef) -> Result> { let path = path.as_ref(); let mime = mime_guess::from_path(path).first_or_octet_stream(); Self::get(path) .map(|file| Asset(mime, file)) .ok_or(NotFound(format!("not found: {path}"))) } fn index() -> Result { // "not found" in this case really is an internal error, as it should // never happen. `index.html` is a known-valid path. Ok(Self::load("index.html")?) } } pub fn router(app: &App) -> Router { [ Router::new() .route("/*path", get(asset)) .route("/setup", get(setup)), Router::new() .route("/", get(root)) .route("/login", get(login)) .route("/ch/:channel", get(channel)) .route_layer(middleware::from_fn_with_state(app.clone(), setup_required)), ] .into_iter() .fold(Router::default(), Router::merge) } async fn asset(Path(path): Path) -> Result> { Assets::load(path) } async fn root(login: Option) -> Result { if login.is_none() { Ok(Redirect::temporary("/login").into_response()) } else { Ok(Assets::index()?.into_response()) } } async fn login() -> Result { Assets::index() } async fn setup(State(app): State) -> Result { if app.setup().completed().await? { Ok(Redirect::to("/login").into_response()) } else { Ok(Assets::index().into_response()) } } async fn channel( State(app): State, login: Option, Path(channel): Path, ) -> Result { if login.is_none() { Ok(Redirect::temporary("/").into_response()) } else if app.channels().get(&channel).await?.is_none() { Ok(NotFound(Assets::index()?).into_response()) } else { Ok(Assets::index()?.into_response()) } } struct Asset(Mime, EmbeddedFile); impl IntoResponse for Asset { fn into_response(self) -> Response { let Self(mime, file) = self; ( StatusCode::OK, [(header::CONTENT_TYPE, mime.as_ref())], file.data, ) .into_response() } } #[derive(Debug, thiserror::Error)] #[error("{0}")] struct NotFound(pub E); impl IntoResponse for NotFound where E: IntoResponse, { fn into_response(self) -> Response { let Self(response) = self; (StatusCode::NOT_FOUND, response).into_response() } } pub async fn setup_required(State(app): State, request: Request, next: Next) -> Response { match app.setup().completed().await { Ok(true) => next.run(request).await, Ok(false) => Redirect::to("/setup").into_response(), Err(error) => Internal::from(error).into_response(), } }