#![forbid(unsafe_code)]
#![cfg_attr(debug_assertions, warn(missing_docs))]
#![cfg_attr(not(debug_assertions), deny(missing_docs))]
#![warn(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::dbg_macro,
clippy::todo,
clippy::unimplemented
)]
use serde::Serialize;
pub use xroute_macros::*;
#[macro_export]
macro_rules! enforce {
($item:expr) => {{
match $item {
Ok(v) => v,
Err(res) => return res,
}
};};
}
pub fn redirect(url: &str) -> crate::prelude::Res {
axum::response::Redirect::to(url).into_response()
}
#[allow(clippy::result_large_err)]
pub fn enforce_user(req: &prelude::Req) -> Result<String, prelude::Res> {
let user = req
.headers()
.get("Remote-User")
.or_else(|| req.headers().get("X-Forwarded-User"));
if user.is_none() {
#[allow(clippy::expect_used)]
return Err(axum::response::Response::builder()
.status(prelude::StatusCode::UNAUTHORIZED)
.header("X-Authelia-Action", "auth")
.body(axum::body::Body::empty())
.expect("Failed to build empty body")
.into_response());
}
Ok(user
.unwrap_or(&axum::http::header::HeaderValue::from_static(""))
.to_str()
.unwrap_or("")
.to_string())
}
#[cfg(feature = "stream")]
pub async fn streamed_response<I, T, E, F, Fut>(
workers: usize,
iter: I,
mapper: F,
) -> crate::prelude::Res
where
I: IntoIterator + Send + 'static,
I::IntoIter: Send + 'static,
I::Item: Send + 'static,
F: Fn(I::Item) -> Fut + Send + Sync + 'static,
Fut: futures::Future<Output = Result<T, E>> + Send + 'static,
T: EasyResponse + Serialize + Send + 'static,
E: Into<Box<dyn std::error::Error + Send + Sync>> + std::fmt::Display + Send + 'static,
{
use axum::response::sse::{Event, Sse};
use futures::{StreamExt, stream};
let mapper = std::sync::Arc::new(mapper);
let stream =
stream::iter(iter.into_iter())
.map(move |item| {
let mapper = mapper.clone();
mapper(item)
})
.buffer_unordered(workers)
.map(|res| match res {
Ok(item) => {
let json = serde_json::to_string(&item).unwrap_or_else(|_| "{}".into());
Ok::<Event, E>(Event::default().data(json))
}
Err(e) => {
log::warn!("Error in streamed response: {}", e);
Ok(Event::default()
.data(serde_json::json!({ "error": e.to_string() }).to_string()))
}
});
Sse::new(stream).into_response()
}
pub mod prelude {
pub type Req = axum::extract::Request<axum::body::Body>;
pub type Res = axum::response::Response;
#[cfg(feature = "stream")]
pub use crate::streamed_response;
pub use crate::x::EasyResponse;
pub use crate::{enforce, redirect, run};
pub use axum::{
extract::{Path, Query},
http::StatusCode,
};
pub use log::{debug, error, info, trace, warn};
pub use xroute_macros::*;
}
pub mod x {
pub use axum::{
Json, Router,
body::Body,
extract::State,
extract::{FromRequest, FromRequestParts},
http::request::Request as HttpRequest,
response::IntoResponse,
response::Response,
routing::{delete, get, patch, post, put},
};
pub use serde::{Deserialize, Serialize};
pub use ts_rs as ts;
pub use tokio::runtime::Builder as AsyncBuilder;
pub trait EasyResponse {
fn res(self) -> Response;
fn res_with(self, status: crate::prelude::StatusCode) -> Response;
fn res_with_headers<const N: usize>(
self,
status: crate::prelude::StatusCode,
headers: [(&str, &str); N],
) -> Response;
fn json(&self) -> String
where
Self: Serialize,
{
serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
}
}
impl<T: Serialize> EasyResponse for T {
fn res(self) -> Response {
Json(self).into_response()
}
fn res_with(self, status: crate::prelude::StatusCode) -> Response {
(status, Json(self)).into_response()
}
fn res_with_headers<const N: usize>(
self,
status: crate::prelude::StatusCode,
headers: [(&str, &str); N],
) -> Response {
(status, headers, Json(self)).into_response()
}
}
}
use axum::{extract::Request, http::Uri};
use std::{path::PathBuf, sync::Arc};
use tower::ServiceExt as _;
use tower_http::{compression::CompressionLayer, services::ServeFile};
use x::{Body, IntoResponse, Router};
use crate::x::EasyResponse;
pub async fn run(router: Router) {
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
let fallback = {
let static_path = "./web/dist/".to_string();
move |uri: Uri| {
let static_path = static_path.clone();
async move {
serve_static(uri, static_path.into(), |uri| {
format!("404 Not Found: {}", uri)
})
.await
}
}
};
let app = Router::new()
.merge(router)
.fallback(fallback)
.layer(CompressionLayer::new());
let addr = "0.0.0.0:3000";
let listener = match tokio::net::TcpListener::bind(addr).await {
Ok(l) => l,
Err(e) => {
log::error!("Failed to listen at `{}`: {}", addr, e);
return;
}
};
log::info!("Listening on http://0.0.0.0:3000");
if let Err(e) = axum::serve(listener, app).await {
log::error!("Server error: {}", e);
}
}
async fn serve_static<R: IntoResponse>(
uri: Uri,
static_path: Arc<String>,
not_found: fn(uri: Uri) -> R,
) -> impl IntoResponse {
let path = uri.path().trim_start_matches('/');
let mut file_path = PathBuf::from(static_path.as_ref());
file_path.push(path);
match tokio::fs::metadata(&file_path).await {
Ok(meta) if meta.is_file() => {}
_ => {
let mut index_path = PathBuf::from(static_path.as_str());
index_path.push("index.html");
file_path = index_path;
}
}
let file_res = ServeFile::new(file_path)
.oneshot(Request::new(Body::empty()))
.await;
match file_res {
Ok(res) => res.into_response(),
Err(_) => not_found(uri).into_response(),
}
}