Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 8 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@ repository = "https://github.com/mairie360/API_lib"

[dependencies]
actix-web = "4"
jsonwebtoken = { version = "10.3.0", default-features = false, features = ["rust_crypto"] }
anyhow = "1.0.102"
async-trait = "0.1.89"
axum = "0.8.8"
deadpool-redis = { version = "0.23.0", features = ["rt_tokio_1"] }
futures-util = "0.3"
jsonwebtoken = { version = "10.3.0", default-features = false, features = ["rust_crypto"] }
once_cell = "1.21.3"
redis = "1.0.3"
tokio = { version = "1.49.0", features = ["full"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tokio-postgres = { version = "0.7", features = ["with-uuid-1"] }
thiserror = "2.0.18"
deadpool-redis = { version = "0.23.0", features = ["rt_tokio_1"] }
sqlx = { version = "0.8.6", features = ["postgres", "ipnetwork", "uuid", "runtime-tokio-rustls"] }
axum = "0.8.8"
anyhow = "1.0.102"
testcontainers = "0.27.0"
thiserror = "2.0.18"
tokio = { version = "1.49.0", features = ["full"] }
tokio-postgres = { version = "0.7", features = ["with-uuid-1"] }

[dev-dependencies]
serial_test = "3.3.1"
Expand Down
38 changes: 15 additions & 23 deletions src/jwt_manager/generate_jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,19 @@ use jsonwebtoken::{encode, EncodingKey, Header};
use std::time::{SystemTime, UNIX_EPOCH};

pub fn generate_jwt(user_id_str: &str) -> Result<String, jsonwebtoken::errors::Error> {
let secret: Vec<u8> = get_jwt_secret().map_err(|_e| {
jsonwebtoken::errors::Error::from(jsonwebtoken::errors::ErrorKind::InvalidKeyFormat)
})?;
let timeout = get_jwt_timeout().map_err(|_e| {
jsonwebtoken::errors::Error::from(jsonwebtoken::errors::ErrorKind::InvalidKeyFormat)
});
match timeout {
Ok(t) => {
let expiration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as usize
+ t; // Token valid for the configured JWT timeout duration
let claims = Claims::new(user_id_str.to_owned(), expiration);
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(&secret),
)?;
Ok(token)
}
Err(e) => Err(e),
}
let secret: Vec<u8> = get_jwt_secret()?;
let timeout = get_jwt_timeout()?;

let expiration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as usize
+ timeout; // Token valid for the configured JWT timeout duration
let claims = Claims::new(user_id_str.to_owned(), expiration);
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(&secret),
)?;
Ok(token)
}
6 changes: 4 additions & 2 deletions src/jwt_manager/get_jwt_secret.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::env_manager::get_env_var;

