129 lines
3.9 KiB
Rust
129 lines
3.9 KiB
Rust
use anyhow::Result;
|
|
use axum::{
|
|
async_trait,
|
|
extract::{FromRequestParts, State},
|
|
http::{
|
|
header::{COOKIE, SET_COOKIE},
|
|
request::Parts,
|
|
Request, StatusCode,
|
|
},
|
|
middleware::Next,
|
|
response::Response,
|
|
Extension,
|
|
};
|
|
use chrono::{Duration, Utc};
|
|
use cookie::Cookie;
|
|
use std::sync::Arc;
|
|
|
|
use crate::{
|
|
auth::{
|
|
session::{Session, SessionRepository},
|
|
user::User,
|
|
},
|
|
database::Database,
|
|
http::error::ApiError,
|
|
state::AppState,
|
|
};
|
|
|
|
pub const INVALID_SESSION: ApiError = ApiError::Client {
|
|
status: StatusCode::UNAUTHORIZED,
|
|
code: "authentication-required",
|
|
message: "Please log-in and submit a valid session as a cookie",
|
|
};
|
|
|
|
pub struct RequireUser(pub User);
|
|
|
|
#[async_trait]
|
|
impl<S> FromRequestParts<S> for RequireUser
|
|
where
|
|
S: Send + Sync,
|
|
{
|
|
type Rejection = ApiError<'static>;
|
|
|
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
|
match Extension::<User>::from_request_parts(parts, state).await {
|
|
Ok(Extension(user)) => Ok(RequireUser(user)),
|
|
_ => Err(INVALID_SESSION),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct OptionalUser(pub Option<User>);
|
|
|
|
#[async_trait]
|
|
impl<S> FromRequestParts<S> for OptionalUser
|
|
where
|
|
S: Send + Sync,
|
|
{
|
|
type Rejection = ApiError<'static>;
|
|
|
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
|
match Extension::<User>::from_request_parts(parts, state).await {
|
|
Ok(Extension(user)) => Ok(OptionalUser(Some(user))),
|
|
_ => Ok(OptionalUser(None)),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct RequireSession(pub Session);
|
|
|
|
#[async_trait]
|
|
impl<S> FromRequestParts<S> for RequireSession
|
|
where
|
|
S: Send + Sync,
|
|
{
|
|
type Rejection = ApiError<'static>;
|
|
|
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
|
match Extension::<Session>::from_request_parts(parts, state).await {
|
|
Ok(Extension(session)) => Ok(RequireSession(session)),
|
|
_ => Err(INVALID_SESSION),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn refresh_sessions<B>(
|
|
State(state): State<Arc<AppState>>,
|
|
mut req: Request<B>,
|
|
next: Next<B>,
|
|
) -> Response {
|
|
if let Some((session_id, session_secret)) = req
|
|
.headers()
|
|
.get(COOKIE)
|
|
.and_then(|header| Cookie::parse(header.to_str().unwrap_or_default()).ok())
|
|
.and_then(|cookie| Session::parse_token(cookie.value()).ok())
|
|
{
|
|
let database = Database::from(&state);
|
|
if let Ok(Some((session, user))) = database.find_session(session_id).await {
|
|
// session validity requirements: secret must match, session must not have expired
|
|
if session.secret == session_secret && session.expires_at >= Utc::now().naive_utc() {
|
|
// in the future we might wanna change the session secret, if we do, do it here!
|
|
if let Ok(session) = database
|
|
.refresh_session(session, Duration::seconds(state.config.session_duration))
|
|
.await
|
|
{
|
|
let extensions = req.extensions_mut();
|
|
extensions.insert(session.clone());
|
|
extensions.insert(user);
|
|
|
|
let mut response = next.run(req).await;
|
|
// Only set the session cookie if it hasn't been set yet (eg. logout)
|
|
let headers = response.headers_mut();
|
|
if !headers.contains_key(SET_COOKIE) {
|
|
headers.insert(
|
|
SET_COOKIE,
|
|
session
|
|
.cookie(state.config.domain().as_str(), state.config.secure())
|
|
.parse()
|
|
.unwrap(),
|
|
);
|
|
}
|
|
return response;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
next.run(req).await
|
|
}
|