Working Login and Logout with Permissions
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
// 1) Try JWT from cookie first
|
||||||
|
let token_from_cookie = jar
|
||||||
|
.get("auth_token")
|
||||||
|
.map(|cookie| cookie.value().to_owned());
|
||||||
|
|
||||||
|
// 2) If no cookie, fall back to Authorization: Bearer ...
|
||||||
|
let token = match token_from_cookie {
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
let auth_header = req
|
let auth_header = req
|
||||||
.headers()
|
.headers()
|
||||||
.get(http::header::AUTHORIZATION)
|
.get(http::header::AUTHORIZATION)
|
||||||
.ok_or(StatusCode::FORBIDDEN)?;
|
.ok_or(StatusCode::FORBIDDEN)?;
|
||||||
|
|
||||||
let auth_header = auth_header.to_str().map_err(|e| {
|
let auth_header = auth_header.to_str().map_err(|e| {
|
||||||
error!(error = %e, method = ?request_method, path = request_path, "authorization header parse failed");
|
error!(
|
||||||
|
error = %e,
|
||||||
|
?method,
|
||||||
|
path,
|
||||||
|
"authorization header parse failed"
|
||||||
|
);
|
||||||
StatusCode::FORBIDDEN
|
StatusCode::FORBIDDEN
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut parts = auth_header.split_whitespace();
|
let mut parts = auth_header.split_whitespace();
|
||||||
let (scheme, token) = match (parts.next(), parts.next()) {
|
match (parts.next(), parts.next()) {
|
||||||
(Some(scheme), Some(token)) if scheme.eq_ignore_ascii_case("bearer") => (scheme, token),
|
(Some(scheme), Some(token)) if scheme.eq_ignore_ascii_case("bearer") => {
|
||||||
|
token.to_owned()
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
warn!(method = ?request_method, path = request_path, "authorization header missing bearer token");
|
warn!(?method, path, "authorization header missing bearer token");
|
||||||
return Err(StatusCode::UNAUTHORIZED);
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let token_data = verify_jwt(token.to_string())?;
|
|
||||||
let username = &token_data.claims.username;
|
|
||||||
|
|
||||||
let current_user = match 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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 4) Load user from DB
|
||||||
|
let current_user = user_routines::get_by_username(state, username)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
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);
|
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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user