refactor: HttpSession/OptionalHttpSession axum extractors

This commit is contained in:
selfhoster selfhoster 2026-09-19 12:30:47 +02:00
commit 44c083a8ef
7 changed files with 111 additions and 81 deletions

View file

@ -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<D: DatabaseInterface>(
State(state): State<HttpState<D>>,
cookies: CookieJar,
session: HttpSession,
Path(domain): Path<String>,
) -> 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<D: DatabaseInterface>(
State(mut state): State<HttpState<D>>,
cookies: CookieJar,
session: HttpSession,
Form(form): Form<DomainCreationForm>,
) -> 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();

View file

@ -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<D: DatabaseInterface>(
State(state): State<HttpState<D>>,
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()
}

View file

@ -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<D: DatabaseInterface>(
(StatusCode::OK, Html(page)).into_response()
}
pub async fn get_login<D: DatabaseInterface>(
State(state): State<HttpState<D>>,
maybe_session: Option<OptionalHttpSession>,
) -> Response {
if maybe_session.is_some() {
return Redirect::to("/").into_response();
}
login_page(State(state), None).await
}
pub async fn post_login<D: DatabaseInterface>(
State(state): State<HttpState<D>>,
session: Option<OptionalHttpSession>,
cookies: CookieJar,
Form(form): Form<LoginForm>,
) -> Response {
if let Some(_session) = state.sessions.get_session(&cookies) {
if session.is_some() {
// Already logged in
return (cookies, Redirect::to("/")).into_response();
}

View file

@ -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<D: DatabaseInterface>(
State(state): State<HttpState<D>>,
session: Option<OptionalHttpSession>,
cookies: CookieJar,
) -> Response {
let Some(session) = state.sessions.get_session(&cookies) else {
let Some(session) = session else {
return (cookies, Redirect::to("/")).into_response();
};

View file

@ -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<D: DatabaseInterface>(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))

View file

@ -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<HttpSession> for HttpSession {
self.uuid == other.uuid
}
}
impl<D: DatabaseInterface> FromRequestParts<HttpState<D>> for HttpSession {
type Rejection = Redirect;
async fn from_request_parts(
parts: &mut Parts,
state: &HttpState<D>,
) -> Result<Self, Self::Rejection> {
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<D: DatabaseInterface> OptionalFromRequestParts<HttpState<D>> for OptionalHttpSession {
type Rejection = Redirect;
async fn from_request_parts(
parts: &mut Parts,
state: &HttpState<D>,
) -> Result<Option<Self>, Self::Rejection> {
let maybe_session = HttpSession::from_request_parts(parts, state)
.await
.ok()
.map(Self);
Ok(maybe_session)
}
}

View file

@ -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<D: DatabaseInterface>(
State(mut state): State<HttpState<D>>,
cookies: CookieJar,
session: HttpSession,
Form(form): Form<UserCreationForm>,
) -> 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(),