Files
ticketsystem/backend/src/cookie/validation.rs
T
2026-06-07 11:56:16 +02:00

214 lines
7.3 KiB
Rust

use std::sync::Arc;
use axum::{
Json,
body::Body,
extract::State,
http::{Request, StatusCode, header},
middleware::Next,
response::IntoResponse,
};
use axum_extra::extract::CookieJar;
use jsonwebtoken::DecodingKey;
use serde_json::json;
use crate::{AppState, cookie::jwt::decode_token, handlers::auth::filter_user, models::User};
/// Axum middleware to validate a JWT token present in cookies or Authorization header.
///
/// This function extracts a JWT from the request (either from the `token` cookie or
/// the `Authorization: Bearer` header), decodes and validates it using [`decode_token`](`crate::cookie::jwt::decode_token`)).
/// If valid, it fetches the corresponding [`User`] from the database and inserts a
/// [`FilteredUser`](crate::models::FilteredUser)
/// (converted via [`filter_user`](`crate::handlers::auth::filter_user`)) into the request extensions for subsequent handlers to use.
///
/// If the token is missing, invalid, or the user is not found, it returns an
/// appropriate error response (401 Unauthorized).
///
/// # Arguments
/// - `cookies`: The `CookieJar` from the request, used to extract the `token` cookie.
/// - `State(data)`: Application state containing `AppState` for database access and `token_secret`.
/// - `mut request`: The incoming HTTP request, which will have user data injected into its extensions.
/// - `next`: The next middleware or handler in the chain.
///
/// # Returns
/// - `Ok(impl IntoResponse)`: If validation succeeds, the request proceeds to the next handler.
/// - `Err((StatusCode, Json<serde_json::Value>))`: An error response if validation fails.
pub async fn validate_token(
cookies: CookieJar,
State(data): State<Arc<AppState>>,
mut request: Request<Body>,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let token = cookies
.get("token")
.map(|cookie| cookie.value().to_string())
.or_else(|| {
request
.headers()
.get(header::AUTHORIZATION)
.and_then(|header| header.to_str().ok())
.and_then(|value| {
if value.starts_with("Bearer ") {
Some(value[7..].to_owned())
} else {
None
}
})
});
let token = token.ok_or_else(|| {
let error = json!({
"status": "error",
"message": "Please provide a valid token"
});
(StatusCode::UNAUTHORIZED, Json(error))
})?;
let claims = decode_token(
token,
&DecodingKey::from_secret(data.env.token_secret.as_ref()),
)
.map_err(|(status, json_err)| {
let error = json!({
"status": json_err.status,
"message": json_err.message
});
(status, Json(error))
})?;
let uuid = (&claims.sub).parse::<i16>().map_err(|_| {
let error = json!({
"status": "error",
"message": "Invalid user id"
});
(StatusCode::UNAUTHORIZED, Json(error))
})?;
let user = sqlx::query_as::<_, User>(r#"SELECT * FROM users WHERE id = $1"#)
.bind(uuid)
.fetch_optional(&data.db)
.await
.map_err(|e| {
let error = json!({
"status": "error",
"message": format!("Database error: {}", e)
});
(StatusCode::INTERNAL_SERVER_ERROR, Json(error))
})?;
let user = user.ok_or_else(|| {
let error = json!({
"status": "error",
"message": "Invalid user"
});
(StatusCode::UNAUTHORIZED, Json(error))
})?;
request.extensions_mut().insert(filter_user(&user));
Ok(next.run(request).await)
}
/// Axum middleware to validate JWT token and ensure the authenticated user has admin privileges.
///
/// This middleware first performs all checks of [`validate_token`]: extracting, decoding,
/// and validating the JWT via [`decode_token`](`crate::cookie::jwt::decode_token`), and fetching the associated [`User`] from the database.
/// Additionally, it verifies that the fetched user has `is_admin` set to `true`. Returns a [`FilteredUser`](crate::models::FilteredUser)
/// (converted via [`filter_user`](`crate::handlers::auth::filter_user`)) in the request extensions if both authentication and admin status are valid.
///
/// If the user is not authenticated or not an administrator, it returns an
/// appropriate error response (401 Unauthorized or 403 Forbidden).
///
/// # Arguments
/// - `cookies`: The `CookieJar` from the request.
/// - `State(data)`: Application state containing `AppState`.
/// - `mut request`: The incoming HTTP request, which will have admin user data injected.
/// - `next`: The next middleware or handler in the chain.
///
/// # Returns
/// - `Ok(impl IntoResponse)`: If validation and admin check succeed, the request proceeds.
/// - `Err((StatusCode, Json<serde_json::Value>))`: An error response if validation fails
/// or the user is not an admin.
pub async fn validate_admin(
cookies: CookieJar,
State(data): State<Arc<AppState>>,
mut request: Request<Body>,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let token = cookies
.get("token")
.map(|cookie| cookie.value().to_string())
.or_else(|| {
request
.headers()
.get(header::AUTHORIZATION)
.and_then(|header| header.to_str().ok())
.and_then(|value| {
if value.starts_with("Bearer ") {
Some(value[7..].to_owned())
} else {
None
}
})
});
let token = token.ok_or_else(|| {
let error = json!({
"status": "error",
"message": "Please provide a valid token"
});
(StatusCode::UNAUTHORIZED, Json(error))
})?;
let claims = decode_token(
token,
&DecodingKey::from_secret(data.env.token_secret.as_ref()),
)
.map_err(|(status, json_err)| {
let error = json!({
"status": json_err.status,
"message": json_err.message
});
(status, Json(error))
})?;
let uuid = (&claims.sub).parse::<i16>().map_err(|_| {
let error = json!({
"status": "error",
"message": "Invalid user id"
});
(StatusCode::UNAUTHORIZED, Json(error))
})?;
let user = sqlx::query_as::<_, User>(r#"SELECT * FROM users WHERE id = $1"#)
.bind(uuid)
.fetch_optional(&data.db)
.await
.map_err(|e| {
let error = json!({
"status": "error",
"message": format!("Database error: {}", e)
});
(StatusCode::INTERNAL_SERVER_ERROR, Json(error))
})?;
let user = user.ok_or_else(|| {
let error = json!({
"status": "error",
"message": "Invalid user"
});
(StatusCode::UNAUTHORIZED, Json(error))
})?;
if !user.is_admin {
let error = json!({
"status": "error",
"message": "Admin access required"
});
return Err((StatusCode::FORBIDDEN, Json(error)));
}
request.extensions_mut().insert(filter_user(&user));
Ok(next.run(request).await)
}