use super::dispatch::DispatchSupervisor;
use super::shared::{
CurrentSession, OptionalSession, auth_request, check_content_length, max_body_bytes,
payload_too_large,
};
use crate::BetterAuth;
use alibi_core::{AuthError, AuthRequest, AuthResponse, AuthResult, AuthSchema, core_paths};
use axum::{
Router,
extract::{FromRef, FromRequestParts, OriginalUri, Request, State},
http::request::Parts,
response::{IntoResponse, Response},
routing::any,
};
use std::{collections::HashSet, future::Future, pin::Pin, sync::Arc};
pub trait AxumIntegration {
type Schema: AuthSchema;
fn axum_router(self) -> Router<Arc<BetterAuth<Self::Schema>>>;
fn axum_router_with_state<S>(self) -> Router<S>
where
Self: Sized,
Arc<BetterAuth<Self::Schema>>: FromRef<S>,
S: Clone + Send + Sync + 'static;
}
impl<T: AuthSchema> AxumIntegration for Arc<BetterAuth<T>> {
type Schema = T;
fn axum_router(self) -> Router<Self> {
self.axum_router_with_state::<Self>()
}
fn axum_router_with_state<S>(self) -> Router<S>
where
Self: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
let supervisor = DispatchSupervisor::new(render);
let mut paths = HashSet::from([
core_paths::OK.to_owned(),
core_paths::ERROR.to_owned(),
core_paths::UPDATE_USER.to_owned(),
]);
for plugin in self.plugins() {
paths.extend(plugin.routes().into_iter().map(|route| route.path));
}
let mut router = Router::new();
for path in paths {
router = router.route(&path, any(handler::<T>(supervisor.clone())));
}
router.fallback(handler::<T>(supervisor))
}
}
impl<S, T> FromRequestParts<S> for CurrentSession<T>
where
T: AuthSchema,
Arc<BetterAuth<T>>: FromRef<S>,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let auth = Arc::<BetterAuth<T>>::from_ref(state);
Self::resolve(&auth, parts.uri.path(), &parts.headers)
.await
.map_err(IntoResponse::into_response)
}
}
impl<S, T> FromRequestParts<S> for OptionalSession<T>
where
T: AuthSchema,
Arc<BetterAuth<T>>: FromRef<S>,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
Ok(Self(
CurrentSession::<T>::from_request_parts(parts, state)
.await
.ok(),
))
}
}
type HandlerFuture = Pin<Box<dyn Future<Output = Response> + Send>>;
fn handler<T: AuthSchema>(
supervisor: DispatchSupervisor<Response>,
) -> impl Fn(State<Arc<BetterAuth<T>>>, Request) -> HandlerFuture + Clone {
move |State(auth): State<Arc<BetterAuth<T>>>, request: Request| {
let supervisor = supervisor.clone();
Box::pin(async move {
match convert_request(&auth, request).await {
Ok(request) => supervisor.dispatch(auth, request).await,
Err(error) => error.into_response(),
}
})
}
}
async fn convert_request<T: AuthSchema>(
auth: &BetterAuth<T>,
request: Request,
) -> AuthResult<AuthRequest> {
let max_bytes = max_body_bytes(auth);
let (parts, body) = request.into_parts();
check_content_length(&parts.headers, max_bytes)?;
let body = axum::body::to_bytes(body, max_bytes)
.await
.map_err(|error| {
if is_length_limit_error(&error) {
return payload_too_large(max_bytes);
}
tracing::warn!(%error, "Failed to read request body");
AuthError::bad_request("Failed to read request body")
})?;
let original_uri = parts
.extensions
.get::<OriginalUri>()
.map_or(&parts.uri, |original| &original.0);
auth_request(
&parts.method,
&parts.uri,
original_uri,
&parts.headers,
body.to_vec(),
&auth.config().base_url,
)
}
fn is_length_limit_error(error: &axum::Error) -> bool {
std::iter::successors(std::error::Error::source(error), |source| source.source())
.any(<dyn std::error::Error>::is::<http_body_util::LengthLimitError>)
}
fn render(result: Result<AuthResponse, AuthError>) -> Response {
result
.unwrap_or_else(AuthError::to_auth_response)
.into_response()
}