From 44c083a8efefab4cbb19b7f7005847e47607d03d Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Sat, 19 Sep 2026 12:30:47 +0200 Subject: [PATCH] refactor: HttpSession/OptionalHttpSession axum extractors --- src/http/domain.rs | 16 ++------ src/http/home.rs | 91 +++++++++++++++++++++------------------------ src/http/login.rs | 16 +++++++- src/http/logout.rs | 5 ++- src/http/mod.rs | 4 +- src/http/session.rs | 46 ++++++++++++++++++++++- src/http/user.rs | 16 +------- 7 files changed, 112 insertions(+), 82 deletions(-) diff --git a/src/http/domain.rs b/src/http/domain.rs index 8bccc5b..1acfdbb 100644 --- a/src/http/domain.rs +++ b/src/http/domain.rs @@ -1,23 +1,17 @@ use axum::extract::{Form, Path, State}; use axum::response::{Html, IntoResponse, Redirect, Response}; -use axum_extra::extract::cookie::CookieJar; use http::StatusCode; use minijinja::context; use serde::Deserialize; use crate::db::{DatabaseInterface, Operation}; -use crate::http::HttpState; -use crate::http::login::login_page; +use crate::http::{HttpSession, HttpState}; pub async fn get_domain( State(state): State>, - cookies: CookieJar, + session: HttpSession, Path(domain): Path, ) -> Response { - let Some(session) = state.sessions.get_session(&cookies) else { - return login_page(State(state), None).await.into_response(); - }; - let domain = match state.db.get_domain(&domain).await { Ok(Some(domain)) => domain, Ok(None) => return format!("Domain not found: {domain}").into_response(), @@ -61,13 +55,9 @@ pub struct DomainCreationForm { pub async fn create_domain( State(mut state): State>, - cookies: CookieJar, + session: HttpSession, Form(form): Form, ) -> Response { - let Some(session) = state.sessions.get_session(&cookies) else { - return login_page(State(state), None).await.into_response(); - }; - let op = Operation::CreateDomain; if !session.user.can_perform(&op) { return "Not authorized to create a new domain".into_response(); diff --git a/src/http/home.rs b/src/http/home.rs index 595ac17..febb6b0 100644 --- a/src/http/home.rs +++ b/src/http/home.rs @@ -1,63 +1,58 @@ use axum::extract::State; use axum::response::{Html, IntoResponse, Response}; -use axum_extra::extract::cookie::CookieJar; use http::StatusCode; use minijinja::context; use crate::db::{DatabaseInterface, Operation}; -use crate::http::HttpState; -use crate::http::login::login_page; +use crate::http::{HttpSession, HttpState}; pub async fn home( State(state): State>, - cookies: CookieJar, + // Only logged in users are allowed here + session: HttpSession, ) -> Response { - if let Some(session) = state.sessions.get_session(&cookies) { - // When the user has no domain (service admin) list all domains - let op = Operation::ListUsers(session.user.domain.clone()); - let other_users = if session.user.can_perform(&op) { - match state - .db - .list_domain_users(session.user.domain.clone()) - .await - { - Ok(other_users) => other_users, - Err(e) => { - return format!("Database error: {e}").into_response(); - } - } - } else { - vec![] - }; - - tracing::info!( - "Found {} users on domain {:?}", - other_users.len(), - session.user.domain - ); - - let domains = match state.db.domains_user_can_see(&session.user).await { - Ok(domains) => domains, + // When the user has no domain (service admin) list all domains + let op = Operation::ListUsers(session.user.domain.clone()); + let other_users = if session.user.can_perform(&op) { + match state + .db + .list_domain_users(session.user.domain.clone()) + .await + { + Ok(other_users) => other_users, Err(e) => { return format!("Database error: {e}").into_response(); } - }; - - let ctx = context! { - domains, - user => session.user, - can_create_domain => session.user.can_create_domain(), - other_users, - }; - - let page = state - .templates - .get_template("home.html") - .unwrap() - .render(ctx) - .unwrap(); - (StatusCode::OK, Html(page)).into_response() + } } else { - login_page(State(state), None).await - } + vec![] + }; + + tracing::info!( + "Found {} users on domain {:?}", + other_users.len(), + session.user.domain + ); + + let domains = match state.db.domains_user_can_see(&session.user).await { + Ok(domains) => domains, + Err(e) => { + return format!("Database error: {e}").into_response(); + } + }; + + let ctx = context! { + domains, + user => session.user, + can_create_domain => session.user.can_create_domain(), + other_users, + }; + + let page = state + .templates + .get_template("home.html") + .unwrap() + .render(ctx) + .unwrap(); + (StatusCode::OK, Html(page)).into_response() } diff --git a/src/http/login.rs b/src/http/login.rs index f62a7b0..8b4a26d 100644 --- a/src/http/login.rs +++ b/src/http/login.rs @@ -6,7 +6,7 @@ use minijinja::context; use serde::{Deserialize, Serialize}; use crate::db::{DatabaseInterface, UserRef}; -use crate::http::HttpState; +use crate::http::{HttpState, OptionalHttpSession}; #[derive(Debug, Deserialize)] pub struct LoginForm { @@ -33,12 +33,24 @@ pub async fn login_page( (StatusCode::OK, Html(page)).into_response() } +pub async fn get_login( + State(state): State>, + maybe_session: Option, +) -> Response { + if maybe_session.is_some() { + return Redirect::to("/").into_response(); + } + + login_page(State(state), None).await +} + pub async fn post_login( State(state): State>, + session: Option, cookies: CookieJar, Form(form): Form, ) -> Response { - if let Some(_session) = state.sessions.get_session(&cookies) { + if session.is_some() { // Already logged in return (cookies, Redirect::to("/")).into_response(); } diff --git a/src/http/logout.rs b/src/http/logout.rs index 5e38309..c742074 100644 --- a/src/http/logout.rs +++ b/src/http/logout.rs @@ -3,14 +3,15 @@ use axum::response::{IntoResponse, Redirect, Response}; use axum_extra::extract::cookie::CookieJar; use crate::db::DatabaseInterface; -use crate::http::HttpState; use crate::http::login::{LoginError, login_page}; +use crate::http::{HttpState, OptionalHttpSession}; pub async fn logout( State(state): State>, + session: Option, cookies: CookieJar, ) -> Response { - let Some(session) = state.sessions.get_session(&cookies) else { + let Some(session) = session else { return (cookies, Redirect::to("/")).into_response(); }; diff --git a/src/http/mod.rs b/src/http/mod.rs index def0f22..17c9448 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -18,7 +18,7 @@ mod home; mod login; mod logout; mod session; -use session::HttpSessionManager; +use session::{HttpSession, HttpSessionManager, OptionalHttpSession}; mod user; impl AxumListener for Listener { @@ -98,7 +98,7 @@ pub async fn http_listen(listener: Listener, db: Database< let app = { Router::new().nest_service("/assets", ServeDir::new("assets")) }; let app = app .route("/", get(home::home)) - .route("/login", get(home::home)) + .route("/login", get(login::get_login)) .route("/login", post(login::post_login)) .route("/logout", get(logout::logout)) .route("/domain/{domain}", get(domain::get_domain)) diff --git a/src/http/session.rs b/src/http/session.rs index 5a3a8db..5005b8c 100644 --- a/src/http/session.rs +++ b/src/http/session.rs @@ -1,9 +1,13 @@ +use axum::extract::{FromRequestParts, OptionalFromRequestParts}; +use axum::response::Redirect; use axum_extra::extract::cookie::{Cookie, CookieJar}; +use http::request::Parts; use uuid::Uuid; use std::sync::{Arc, RwLock}; -use crate::db::User; +use crate::db::{DatabaseInterface, User}; +use crate::http::HttpState; pub const COOKIE_NAME: &str = "lldap_session"; @@ -78,3 +82,43 @@ impl PartialEq for HttpSession { self.uuid == other.uuid } } + +impl FromRequestParts> for HttpSession { + type Rejection = Redirect; + + async fn from_request_parts( + parts: &mut Parts, + state: &HttpState, + ) -> Result { + let cookies = CookieJar::from_request_parts(parts, state).await.unwrap(); + state + .sessions + .get_session(&cookies) + .ok_or(Redirect::to("/login")) + } +} + +pub struct OptionalHttpSession(pub HttpSession); + +impl std::ops::Deref for OptionalHttpSession { + type Target = HttpSession; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl OptionalFromRequestParts> for OptionalHttpSession { + type Rejection = Redirect; + + async fn from_request_parts( + parts: &mut Parts, + state: &HttpState, + ) -> Result, Self::Rejection> { + let maybe_session = HttpSession::from_request_parts(parts, state) + .await + .ok() + .map(Self); + Ok(maybe_session) + } +} diff --git a/src/http/user.rs b/src/http/user.rs index d6eb054..2d0d385 100644 --- a/src/http/user.rs +++ b/src/http/user.rs @@ -1,11 +1,9 @@ use axum::extract::{Form, State}; use axum::response::{IntoResponse, Redirect, Response}; -use axum_extra::extract::cookie::CookieJar; use serde::Deserialize; use crate::db::{DatabaseInterface, Role, User}; -use crate::http::HttpState; -use crate::http::login::login_page; +use crate::http::{HttpSession, HttpState}; #[derive(Clone, Debug, Deserialize)] pub struct UserCreationForm { @@ -16,19 +14,9 @@ pub struct UserCreationForm { pub async fn create_user( State(mut state): State>, - cookies: CookieJar, + session: HttpSession, Form(form): Form, ) -> Response { - let Some(session) = state.sessions.get_session(&cookies) else { - return login_page(State(state), None).await.into_response(); - }; - - // let domain = form.domain; - // let op = Operation::CreateUser(domain.clone()); - // if !session.user.can_perform(&op) { - // return format!("Not authorized to create a new user on domain {domain}").into_response() - // } - let new_user = User { mail: format!("{}@{}", form.username, form.domain), username: form.username.clone(),