Working Login and Logout with Permissions

This commit is contained in:
2025-12-05 23:43:29 +01:00
parent 1ae9057baf
commit c1fee82ba9
5 changed files with 105 additions and 33 deletions

View File

@@ -25,6 +25,7 @@ tracing-subscriber = "0.3.22"
uuid = { version = "1.19.0", features = ["v4", "serde"] } uuid = { version = "1.19.0", features = ["v4", "serde"] }
validator = { version = "0.20.0", features = ["derive"] } validator = { version = "0.20.0", features = ["derive"] }
mineguard = {path = "../../../MineGuard/"} mineguard = {path = "../../../MineGuard/"}
axum-extra = { version = "0.12.2", features = ["cookie"] }
[build-dependencies] [build-dependencies]
chrono = "0.4.42" chrono = "0.4.42"

View File

@@ -17,7 +17,10 @@ use crate::{
state::AppState, state::AppState,
}; };
pub async fn login(state: Arc<AppState>, login_data: LoginData) -> Result<String, StatusCode> { pub async fn login(
state: Arc<AppState>,
login_data: LoginData,
) -> Result<(String, User), StatusCode> {
debug!(username = login_data.username.as_str(), "login started"); debug!(username = login_data.username.as_str(), "login started");
let user = db::user::get_by_username(&state.db_pool, &login_data.username) let user = db::user::get_by_username(&state.db_pool, &login_data.username)
@@ -45,7 +48,7 @@ pub async fn login(state: Arc<AppState>, login_data: LoginData) -> Result<String
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?; })?;
Ok(token) Ok((token, User::from(user)))
} }
pub async fn create(state: Arc<AppState>, new_user: NewUser) -> Result<User, StatusCode> { pub async fn create(state: Arc<AppState>, new_user: NewUser) -> Result<User, StatusCode> {

View File

@@ -9,6 +9,7 @@ use axum::{
response::Response, response::Response,
}; };
use axum_extra::extract::CookieJar;
use tower_http::cors::{Any, CorsLayer}; use tower_http::cors::{Any, CorsLayer};
use tracing::debug; use tracing::debug;
@@ -16,50 +17,84 @@ use crate::{auth::verify_jwt, infra::db, state::AppState};
pub async fn auth( pub async fn auth(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
jar: CookieJar,
mut req: Request, mut req: Request,
next: Next, next: Next,
) -> Result<Response, StatusCode> { ) -> Result<Response, StatusCode> {
let request_method = req.method().clone(); let method = req.method().clone();
let request_path = req.uri().path().to_string(); 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 // 1) Try JWT from cookie first
.headers() let token_from_cookie = jar
.get(http::header::AUTHORIZATION) .get("auth_token")
.ok_or(StatusCode::FORBIDDEN)?; .map(|cookie| cookie.value().to_owned());
let auth_header = auth_header.to_str().map_err(|e| { // 2) If no cookie, fall back to Authorization: Bearer ...
error!(error = %e, method = ?request_method, path = request_path, "authorization header parse failed"); let token = match token_from_cookie {
StatusCode::FORBIDDEN 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 auth_header = auth_header.to_str().map_err(|e| {
let (scheme, token) = match (parts.next(), parts.next()) { error!(
(Some(scheme), Some(token)) if scheme.eq_ignore_ascii_case("bearer") => (scheme, token), error = %e,
_ => { ?method,
warn!(method = ?request_method, path = request_path, "authorization header missing bearer token"); path,
return Err(StatusCode::UNAUTHORIZED); "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 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 .await
.map_err(|e| { .map_err(|e| {
error!(error = %e, method = ?request_method, path = request_path, username, "fetch user for auth failed"); error!(
return StatusCode::INTERNAL_SERVER_ERROR; error = %e,
})? { ?method,
Some(user) => user, path,
None => { username,
error!(method = ?request_method, path = request_path, username, "authenticated user missing in database"); "fetch user for auth failed"
return Err(StatusCode::INTERNAL_SERVER_ERROR); );
} 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); req.extensions_mut().insert(current_user);
// 6) Continue the chain
Ok(next.run(req).await) Ok(next.run(req).await)
} }
@@ -82,6 +117,10 @@ pub async fn permissions(
debug!(method = ?request_method, path = request_path, "permissions request started"); debug!(method = ?request_method, path = request_path, "permissions request started");
debug!("Calling user {}", user.username.clone()); debug!("Calling user {}", user.username.clone());
if user.permissions.root == true {
return Ok(next.run(req).await);
}
let method: Method = req.method().clone(); let method: Method = req.method().clone();
let path = req let path = req

View File

@@ -58,6 +58,10 @@ pub async fn init_router(app_state: Arc<AppState>) -> Router {
post(user_routes::login) post(user_routes::login)
.layer(middleware!(cors)) .layer(middleware!(cors))
.with_state(app_state.clone()), .with_state(app_state.clone()),
)
.route(
"/api/logout",
post(user_routes::logout).layer(middleware!(cors)),
); );
info!("router initialization completed"); info!("router initialization completed");

View File

@@ -12,6 +12,10 @@ use axum::{
extract::{Path, State}, extract::{Path, State},
http::StatusCode, http::StatusCode,
}; };
use axum_extra::extract::{
CookieJar,
cookie::{Cookie, SameSite},
};
use uuid::Uuid; use uuid::Uuid;
pub async fn create( pub async fn create(
@@ -43,8 +47,29 @@ pub async fn get_uuid(
pub async fn login( pub async fn login(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
jar: CookieJar,
Json(login_data): Json<LoginData>, Json(login_data): Json<LoginData>,
) -> Result<Json<String>, StatusCode> { ) -> Result<(CookieJar, Json<User>), StatusCode> {
let result = core::user_routines::login(state, login_data).await?; let (jwt, user) = core::user_routines::login(state, login_data).await?;
Ok(Json(result))
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<CookieJar, StatusCode> {
let cookie = Cookie::build(("auth_token", ""))
.path("/")
.http_only(true)
.build();
let jar = jar.remove(cookie);
Ok(jar)
} }