mabel/src/http/session.rs

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
}