From c1fee82ba9577a7baf2943bded40d590562a39fe Mon Sep 17 00:00:00 2001 From: Hector van der Aa Date: Fri, 5 Dec 2025 23:43:29 +0100 Subject: [PATCH] Working Login and Logout with Permissions --- src/backend/Cargo.toml | 1 + src/backend/src/core/user_routines.rs | 7 +- src/backend/src/router/middleware.rs | 95 +++++++++++++++++++-------- src/backend/src/router/mod.rs | 4 ++ src/backend/src/router/user_routes.rs | 31 ++++++++- 5 files changed, 105 insertions(+), 33 deletions(-) diff --git a/src/backend/Cargo.toml b/src/backend/Cargo.toml index 4d03454..d79f0b0 100644 --- a/src/backend/Cargo.toml +++ b/src/backend/Cargo.toml @@ -25,6 +25,7 @@ tracing-subscriber = "0.3.22" uuid = { version = "1.19.0", features = ["v4", "serde"] } validator = { version = "0.20.0", features = ["derive"] } mineguard = {path = "../../../MineGuard/"} +axum-extra = { version = "0.12.2", features = ["cookie"] } [build-dependencies] chrono = "0.4.42" diff --git a/src/backend/src/core/user_routines.rs b/src/backend/src/core/user_routines.rs index fb0714d..118795d 100644 --- a/src/backend/src/core/user_routines.rs +++ b/src/backend/src/core/user_routines.rs @@ -17,7 +17,10 @@ use crate::{ state::AppState, }; -pub async fn login(state: Arc, login_data: LoginData) -> Result { +pub async fn login( + state: Arc, + login_data: LoginData, +) -> Result<(String, User), StatusCode> { debug!(username = login_data.username.as_str(), "login started"); let user = db::user::get_by_username(&state.db_pool, &login_data.username) @@ -45,7 +48,7 @@ pub async fn login(state: Arc, login_data: LoginData) -> Result, new_user: NewUser) -> Result { diff --git a/src/backend/src/router/middleware.rs b/src/backend/src/router/middleware.rs index 828c992..1520de5 100644 --- a/src/backend/src/router/middleware.rs +++ b/src/backend/src/router/middleware.rs @@ -9,6 +9,7 @@ use axum::{ response::Response, }; +use axum_extra::extract::CookieJar; use tower_http::cors::{Any, CorsLayer}; use tracing::debug; @@ -16,50 +17,84 @@ use crate::{auth::verify_jwt, infra::db, state::AppState}; pub async fn auth( State(state): State>, + jar: CookieJar, mut req: Request, next: Next, ) -> Result { - let request_method = req.method().clone(); - let request_path = req.uri().path().to_string(); + let method = req.method().clone(); + let path = req.uri().path().to_string(); - debug!(method = ?request_method, path = request_path, "authenticate request started"); + debug!(?method, path, "authenticate request started"); - let auth_header = req - .headers() - .get(http::header::AUTHORIZATION) - .ok_or(StatusCode::FORBIDDEN)?; + // 1) Try JWT from cookie first + let token_from_cookie = jar + .get("auth_token") + .map(|cookie| cookie.value().to_owned()); - let auth_header = auth_header.to_str().map_err(|e| { - error!(error = %e, method = ?request_method, path = request_path, "authorization header parse failed"); - StatusCode::FORBIDDEN - })?; + // 2) If no cookie, fall back to Authorization: Bearer ... + let token = match token_from_cookie { + Some(t) => t, + None => { + let auth_header = req + .headers() + .get(http::header::AUTHORIZATION) + .ok_or(StatusCode::FORBIDDEN)?; - let mut parts = auth_header.split_whitespace(); - let (scheme, token) = match (parts.next(), parts.next()) { - (Some(scheme), Some(token)) if scheme.eq_ignore_ascii_case("bearer") => (scheme, token), - _ => { - warn!(method = ?request_method, path = request_path, "authorization header missing bearer token"); - return Err(StatusCode::UNAUTHORIZED); + let auth_header = auth_header.to_str().map_err(|e| { + error!( + error = %e, + ?method, + path, + "authorization header parse failed" + ); + StatusCode::FORBIDDEN + })?; + + let mut parts = auth_header.split_whitespace(); + match (parts.next(), parts.next()) { + (Some(scheme), Some(token)) if scheme.eq_ignore_ascii_case("bearer") => { + token.to_owned() + } + _ => { + warn!(?method, path, "authorization header missing bearer token"); + return Err(StatusCode::UNAUTHORIZED); + } + } } }; - let token_data = verify_jwt(token.to_string())?; + // 3) Verify JWT + let token_data = verify_jwt(token).map_err(|e| { + warn!(error = %e, ?method, path, "invalid jwt"); + StatusCode::UNAUTHORIZED + })?; let username = &token_data.claims.username; - let current_user = match user_routines::get_by_username(state, username) + // 4) Load user from DB + let current_user = user_routines::get_by_username(state, username) .await .map_err(|e| { - error!(error = %e, method = ?request_method, path = request_path, username, "fetch user for auth failed"); - return StatusCode::INTERNAL_SERVER_ERROR; - })? { - Some(user) => user, - None => { - error!(method = ?request_method, path = request_path, username, "authenticated user missing in database"); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - }; + error!( + error = %e, + ?method, + path, + username, + "fetch user for auth failed" + ); + StatusCode::INTERNAL_SERVER_ERROR + })? + .ok_or_else(|| { + error!( + ?method, + path, username, "authenticated user missing in database" + ); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + // 5) Attach user to request extensions req.extensions_mut().insert(current_user); + // 6) Continue the chain Ok(next.run(req).await) } @@ -82,6 +117,10 @@ pub async fn permissions( debug!(method = ?request_method, path = request_path, "permissions request started"); debug!("Calling user {}", user.username.clone()); + if user.permissions.root == true { + return Ok(next.run(req).await); + } + let method: Method = req.method().clone(); let path = req diff --git a/src/backend/src/router/mod.rs b/src/backend/src/router/mod.rs index 947cd72..55345a7 100644 --- a/src/backend/src/router/mod.rs +++ b/src/backend/src/router/mod.rs @@ -58,6 +58,10 @@ pub async fn init_router(app_state: Arc) -> Router { post(user_routes::login) .layer(middleware!(cors)) .with_state(app_state.clone()), + ) + .route( + "/api/logout", + post(user_routes::logout).layer(middleware!(cors)), ); info!("router initialization completed"); diff --git a/src/backend/src/router/user_routes.rs b/src/backend/src/router/user_routes.rs index afb15ac..51de9e1 100644 --- a/src/backend/src/router/user_routes.rs +++ b/src/backend/src/router/user_routes.rs @@ -12,6 +12,10 @@ use axum::{ extract::{Path, State}, http::StatusCode, }; +use axum_extra::extract::{ + CookieJar, + cookie::{Cookie, SameSite}, +}; use uuid::Uuid; pub async fn create( @@ -43,8 +47,29 @@ pub async fn get_uuid( pub async fn login( State(state): State>, + jar: CookieJar, Json(login_data): Json, -) -> Result, StatusCode> { - let result = core::user_routines::login(state, login_data).await?; - Ok(Json(result)) +) -> Result<(CookieJar, Json), StatusCode> { + let (jwt, user) = core::user_routines::login(state, login_data).await?; + + let cookie = Cookie::build(("auth_token", jwt)) + .http_only(true) + .secure(false) + .same_site(SameSite::None) + .path("/") + .build(); + + let jar = jar.add(cookie); + + Ok((jar, Json(user))) +} + +pub async fn logout(jar: CookieJar) -> Result { + let cookie = Cookie::build(("auth_token", "")) + .path("/") + .http_only(true) + .build(); + let jar = jar.remove(cookie); + + Ok(jar) }