pub fn get_jwt_secret() -> Result<Vec<u8>, String> {
pub fn get_jwt_secret() -> Result<Vec<u8>, jsonwebtoken::errors::ErrorKind> {
match get_env_var("JWT_SECRET") {
Some(secret) => Ok(secret.into_bytes()),
None => Err("JWT_SECRET environment variable not set".to_string()),
None => Err(jsonwebtoken::errors::ErrorKind::MissingRequiredClaim(
"JWT_SECRET environment variable not set".to_string(),
)),
Comment on lines +3 to +8
}
}
15 changes: 10 additions & 5 deletions src/jwt_manager/get_jwt_timeout.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use jsonwebtoken::errors::ErrorKind::InvalidKeyFormat;

use crate::env_manager::get_env_var;

pub fn get_jwt_timeout() -> Result<usize, String> {
pub fn get_jwt_timeout() -> Result<usize, jsonwebtoken::errors::ErrorKind> {
match get_env_var("JWT_TIMEOUT") {
Some(secret) => secret
.parse::<usize>()
.map_err(|_| "JWT_TIMEOUT is not a valid usize".to_string()),
None => Err("JWT_TIMEOUT environment variable not set".to_string()),
Some(secret) => {
let secret = secret.parse::<usize>().map_err(|_| InvalidKeyFormat)?;
Ok(secret)
}
None => Err(jsonwebtoken::errors::ErrorKind::MissingRequiredClaim(
"JWT_TIMEOUT environment variable not set".to_string(),
Comment on lines +5 to +12
)),
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ pub mod env_manager;
pub mod jwt_manager;
pub mod pool;
mod redis;
pub mod security;
pub mod test_setup;
38 changes: 25 additions & 13 deletions src/pool/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,49 @@ use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;

pub struct AppState {
redis_pool: Pool,
pub db_pool: PgPool,
redis_pool: Option<Pool>,
pub db_pool: Option<PgPool>,
Comment on lines +7 to +8
}

impl AppState {
pub async fn new(redis_url: String, pg_url: String) -> Self {
// --- Initialisation Redis ---
let redis_cfg = Config::from_url(redis_url);
let redis_pool = redis_cfg
.create_pool(Some(Runtime::Tokio1))
.expect("Failed to create Redis pool");
let redis_pool = redis_cfg.create_pool(Some(Runtime::Tokio1));

// --- Initialisation PostgreSQL ---
let db_pool = PgPoolOptions::new()
.max_connections(5)
.acquire_timeout(std::time::Duration::from_secs(3))
.connect(&pg_url)
.await
.expect("Failed to create Postgres pool");
.await;

eprintln!("redis status: {:?}", redis_pool.is_ok());
eprintln!("pg status: {:?}", db_pool.is_ok());

Self {
redis_pool,
db_pool,
redis_pool: match redis_pool {
Ok(pool) => Some(pool),
Err(_) => None,
},
db_pool: match db_pool {
Ok(pool) => Some(pool),
Err(_) => None,
Comment on lines 27 to +34
},
}
}

pub async fn get_redis_conn(&self) -> deadpool_redis::Connection {
self.redis_pool.get().await.unwrap()
pub async fn get_redis_conn(&self) -> Option<deadpool_redis::Connection> {
match &self.redis_pool {
Some(pool) => pool.get().await.ok(),
None => None,
}
}

pub async fn get_db_conn(&self) -> sqlx::pool::PoolConnection<sqlx::Postgres> {
self.db_pool.acquire().await.unwrap()
pub async fn get_db_conn(&self) -> Option<sqlx::pool::PoolConnection<sqlx::Postgres>> {
match &self.db_pool {
Some(pool) => pool.acquire().await.ok(),
None => None,
}
}
}
12 changes: 11 additions & 1 deletion src/pool/redis/handle_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ use axum::{extract::State, http::StatusCode, response::IntoResponse};
use std::sync::Arc;

pub async fn handle_get(State(state): State<Arc<AppState>>) -> impl IntoResponse {
match state.redis_pool.get().await {
let pool = match state.redis_pool.as_ref() {
Some(pool) => pool,
None => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Redis pool not initialized",
)
.into_response()
}
};
match pool.get().await {
Ok(_) => (StatusCode::OK, "Connexion Redis réussie !").into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Échec connexion Redis").into_response(),
}
Expand Down
149 changes: 149 additions & 0 deletions src/security/auth_middleware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use actix_web::{
body::EitherBody,
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error, HttpMessage, HttpResponse,
};
use futures_util::future::LocalBoxFuture;
use std::future::{ready, Ready};
use std::rc::Rc;

use crate::jwt_manager::{
check_jwt_validity, get_jwt_from_request, get_user_id_from_jwt, JWTCheckError,
};
use crate::pool::AppState;

use crate::security::AuthenticatedUser;

/**
* Middleware to check the validity of JWT tokens in incoming requests.
* If the token is valid, the request is passed to the next service in the chain.
* If the token is invalid or missing, an appropriate HTTP response is returned.
*/
pub struct JwtMiddleware;

impl<S, B> Transform<S, ServiceRequest> for JwtMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type InitError = ();
type Transform = JwtMiddlewareService<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;

/**
* Creates a new instance of the middleware service, wrapping the provided service.
*/
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(JwtMiddlewareService {
service: Rc::new(service),
}))
}
}

/**
* Service that implements the actual logic of checking JWT tokens for each incoming request.
* It uses the `get_jwt_from_request` function to extract the token and the `check_jwt_validity` function to validate it.
* Depending on the result, it either forwards the request to the next service or returns an appropriate HTTP response.
*/
pub struct JwtMiddlewareService<S> {
service: Rc<S>,
}

impl<S, B> Service<ServiceRequest> for JwtMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

forward_ready!(service);

/**
* Handles the incoming request by checking for a JWT token and validating it.
*/
fn call(&self, req: ServiceRequest) -> Self::Future {
let svc = self.service.clone();
let app_state = req.app_data::<actix_web::web::Data<AppState>>();

// On clone le pool pour la closure async move
let pool = match app_state {
Some(state) => state.db_pool.clone(),
None => None,
};

let path = req.path();
if path == "/"
|| path.starts_with("/swagger-ui")
|| path.starts_with("/api-docs")
|| path.contains("/auth")
{
return Box::pin(async move {
let res = svc.call(req).await?;
Ok(res.map_into_left_body())
});
}

Box::pin(async move {
let pool = match pool {
Some(p) => p,
None => {
// Erreur si le pool n'a pas été injecté dans l'App
let res = HttpResponse::InternalServerError()
.body("DB Pool missing")
.map_into_right_body();
return Ok(req.into_response(res));
}
};

let jwt_option = get_jwt_from_request(req.request());

let jwt = match jwt_option {
Some(token) => token,
None => {
eprint!("no jwt");
let response = HttpResponse::Unauthorized()
.body("Unauthorized: No JWT token provided.")
.map_into_right_body();
return Ok(req.into_response(response));
}
};

match check_jwt_validity(&jwt, pool).await {
Ok(_) => {
// ON AJOUTE L'UTILISATEUR DANS LES EXTENSIONS
// Supposons que claims.sub contient l'ID
req.extensions_mut().insert(AuthenticatedUser {
id: get_user_id_from_jwt(&jwt).unwrap().parse().unwrap_or(0),
});
Comment on lines +117 to +123

let res = svc.call(req).await?;
Ok(res.map_into_left_body())
}
Err(error) => {
eprint!("error no jwt");
let response =
match error {
JWTCheckError::DatabaseError => HttpResponse::InternalServerError()
.body("Internal server error: Database not initialized."),
JWTCheckError::NoTokenProvided => HttpResponse::Unauthorized()
.body("Unauthorized: No JWT token provided."),
JWTCheckError::ExpiredToken => HttpResponse::Unauthorized()
.body("Unauthorized: JWT token is expired."),
JWTCheckError::InvalidToken => HttpResponse::Unauthorized()
.body("Unauthorized: Invalid JWT token."),
JWTCheckError::UnknownUser => {
HttpResponse::NotFound().body("User not found.")
}
};
Ok(req.into_response(response.map_into_right_body()))
}
}
})
}
}
27 changes: 27 additions & 0 deletions src/security/auth_user.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use actix_web::HttpMessage;

use actix_web::{dev::Payload, FromRequest, HttpRequest};
use futures_util::future::{ready, Ready};

#[derive(Copy, Clone)]
pub struct AuthenticatedUser {
pub id: u64,
}

impl FromRequest for AuthenticatedUser {
type Error = actix_web::Error;
type Future = Ready<Result<Self, Self::Error>>;

fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
// Comme ton Middleware a DEJA validé le token et l'a mis dans les extensions :
if let Some(user) = req.extensions().get::<AuthenticatedUser>() {
return ready(Ok(AuthenticatedUser { id: user.id }));
}

// Si on arrive ici, c'est que le middleware n'a pas fait son job
// ou que la route n'est pas protégée
ready(Err(actix_web::error::ErrorUnauthorized(
"User not authenticated",
)))
}
}
6 changes: 6 additions & 0 deletions src/security/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mod auth_middleware;
pub use auth_middleware::JwtMiddleware;
mod auth_user;
pub use auth_user::AuthenticatedUser;
mod right_middleware;
pub use right_middleware::{access_guard_middleware, AccessCheckConfig};
Loading
Loading