use std::convert::Infallible;
use std::marker::PhantomData;
use axum::extract::FromRequestParts;
use axum::response::{IntoResponse, Response};
use crate::dx::auth_user::AuthUser;
use crate::dx::user_loader::UserLoader;
pub struct Auth<U: AuthUser>(pub U);
impl<U: AuthUser> Auth<U> {
pub fn into_inner(self) -> U {
self.0
}
pub fn user(&self) -> &U {
&self.0
}
pub fn authorize<M, P: super::policy::Policy<M, User = U>>(
&self,
action: &str,
resource: &M,
) -> Result<(), super::policy::AuthzError> {
if P::check(&self.0, action, resource) {
Ok(())
} else {
Err(super::policy::AuthzError::Forbidden)
}
}
}
impl<U, S> FromRequestParts<S> for Auth<U>
where
U: UserLoader<S>,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
let user = load_user::<U, S>(parts, state).await?;
user.map(Auth).ok_or_else(|| {
(
axum::http::StatusCode::UNAUTHORIZED,
"Authentication required",
)
.into_response()
})
}
}
pub struct OptionalAuth<U: AuthUser>(pub Option<U>);
impl<U: AuthUser> OptionalAuth<U> {
pub fn user(&self) -> Option<&U> {
self.0.as_ref()
}
pub fn is_authenticated(&self) -> bool {
self.0.is_some()
}
}
impl<U, S> FromRequestParts<S> for OptionalAuth<U>
where
U: UserLoader<S>,
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
let user = load_user::<U, S>(parts, state).await.unwrap_or(None);
Ok(OptionalAuth(user))
}
}
pub type Current<U> = Auth<U>;
pub type OptionalCurrent<U> = OptionalAuth<U>;
pub struct AuthManager<U: AuthUser> {
session: crate::auth::tower_sessions::Session,
_marker: PhantomData<U>,
}
impl<U: AuthUser> AuthManager<U> {
pub fn login(&self, user: &U) -> LoginBuilder<'_, U> {
LoginBuilder {
session: &self.session,
user_id: user.id().clone(),
remember: false,
}
}
pub async fn logout(&self) -> Result<(), AuthError> {
self.session
.flush()
.await
.map_err(|e| AuthError::Session(e.to_string()))?;
Ok(())
}
pub async fn regenerate(&self) -> Result<(), AuthError> {
self.session
.cycle_id()
.await
.map_err(|e| AuthError::Session(e.to_string()))?;
Ok(())
}
}
impl<U, S> FromRequestParts<S> for AuthManager<U>
where
U: AuthUser,
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
let session = crate::auth::tower_sessions::Session::from_request_parts(parts, state)
.await
.map_err(|_| unreachable!("Session extraction is infallible"))?;
Ok(AuthManager {
session,
_marker: PhantomData,
})
}
}
pub struct LoginBuilder<'a, U: AuthUser> {
session: &'a crate::auth::tower_sessions::Session,
user_id: U::Id,
remember: bool,
}
impl<'a, U: AuthUser> LoginBuilder<'a, U> {
pub fn remember(mut self, remember: bool) -> Self {
self.remember = remember;
self
}
}
impl<'a, U: AuthUser> std::future::IntoFuture for LoginBuilder<'a, U> {
type Output = Result<(), AuthError>;
type IntoFuture = std::pin::Pin<
std::boxed::Box<dyn std::future::Future<Output = Result<(), AuthError>> + Send + 'a>,
>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
self.session
.cycle_id()
.await
.map_err(|e| AuthError::Session(e.to_string()))?;
self.session
.insert(U::SESSION_KEY, &self.user_id)
.await
.map_err(|e| AuthError::Session(e.to_string()))?;
if self.remember {
self.session
.insert("remember_me", true)
.await
.map_err(|e| AuthError::Session(e.to_string()))?;
}
Ok(())
})
}
}
#[derive(Debug)]
pub enum AuthError {
Session(String),
}
impl std::fmt::Display for AuthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Session(msg) => write!(f, "session error: {msg}"),
}
}
}
impl std::error::Error for AuthError {}
async fn load_user<U, S>(
parts: &mut axum::http::request::Parts,
state: &S,
) -> Result<Option<U>, Response>
where
U: UserLoader<S>,
S: Send + Sync,
{
let session = crate::auth::tower_sessions::Session::from_request_parts(parts, state)
.await
.map_err(|_| {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"session extraction failed",
)
.into_response()
})?;
let user_id: Option<U::Id> = session.get(U::SESSION_KEY).await.map_err(|_err| {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"session read failed",
)
.into_response()
})?;
let user_id = match user_id {
Some(id) => id,
None => return Ok(None),
};
let user = U::load_user(&user_id, state).await.map_err(|_err| {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"user load failed",
)
.into_response()
})?;
Ok(user)
}