From f2a55919a878174780db8c20be336d691cd1299b Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Tue, 5 May 2026 11:25:53 +0800 Subject: [PATCH 01/16] fix: update mairie360_api_lib version --- Cargo.lock | 13 +- Cargo.toml | 2 +- src/auth_middleware.rs | 143 ------------------ .../v1/admin/roles/delete/endpoint.rs | 6 +- src/endpoints/v1/admin/roles/get/endpoint.rs | 2 +- .../v1/admin/roles/patch/endpoint.rs | 4 +- src/endpoints/v1/admin/roles/post/endpoint.rs | 2 +- src/endpoints/v1/admin/roles/put/endpoint.rs | 4 +- src/endpoints/v1/auth/login/endpoint.rs | 2 +- src/endpoints/v1/auth/mod.rs | 4 +- src/endpoints/v1/auth/register/endpoint.rs | 4 +- src/endpoints/v1/roles/get/endpoint.rs | 2 +- src/endpoints/v1/sessions/get/endpoint.rs | 32 ++-- src/endpoints/v1/sessions/history/endpoint.rs | 32 ++-- src/endpoints/v1/sessions/refresh/endpoint.rs | 2 +- src/endpoints/v1/sessions/revoke/endpoint.rs | 4 +- src/endpoints/v1/user/about/endpoint.rs | 42 +++-- src/lib.rs | 1 - src/main.rs | 2 +- 19 files changed, 94 insertions(+), 209 deletions(-) delete mode 100644 src/auth_middleware.rs diff --git a/Cargo.lock b/Cargo.lock index 8b2734d..76f9366 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1040,7 +1040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1588,7 +1588,7 @@ dependencies = [ "hyper", "libc", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1942,15 +1942,16 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "mairie360_api_lib" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cc9bd98ad970c3123be3ad8c53f0200361224c9a3a82fb79485c5b4777e036" +checksum = "94515afc879a7d47d82c0d28d54c85d60592c1882f62aa61d3589f74d363f748" dependencies = [ "actix-web", "anyhow", "async-trait", "axum", "deadpool-redis", + "futures-util", "jsonwebtoken", "once_cell", "redis", @@ -2754,7 +2755,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4184,7 +4185,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c4fe7b2..a32cf60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ async-trait = "0.1.89" base64 = "0.22.1" chrono = { version = "0.4", features = ["serde"] } futures-util = "0.3" -mairie360_api_lib = "0.4.1" +mairie360_api_lib = "0.5.0" rand = "0.9" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" diff --git a/src/auth_middleware.rs b/src/auth_middleware.rs deleted file mode 100644 index 2be8193..0000000 --- a/src/auth_middleware.rs +++ /dev/null @@ -1,143 +0,0 @@ -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 mairie360_api_lib::jwt_manager::{ - check_jwt_validity, get_jwt_from_request, get_user_id_from_jwt, JWTCheckError, -}; -use mairie360_api_lib::pool::AppState; - -use crate::endpoints::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 Transform for JwtMiddleware -where - S: Service, Error = Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse>; - type Error = Error; - type InitError = (); - type Transform = JwtMiddlewareService; - type Future = Ready>; - - /** - * 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 { - service: Rc, -} - -impl Service for JwtMiddlewareService -where - S: Service, Error = Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse>; - type Error = Error; - type Future = LocalBoxFuture<'static, Result>; - - 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::>(); - - // On clone le pool pour la closure async move - let pool = app_state.map(|state| state.db_pool.clone()); - - 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 => { - 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), - }); - - let res = svc.call(req).await?; - Ok(res.map_into_left_body()) - } - Err(error) => { - 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())) - } - } - }) - } -} diff --git a/src/endpoints/v1/admin/roles/delete/endpoint.rs b/src/endpoints/v1/admin/roles/delete/endpoint.rs index 1b60621..8648d39 100644 --- a/src/endpoints/v1/admin/roles/delete/endpoint.rs +++ b/src/endpoints/v1/admin/roles/delete/endpoint.rs @@ -56,14 +56,14 @@ async fn can_delete_role(id: u64, pool: PgPool) -> bool { } async fn delete_role(id: u64, state: web::Data) -> Result<(), DeleteError> { - if !does_role_exist(id, state.db_pool.clone()).await { + if !does_role_exist(id, state.db_pool.clone().unwrap()).await { return Err(DeleteError::NotFound); } - if !can_delete_role(id, state.db_pool.clone()).await { + if !can_delete_role(id, state.db_pool.clone().unwrap()).await { return Err(DeleteError::Forbidden); } let view = DeleteRoleQueryView::new(id); - let result = delete_role_query(view, state.db_pool.clone()).await; + let result = delete_role_query(view, state.db_pool.clone().unwrap()).await; result.map_err(|_| DeleteError::DatabaseError) } diff --git a/src/endpoints/v1/admin/roles/get/endpoint.rs b/src/endpoints/v1/admin/roles/get/endpoint.rs index ed82fe2..df74e59 100644 --- a/src/endpoints/v1/admin/roles/get/endpoint.rs +++ b/src/endpoints/v1/admin/roles/get/endpoint.rs @@ -33,7 +33,7 @@ impl ResponseError for GetError { async fn get_roles(state: web::Data) -> Result { let view = GetRolesQueryView {}; - let result = get_roles_query(view, state.db_pool.clone()) + let result = get_roles_query(view, state.db_pool.clone().unwrap()) .await .map_err(|e| { eprintln!("Login DB Error: {}", e); diff --git a/src/endpoints/v1/admin/roles/patch/endpoint.rs b/src/endpoints/v1/admin/roles/patch/endpoint.rs index 59b177a..d0c8647 100644 --- a/src/endpoints/v1/admin/roles/patch/endpoint.rs +++ b/src/endpoints/v1/admin/roles/patch/endpoint.rs @@ -50,7 +50,7 @@ async fn patch_role( payload: PatchView, state: web::Data, ) -> Result<(), PatchError> { - if !does_role_exist(id, state.db_pool.clone()).await { + if !does_role_exist(id, state.db_pool.clone().unwrap()).await { return Err(PatchError::NotFound); } let view = PatchRoleQueryView::new( @@ -59,7 +59,7 @@ async fn patch_role( payload.description(), payload.can_be_deleted(), ); - patch_role_query(view, state.db_pool.clone()) + patch_role_query(view, state.db_pool.clone().unwrap()) .await .map_err(|_| PatchError::DatabaseError)?; Ok(()) diff --git a/src/endpoints/v1/admin/roles/post/endpoint.rs b/src/endpoints/v1/admin/roles/post/endpoint.rs index a5a9fdd..e77f737 100644 --- a/src/endpoints/v1/admin/roles/post/endpoint.rs +++ b/src/endpoints/v1/admin/roles/post/endpoint.rs @@ -39,7 +39,7 @@ async fn create_role(payload: RoleWriteView, state: web::Data) -> Resu payload.can_be_deleted(), ); - create_role_query(view, state.db_pool.clone()) + create_role_query(view, state.db_pool.clone().unwrap()) .await .map_err(|_| PostError::DatabaseError)?; diff --git a/src/endpoints/v1/admin/roles/put/endpoint.rs b/src/endpoints/v1/admin/roles/put/endpoint.rs index 735bbf5..931ab05 100644 --- a/src/endpoints/v1/admin/roles/put/endpoint.rs +++ b/src/endpoints/v1/admin/roles/put/endpoint.rs @@ -50,7 +50,7 @@ async fn put_role( payload: RoleWriteView, state: web::Data, ) -> Result<(), PutError> { - if !does_role_exist(id, state.db_pool.clone()).await { + if !does_role_exist(id, state.db_pool.clone().unwrap()).await { return Err(PutError::NotFound); } let view = ChangeRoleQueryView::new( @@ -59,7 +59,7 @@ async fn put_role( payload.description(), payload.can_be_deleted(), ); - change_role_query(view, state.db_pool.clone()) + change_role_query(view, state.db_pool.clone().unwrap()) .await .map_err(|_| PutError::DatabaseError)?; Ok(()) diff --git a/src/endpoints/v1/auth/login/endpoint.rs b/src/endpoints/v1/auth/login/endpoint.rs index 06af24c..e24d08b 100644 --- a/src/endpoints/v1/auth/login/endpoint.rs +++ b/src/endpoints/v1/auth/login/endpoint.rs @@ -63,7 +63,7 @@ async fn login_user( ) -> Result<(String, String), LoginError> { let view = LoginUserQueryView::new(login_view.email(), login_view.password()); - let user_record = login_query(view, state.db_pool.clone()) + let user_record = login_query(view, state.db_pool.clone().unwrap()) .await .map_err(|e| { eprintln!("Login DB Error: {}", e); diff --git a/src/endpoints/v1/auth/mod.rs b/src/endpoints/v1/auth/mod.rs index 474c903..50cd355 100644 --- a/src/endpoints/v1/auth/mod.rs +++ b/src/endpoints/v1/auth/mod.rs @@ -25,7 +25,7 @@ pub async fn revoke_previous_session( device_info: &str, ) { let view = RevokePreviousSessionQueryView::new(user_id, ip_adress.clone(), device_info); - revoke_previous_session_query(view, state.db_pool.clone()) + revoke_previous_session_query(view, state.db_pool.clone().unwrap()) .await .map_err(|e| { eprintln!("Revoke Previous Session DB Error: {}", e); @@ -45,7 +45,7 @@ pub async fn create_new_session( view.get_device_info(), ) .await; - create_session_query(view, state.db_pool.clone()) + create_session_query(view, state.db_pool.clone().unwrap()) .await .map_err(|e| { eprintln!("Create Session DB Error: {}", e); diff --git a/src/endpoints/v1/auth/register/endpoint.rs b/src/endpoints/v1/auth/register/endpoint.rs index b1a538c..a4f5301 100644 --- a/src/endpoints/v1/auth/register/endpoint.rs +++ b/src/endpoints/v1/auth/register/endpoint.rs @@ -97,7 +97,7 @@ async fn register_user( register_view: &RegisterView, state: web::Data, ) -> Result<(), RegisterError> { - can_be_registered(register_view, &state.db_pool).await?; + can_be_registered(register_view, &state.db_pool.clone().unwrap()).await?; let view = RegisterUserQueryView::new( register_view.first_name(), @@ -107,7 +107,7 @@ async fn register_user( register_view.phone_number().map(|s| s), ); - let success = register_query(view, state.db_pool.clone()) + let success = register_query(view, state.db_pool.clone().unwrap()) .await .map_err(|e| { eprintln!("Database error: {}", e); diff --git a/src/endpoints/v1/roles/get/endpoint.rs b/src/endpoints/v1/roles/get/endpoint.rs index 8d7e84c..7fdf15b 100644 --- a/src/endpoints/v1/roles/get/endpoint.rs +++ b/src/endpoints/v1/roles/get/endpoint.rs @@ -33,7 +33,7 @@ impl ResponseError for GetError { async fn get_roles(state: web::Data) -> Result { let view = GetRolesQueryView {}; - let result = get_roles_query(view, state.db_pool.clone()) + let result = get_roles_query(view, state.db_pool.clone().unwrap()) .await .map_err(|e| { eprintln!("Login DB Error: {}", e); diff --git a/src/endpoints/v1/sessions/get/endpoint.rs b/src/endpoints/v1/sessions/get/endpoint.rs index de7bb19..8c7d2e1 100644 --- a/src/endpoints/v1/sessions/get/endpoint.rs +++ b/src/endpoints/v1/sessions/get/endpoint.rs @@ -41,21 +41,29 @@ impl ResponseError for GetError { // --- Cache Logic --- async fn get_cache_value(user_id: u64, state: &web::Data) -> Option { - let redis_manager = state.get_redis_conn().await; - let key = format!("sessions:{}", user_id); - - if let Ok(json_str) = handle_secure_get(redis_manager, &key).await { - // La désérialisation vers la struct valide automatiquement le format - return serde_json::from_str::(&json_str).ok(); + match state.get_redis_conn().await { + Some(redis_manager) => { + let key = format!("sessions:{}", user_id); + if let Ok(json_str) = handle_secure_get(redis_manager, &key).await { + // La désérialisation vers la struct valide automatiquement le format + serde_json::from_str::(&json_str).ok() + } else { + None + } + } + None => None, } - None } async fn set_cache_value(user_id: u64, data: &Vec, state: &web::Data) { - let redis_manager = state.get_redis_conn().await; - if let Ok(json_str) = serde_json::to_string(data) { - let key = format!("sessions:{}", user_id); - let _ = handle_secure_post(redis_manager, &key, &json_str).await; + match state.get_redis_conn().await { + Some(redis_manager) => { + if let Ok(json_str) = serde_json::to_string(data) { + let key = format!("sessions:{}", user_id); + let _ = handle_secure_post(redis_manager, &key, &json_str).await; + } + } + None => {} } } @@ -73,7 +81,7 @@ async fn get_user_info( // 2. Récupération depuis la base de données let query_result = get_active_sessions_query( GetActiveSessionsQueryView::new(user_id), - state.db_pool.clone(), + state.db_pool.clone().unwrap(), ) .await .map_err(|_| GetError::DatabaseError)?; diff --git a/src/endpoints/v1/sessions/history/endpoint.rs b/src/endpoints/v1/sessions/history/endpoint.rs index 6033b40..ed8ed48 100644 --- a/src/endpoints/v1/sessions/history/endpoint.rs +++ b/src/endpoints/v1/sessions/history/endpoint.rs @@ -41,21 +41,29 @@ impl ResponseError for HistoryError { // --- Cache Logic --- async fn get_cache_value(user_id: u64, state: &web::Data) -> Option { - let redis_manager = state.get_redis_conn().await; - let key = format!("user:{}:history", user_id); - - if let Ok(json_str) = handle_secure_get(redis_manager, &key).await { - // La désérialisation vers la struct valide automatiquement le format - return serde_json::from_str::(&json_str).ok(); + match state.get_redis_conn().await { + Some(redis_manager) => { + let key = format!("user:{}:history", user_id); + if let Ok(json_str) = handle_secure_get(redis_manager, &key).await { + // La désérialisation vers la struct valide automatiquement le format + serde_json::from_str::(&json_str).ok() + } else { + None + } + } + None => None, } - None } async fn set_cache_value(user_id: u64, data: &Vec, state: &web::Data) { - let redis_manager = state.get_redis_conn().await; - if let Ok(json_str) = serde_json::to_string(data) { - let key = format!("user:{}:history", user_id); - let _ = handle_secure_post(redis_manager, &key, &json_str).await; + match state.get_redis_conn().await { + Some(redis_manager) => { + if let Ok(json_str) = serde_json::to_string(data) { + let key = format!("user:{}:history", user_id); + let _ = handle_secure_post(redis_manager, &key, &json_str).await; + } + } + None => {} } } @@ -73,7 +81,7 @@ async fn get_user_info( // 2. Récupération depuis la base de données let query_result = get_sessions_by_user_query( GetSessionsByUserQueryView::new(user_id), - state.db_pool.clone(), + state.db_pool.clone().unwrap(), ) .await .map_err(|_| HistoryError::DatabaseError)?; diff --git a/src/endpoints/v1/sessions/refresh/endpoint.rs b/src/endpoints/v1/sessions/refresh/endpoint.rs index 25191f5..cd5aa97 100644 --- a/src/endpoints/v1/sessions/refresh/endpoint.rs +++ b/src/endpoints/v1/sessions/refresh/endpoint.rs @@ -52,7 +52,7 @@ async fn refresh_request( let db_view = IsSessionTokenValidQueryView::new(user_id, view.refresh_token(), ip_adress); - let is_valid = is_session_token_valid_query(db_view, state.db_pool.clone()).await; + let is_valid = is_session_token_valid_query(db_view, state.db_pool.clone().unwrap()).await; match is_valid { Ok(true) => generate_jwt(&user_id.to_string()).map_err(|_| RefreshError::DatabaseError), diff --git a/src/endpoints/v1/sessions/revoke/endpoint.rs b/src/endpoints/v1/sessions/revoke/endpoint.rs index cbb12fb..a464e63 100644 --- a/src/endpoints/v1/sessions/revoke/endpoint.rs +++ b/src/endpoints/v1/sessions/revoke/endpoint.rs @@ -60,7 +60,7 @@ async fn revoke_request( let db_view = IsSessionTokenValidQueryView::new(user_id, view.refresh_token(), ip_adress); - let is_valid = is_session_token_valid_query(db_view, state.db_pool.clone()).await; + let is_valid = is_session_token_valid_query(db_view, state.db_pool.clone().unwrap()).await; let db_view = match is_valid { Ok(true) => RevokeSessionByTokenQueryView::new(user_id, &view.refresh_token()), @@ -68,7 +68,7 @@ async fn revoke_request( Err(_) => return Err(RevokeError::DatabaseError), }; - match revoke_session_by_token_query(db_view, state.db_pool.clone()).await { + match revoke_session_by_token_query(db_view, state.db_pool.clone().unwrap()).await { Ok(_) => Ok(()), Err(_) => Err(RevokeError::DatabaseError), } diff --git a/src/endpoints/v1/user/about/endpoint.rs b/src/endpoints/v1/user/about/endpoint.rs index ce04fcb..0c4719a 100644 --- a/src/endpoints/v1/user/about/endpoint.rs +++ b/src/endpoints/v1/user/about/endpoint.rs @@ -48,21 +48,30 @@ impl ResponseError for AboutError { // --- Cache Logic --- async fn get_cache_value(user_id: u64, state: &web::Data) -> Option { - let redis_manager = state.get_redis_conn().await; - let key = format!("user:{}:about", user_id); - - if let Ok(json_str) = handle_secure_get(redis_manager, &key).await { - // La désérialisation vers la struct valide automatiquement le format - return serde_json::from_str::(&json_str).ok(); + match state.get_redis_conn().await { + Some(redis_manager) => { + let key = format!("user:{}:about", user_id); + + if let Ok(json_str) = handle_secure_get(redis_manager, &key).await { + // La désérialisation vers la struct valide automatiquement le format + serde_json::from_str::(&json_str).ok() + } else { + None + } + } + None => None, } - None } async fn set_cache_value(user_id: u64, data: &AboutResponseView, state: &web::Data) { - let redis_manager = state.get_redis_conn().await; - if let Ok(json_str) = serde_json::to_string(data) { - let key = format!("user:{}:about", user_id); - let _ = handle_secure_post(redis_manager, &key, &json_str).await; + match state.get_redis_conn().await { + Some(redis_manager) => { + if let Ok(json_str) = serde_json::to_string(data) { + let key = format!("user:{}:about", user_id); + let _ = handle_secure_post(redis_manager, &key, &json_str).await; + } + } + None => {} } } @@ -74,7 +83,7 @@ async fn about_request( let exists = does_user_exist_by_id_query( DoesUserExistByIdQueryView::new(user_id), - state.db_pool.clone(), + state.db_pool.clone().unwrap(), ) .await .map_err(|e| { @@ -92,9 +101,12 @@ async fn about_request( } // 2. Query Database - let query_result = about_user_query(AboutUserQueryView::new(user_id), state.db_pool.clone()) - .await - .map_err(|_| AboutError::DatabaseError)?; + let query_result = about_user_query( + AboutUserQueryView::new(user_id), + state.db_pool.clone().unwrap(), + ) + .await + .map_err(|_| AboutError::DatabaseError)?; // On transforme le résultat brut en AboutResponseView // Si about_user_query renvoie déjà une structure compatible, on l'utilise diff --git a/src/lib.rs b/src/lib.rs index 17d7806..9ba59cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,2 @@ -pub mod auth_middleware; pub mod database; pub mod endpoints; diff --git a/src/main.rs b/src/main.rs index 157efec..12c583b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ use actix_web::{middleware, web, App, HttpServer}; -use core_api::auth_middleware::JwtMiddleware; use core_api::endpoints::config; use core_api::endpoints::swagger::ApiDoc; use core_api::endpoints::{health, hello}; +use mairie360_api_lib::security::JwtMiddleware; use mairie360_api_lib::env_manager::get_critical_env_var; use mairie360_api_lib::pool::AppState; From 5c52185b3a6612e4a43ba19b2a15453aa7f5d966 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Tue, 5 May 2026 11:37:08 +0800 Subject: [PATCH 02/16] fix: remove depreciated type --- src/endpoints/v1/auth/login/endpoint.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/endpoints/v1/auth/login/endpoint.rs b/src/endpoints/v1/auth/login/endpoint.rs index e24d08b..6f85402 100644 --- a/src/endpoints/v1/auth/login/endpoint.rs +++ b/src/endpoints/v1/auth/login/endpoint.rs @@ -6,7 +6,7 @@ use actix_web::{ }; use base64::{engine::general_purpose, Engine as _}; use mairie360_api_lib::pool::AppState; -use rand::{thread_rng, RngCore}; +use rand::{rng, RngCore}; use super::login_response_view::LoginResponseView; use super::login_view::LoginView; @@ -50,7 +50,7 @@ fn generate_refresh_token() -> String { let mut buffer = [0u8; 32]; // Remplissage avec des données aléatoires sécurisées - thread_rng().fill_bytes(&mut buffer); + rng().fill_bytes(&mut buffer); // Encodage en Base64 pour avoir une String lisible general_purpose::URL_SAFE_NO_PAD.encode(buffer) From 4447ee43a0cb08c2d71451f8a5f46c0e93d8b1de Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Tue, 5 May 2026 14:18:20 +0800 Subject: [PATCH 03/16] del: remove previous auth middleware code --- src/endpoints/mod.rs | 28 +------------------ .../v1/admin/users/roles/remove/endpoint.rs | 2 +- src/endpoints/v1/sessions/get/endpoint.rs | 2 +- src/endpoints/v1/sessions/history/endpoint.rs | 2 +- src/endpoints/v1/sessions/refresh/endpoint.rs | 2 +- src/endpoints/v1/sessions/revoke/endpoint.rs | 2 +- 6 files changed, 6 insertions(+), 32 deletions(-) diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 4112927..cd960dc 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -3,36 +3,10 @@ pub mod hello; pub mod swagger; pub mod v1; -use actix_web::{web, HttpMessage}; +use actix_web::web; pub fn config(cfg: &mut web::ServiceConfig) { cfg.configure(v1::config); cfg.service(health::health); cfg.service(hello::hello); } - -use actix_web::{dev::Payload, FromRequest, HttpRequest}; -use futures_util::future::{ready, Ready}; -// Importe ici tes Claims ou ta logique de décodage - -pub struct AuthenticatedUser { - pub id: u64, -} - -impl FromRequest for AuthenticatedUser { - type Error = actix_web::Error; - type Future = Ready>; - - 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::() { - 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", - ))) - } -} diff --git a/src/endpoints/v1/admin/users/roles/remove/endpoint.rs b/src/endpoints/v1/admin/users/roles/remove/endpoint.rs index e3e0b38..221662d 100644 --- a/src/endpoints/v1/admin/users/roles/remove/endpoint.rs +++ b/src/endpoints/v1/admin/users/roles/remove/endpoint.rs @@ -1,4 +1,4 @@ -use crate::endpoints::AuthenticatedUser; +use mairie360_api_lib::security::AuthenticatedUser; use actix_web::http::StatusCode; use actix_web::{delete, web, HttpResponse, Responder, ResponseError}; diff --git a/src/endpoints/v1/sessions/get/endpoint.rs b/src/endpoints/v1/sessions/get/endpoint.rs index 8c7d2e1..cfae065 100644 --- a/src/endpoints/v1/sessions/get/endpoint.rs +++ b/src/endpoints/v1/sessions/get/endpoint.rs @@ -3,7 +3,7 @@ use crate::database::sessions::get_active_sessions::{ }; use crate::database::sessions::Session; use crate::endpoints::v1::sessions::get::response_view::GetResponseView; -use crate::endpoints::AuthenticatedUser; +use mairie360_api_lib::security::AuthenticatedUser; use actix_web::http::StatusCode; use actix_web::{get, web, HttpResponse, Responder, ResponseError}; diff --git a/src/endpoints/v1/sessions/history/endpoint.rs b/src/endpoints/v1/sessions/history/endpoint.rs index ed8ed48..7fbe61d 100644 --- a/src/endpoints/v1/sessions/history/endpoint.rs +++ b/src/endpoints/v1/sessions/history/endpoint.rs @@ -3,7 +3,7 @@ use crate::database::sessions::get_sessions_by_user::{ }; use crate::database::sessions::Session; use crate::endpoints::v1::sessions::history::response_view::HistoryResponseView; -use crate::endpoints::AuthenticatedUser; +use mairie360_api_lib::security::AuthenticatedUser; use actix_web::http::StatusCode; use actix_web::{get, web, HttpResponse, Responder, ResponseError}; diff --git a/src/endpoints/v1/sessions/refresh/endpoint.rs b/src/endpoints/v1/sessions/refresh/endpoint.rs index cd5aa97..af1c164 100644 --- a/src/endpoints/v1/sessions/refresh/endpoint.rs +++ b/src/endpoints/v1/sessions/refresh/endpoint.rs @@ -6,7 +6,7 @@ use mairie360_api_lib::jwt_manager::generate_jwt; use mairie360_api_lib::pool::AppState; use crate::endpoints::v1::sessions::refresh::request_view::RefreshRequestView; -use crate::endpoints::AuthenticatedUser; +use mairie360_api_lib::security::AuthenticatedUser; use std::net::IpAddr; #[derive(Debug, Clone, PartialEq)] diff --git a/src/endpoints/v1/sessions/revoke/endpoint.rs b/src/endpoints/v1/sessions/revoke/endpoint.rs index a464e63..b764e2d 100644 --- a/src/endpoints/v1/sessions/revoke/endpoint.rs +++ b/src/endpoints/v1/sessions/revoke/endpoint.rs @@ -4,7 +4,7 @@ use crate::database::sessions::revoke_session_by_token::{ revoke_session_by_token_query, RevokeSessionByTokenQueryView, }; use crate::endpoints::v1::sessions::revoke::request_view::RevokeRequestView; -use crate::endpoints::AuthenticatedUser; +use mairie360_api_lib::security::AuthenticatedUser; use actix_web::http::StatusCode; use actix_web::{post, web, HttpRequest, HttpResponse, Responder, ResponseError}; From 489f7f0a75855303a9d6c6e44a5271746226df38 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Wed, 6 May 2026 18:59:43 +0800 Subject: [PATCH 04/16] feat: re organise tests, add permissions and rights queries, add tests for permissions and rights --- src/database/mod.rs | 2 + .../ressources/add_access_to_user/mod.rs | 5 + .../ressources/add_access_to_user/query.rs | 19 + .../ressources/add_access_to_user/view.rs | 60 +++ src/database/ressources/can_add_access/mod.rs | 5 + .../ressources/can_add_access/query.rs | 25 + .../ressources/can_add_access/view.rs | 73 +++ .../ressources/get_ressource_type_id/mod.rs | 5 + .../ressources/get_ressource_type_id/query.rs | 16 + .../ressources/get_ressource_type_id/view.rs | 34 ++ src/database/ressources/is_owner/mod.rs | 5 + src/database/ressources/is_owner/query.rs | 14 + src/database/ressources/is_owner/view.rs | 50 ++ src/database/ressources/mod.rs | 5 + src/database/ressources/remove_access/mod.rs | 5 + .../ressources/remove_access/query.rs | 16 + src/database/ressources/remove_access/view.rs | 28 ++ src/database/rights/get_permission_id/mod.rs | 6 + .../rights/get_permission_id/query.rs | 17 + src/database/rights/get_permission_id/view.rs | 89 ++++ src/database/rights/mod.rs | 1 + src/endpoints/v1/doc.rs | 2 + src/endpoints/v1/mod.rs | 1 + .../v1/ressources/add_access/endpoint.rs | 73 +++ src/endpoints/v1/ressources/add_access/mod.rs | 2 + .../v1/ressources/add_access/view.rs | 71 +++ src/endpoints/v1/ressources/doc.rs | 14 + src/endpoints/v1/ressources/mod.rs | 14 + .../v1/ressources/remove_access/endpoint.rs | 73 +++ .../v1/ressources/remove_access/mod.rs | 2 + .../v1/ressources/remove_access/view.rs | 16 + tests/common/get_pool.rs | 10 + tests/common/mod.rs | 3 + tests/common/roles.rs | 44 ++ tests/integration_test.rs | 2 + tests/queries/auth/injection.rs | 68 +++ tests/queries/auth/login.rs | 55 +++ tests/queries/auth/mod.rs | 3 + tests/queries/auth/register.rs | 83 ++++ tests/queries/mod.rs | 6 + .../queries/ressources/add_access_to_user.rs | 81 ++++ tests/queries/ressources/can_add_access.rs | 65 +++ .../ressources/get_ressource_type_id.rs | 25 + tests/queries/ressources/is_owner.rs | 31 ++ tests/queries/ressources/mod.rs | 5 + tests/queries/ressources/remove_access.rs | 12 + tests/queries/rights/get_permission_id.rs | 64 +++ tests/queries/rights/mod.rs | 1 + tests/queries/roles/can_delete_role.rs | 36 ++ tests/queries/roles/change_role.rs | 72 +++ tests/queries/roles/create_role.rs | 39 ++ tests/queries/roles/delete_role.rs | 31 ++ tests/queries/roles/does_role_exist.rs | 32 ++ tests/queries/roles/get_roles.rs | 17 + tests/queries/roles/mod.rs | 7 + tests/queries/roles/patch_role.rs | 86 ++++ tests/queries/session/create_session.rs | 67 +++ tests/queries/session/get_session_by_token.rs | 88 ++++ tests/queries/session/get_sessions_by_user.rs | 83 ++++ tests/queries/session/mod.rs | 7 + .../session/revoke_previous_session.rs | 38 ++ tests/queries/session/revoke_session.rs | 35 ++ tests/queries/session/revoke_session_by_id.rs | 76 +++ .../session/revoke_session_by_token.rs | 67 +++ tests/queries/users/mod.rs | 0 tests/queries/users/test_users_queries.rs | 60 +++ tests/test_auth_queries.rs | 226 --------- tests/test_roles_queries.rs | 344 -------------- tests/test_session_queries.rs | 437 ------------------ tests/test_users_queries.rs | 60 --- 70 files changed, 2147 insertions(+), 1067 deletions(-) create mode 100644 src/database/ressources/add_access_to_user/mod.rs create mode 100644 src/database/ressources/add_access_to_user/query.rs create mode 100644 src/database/ressources/add_access_to_user/view.rs create mode 100644 src/database/ressources/can_add_access/mod.rs create mode 100644 src/database/ressources/can_add_access/query.rs create mode 100644 src/database/ressources/can_add_access/view.rs create mode 100644 src/database/ressources/get_ressource_type_id/mod.rs create mode 100644 src/database/ressources/get_ressource_type_id/query.rs create mode 100644 src/database/ressources/get_ressource_type_id/view.rs create mode 100644 src/database/ressources/is_owner/mod.rs create mode 100644 src/database/ressources/is_owner/query.rs create mode 100644 src/database/ressources/is_owner/view.rs create mode 100644 src/database/ressources/mod.rs create mode 100644 src/database/ressources/remove_access/mod.rs create mode 100644 src/database/ressources/remove_access/query.rs create mode 100644 src/database/ressources/remove_access/view.rs create mode 100644 src/database/rights/get_permission_id/mod.rs create mode 100644 src/database/rights/get_permission_id/query.rs create mode 100644 src/database/rights/get_permission_id/view.rs create mode 100644 src/database/rights/mod.rs create mode 100644 src/endpoints/v1/ressources/add_access/endpoint.rs create mode 100644 src/endpoints/v1/ressources/add_access/mod.rs create mode 100644 src/endpoints/v1/ressources/add_access/view.rs create mode 100644 src/endpoints/v1/ressources/doc.rs create mode 100644 src/endpoints/v1/ressources/mod.rs create mode 100644 src/endpoints/v1/ressources/remove_access/endpoint.rs create mode 100644 src/endpoints/v1/ressources/remove_access/mod.rs create mode 100644 src/endpoints/v1/ressources/remove_access/view.rs create mode 100644 tests/common/get_pool.rs create mode 100644 tests/common/mod.rs create mode 100644 tests/common/roles.rs create mode 100644 tests/integration_test.rs create mode 100644 tests/queries/auth/injection.rs create mode 100644 tests/queries/auth/login.rs create mode 100644 tests/queries/auth/mod.rs create mode 100644 tests/queries/auth/register.rs create mode 100644 tests/queries/mod.rs create mode 100644 tests/queries/ressources/add_access_to_user.rs create mode 100644 tests/queries/ressources/can_add_access.rs create mode 100644 tests/queries/ressources/get_ressource_type_id.rs create mode 100644 tests/queries/ressources/is_owner.rs create mode 100644 tests/queries/ressources/mod.rs create mode 100644 tests/queries/ressources/remove_access.rs create mode 100644 tests/queries/rights/get_permission_id.rs create mode 100644 tests/queries/rights/mod.rs create mode 100644 tests/queries/roles/can_delete_role.rs create mode 100644 tests/queries/roles/change_role.rs create mode 100644 tests/queries/roles/create_role.rs create mode 100644 tests/queries/roles/delete_role.rs create mode 100644 tests/queries/roles/does_role_exist.rs create mode 100644 tests/queries/roles/get_roles.rs create mode 100644 tests/queries/roles/mod.rs create mode 100644 tests/queries/roles/patch_role.rs create mode 100644 tests/queries/session/create_session.rs create mode 100644 tests/queries/session/get_session_by_token.rs create mode 100644 tests/queries/session/get_sessions_by_user.rs create mode 100644 tests/queries/session/mod.rs create mode 100644 tests/queries/session/revoke_previous_session.rs create mode 100644 tests/queries/session/revoke_session.rs create mode 100644 tests/queries/session/revoke_session_by_id.rs create mode 100644 tests/queries/session/revoke_session_by_token.rs create mode 100644 tests/queries/users/mod.rs create mode 100644 tests/queries/users/test_users_queries.rs delete mode 100644 tests/test_auth_queries.rs delete mode 100644 tests/test_roles_queries.rs delete mode 100644 tests/test_session_queries.rs delete mode 100644 tests/test_users_queries.rs diff --git a/src/database/mod.rs b/src/database/mod.rs index 3c43739..af9d34e 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,4 +1,6 @@ pub mod auth; +pub mod ressources; +pub mod rights; pub mod roles; pub mod sessions; pub mod users; diff --git a/src/database/ressources/add_access_to_user/mod.rs b/src/database/ressources/add_access_to_user/mod.rs new file mode 100644 index 0000000..d802173 --- /dev/null +++ b/src/database/ressources/add_access_to_user/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::add_access_to_user_query; + +mod view; +pub use view::AddAccessToUserQueryView; diff --git a/src/database/ressources/add_access_to_user/query.rs b/src/database/ressources/add_access_to_user/query.rs new file mode 100644 index 0000000..7f32e47 --- /dev/null +++ b/src/database/ressources/add_access_to_user/query.rs @@ -0,0 +1,19 @@ +use crate::database::ressources::add_access_to_user::view::AddAccessToUserQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn add_access_to_user_query( + view: AddAccessToUserQueryView, + pool: PgPool, +) -> Result<(), DatabaseError> { + sqlx::query(&view.get_request()) + .bind(view.user_id() as i64) + .bind(view.ressource_type_id() as i64) + .bind(view.ressource_instance_id() as i64) + .bind(view.access_type_id() as i64) + .fetch_one(&pool) + .await?; + + Ok(()) +} diff --git a/src/database/ressources/add_access_to_user/view.rs b/src/database/ressources/add_access_to_user/view.rs new file mode 100644 index 0000000..53d2ac5 --- /dev/null +++ b/src/database/ressources/add_access_to_user/view.rs @@ -0,0 +1,60 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use std::fmt::Display; + +pub struct AddAccessToUserQueryView { + user_id: u64, + ressource_type_id: u64, + ressource_instance_id: u64, + access_type_id: u64, +} + +impl AddAccessToUserQueryView { + pub fn new( + user_id: u64, + ressource_type_id: u64, + ressource_instance_id: u64, + access_type_id: u64, + ) -> Self { + Self { + user_id, + ressource_type_id, + ressource_instance_id, + access_type_id, + } + } + + pub fn user_id(&self) -> u64 { + self.user_id + } + + pub fn ressource_type_id(&self) -> u64 { + self.ressource_type_id + } + + pub fn ressource_instance_id(&self) -> u64 { + self.ressource_instance_id + } + + pub fn access_type_id(&self) -> u64 { + self.access_type_id + } +} + +impl DatabaseQueryView for AddAccessToUserQueryView { + fn get_request(&self) -> String { + "INSERT INTO access_control (user_id, resource_id, resource_instance_id, permission_id) VALUES ($1, $2, $3, $4)".to_string() + } +} + +impl Display for AddAccessToUserQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "AddAccessToUser: user_id = {}, ressource_type_id = {}, ressource_instance_id = {}, access_type_id = {}", + self.user_id, + self.ressource_type_id, + self.ressource_instance_id, + self.access_type_id + ) + } +} diff --git a/src/database/ressources/can_add_access/mod.rs b/src/database/ressources/can_add_access/mod.rs new file mode 100644 index 0000000..d9b5d86 --- /dev/null +++ b/src/database/ressources/can_add_access/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::can_add_access_query; + +mod view; +pub use view::CanAddAccessQueryView; diff --git a/src/database/ressources/can_add_access/query.rs b/src/database/ressources/can_add_access/query.rs new file mode 100644 index 0000000..4fda54a --- /dev/null +++ b/src/database/ressources/can_add_access/query.rs @@ -0,0 +1,25 @@ +use crate::database::ressources::can_add_access::CanAddAccessQueryView; +use crate::database::ressources::is_owner::{is_owner_query, IsOwnerQueryView}; +use crate::endpoints::v1::ressources::AccessType; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn can_add_access_query( + view: CanAddAccessQueryView, + pool: PgPool, +) -> Result { + if view.access_type() == AccessType::Error { + return Ok(false); + } + if is_owner_query( + IsOwnerQueryView::new(view.owner_id(), view.ressource_id(), view.ressource_type()), + pool, + ) + .await? + { + return Ok(true); + } + eprintln!("TODO"); + Ok(false) +} diff --git a/src/database/ressources/can_add_access/view.rs b/src/database/ressources/can_add_access/view.rs new file mode 100644 index 0000000..63d794e --- /dev/null +++ b/src/database/ressources/can_add_access/view.rs @@ -0,0 +1,73 @@ +use crate::endpoints::v1::ressources::AccessType; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use std::fmt::Display; + +pub struct CanAddAccessQueryView { + owner_id: u64, + target_id: u64, + ressource_id: u64, + ressource_type: String, + access_type: AccessType, +} + +impl CanAddAccessQueryView { + pub fn new( + owner_id: u64, + target_id: u64, + ressource_id: u64, + ressource_type: &str, + access_type: AccessType, + ) -> Self { + Self { + owner_id, + target_id, + ressource_id, + ressource_type: ressource_type.to_string(), + access_type, + } + } + + pub fn owner_id(&self) -> u64 { + self.owner_id + } + + pub fn target_id(&self) -> u64 { + self.target_id + } + + pub fn ressource_id(&self) -> u64 { + self.ressource_id + } + + pub fn ressource_type(&self) -> &str { + &self.ressource_type + } + + pub fn access_type(&self) -> AccessType { + self.access_type + } +} + +impl DatabaseQueryView for CanAddAccessQueryView { + fn get_request(&self) -> String { + format!( + "SELECT EXISTS(SELECT 1 FROM {} WHERE id = $1 AND owner_id = $2)", + self.ressource_type + ) + .to_string() + } +} + +impl Display for CanAddAccessQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "CanAddAccess: owner_id = {}, target_id = {}, ressource_id = {}, ressource_type = {}, access_type = {}", + self.owner_id, + self.target_id, + self.ressource_id, + self.ressource_type, + self.access_type.as_str() + ) + } +} diff --git a/src/database/ressources/get_ressource_type_id/mod.rs b/src/database/ressources/get_ressource_type_id/mod.rs new file mode 100644 index 0000000..438fa86 --- /dev/null +++ b/src/database/ressources/get_ressource_type_id/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::get_ressource_type_id_query; + +mod view; +pub use view::GetRessourceTypeIdQueryView; diff --git a/src/database/ressources/get_ressource_type_id/query.rs b/src/database/ressources/get_ressource_type_id/query.rs new file mode 100644 index 0000000..7dac31f --- /dev/null +++ b/src/database/ressources/get_ressource_type_id/query.rs @@ -0,0 +1,16 @@ +use crate::database::ressources::get_ressource_type_id::GetRessourceTypeIdQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn get_ressource_type_id_query( + view: GetRessourceTypeIdQueryView, + pool: PgPool, +) -> Result { + let result: i32 = sqlx::query_scalar(&view.get_request()) + .bind(view.ressource_type()) + .fetch_one(&pool) + .await?; + + Ok(result as u64) +} diff --git a/src/database/ressources/get_ressource_type_id/view.rs b/src/database/ressources/get_ressource_type_id/view.rs new file mode 100644 index 0000000..2d8ef54 --- /dev/null +++ b/src/database/ressources/get_ressource_type_id/view.rs @@ -0,0 +1,34 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use std::fmt::Display; + +pub struct GetRessourceTypeIdQueryView { + ressource_type: String, +} + +impl GetRessourceTypeIdQueryView { + pub fn new(ressource_type: &str) -> Self { + Self { + ressource_type: ressource_type.to_string(), + } + } + + pub fn ressource_type(&self) -> &str { + &self.ressource_type + } +} + +impl DatabaseQueryView for GetRessourceTypeIdQueryView { + fn get_request(&self) -> String { + "SELECT id FROM resources WHERE name = $1".to_string() + } +} + +impl Display for GetRessourceTypeIdQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "GetRessourceTypeId: ressource_type = {}", + self.ressource_type, + ) + } +} diff --git a/src/database/ressources/is_owner/mod.rs b/src/database/ressources/is_owner/mod.rs new file mode 100644 index 0000000..a0cc26c --- /dev/null +++ b/src/database/ressources/is_owner/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::is_owner_query; + +mod view; +pub use view::IsOwnerQueryView; diff --git a/src/database/ressources/is_owner/query.rs b/src/database/ressources/is_owner/query.rs new file mode 100644 index 0000000..0ed7b7a --- /dev/null +++ b/src/database/ressources/is_owner/query.rs @@ -0,0 +1,14 @@ +use crate::database::ressources::is_owner::IsOwnerQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn is_owner_query(view: IsOwnerQueryView, pool: PgPool) -> Result { + let result: bool = sqlx::query_scalar::<_, bool>(&view.get_request()) + .bind(view.ressource_id() as i64) + .bind(view.owner_id() as i64) + .fetch_one(&pool) + .await?; + + Ok(result) +} diff --git a/src/database/ressources/is_owner/view.rs b/src/database/ressources/is_owner/view.rs new file mode 100644 index 0000000..2592569 --- /dev/null +++ b/src/database/ressources/is_owner/view.rs @@ -0,0 +1,50 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use std::fmt::Display; + +pub struct IsOwnerQueryView { + ressource_type: String, + ressource_id: u64, + owner_id: u64, +} + +impl IsOwnerQueryView { + pub fn new(owner_id: u64, ressource_id: u64, ressource_type: &str) -> Self { + Self { + owner_id, + ressource_id, + ressource_type: ressource_type.to_string(), + } + } + + pub fn owner_id(&self) -> u64 { + self.owner_id + } + + pub fn ressource_id(&self) -> u64 { + self.ressource_id + } + + pub fn ressource_type(&self) -> &str { + &self.ressource_type + } +} + +impl DatabaseQueryView for IsOwnerQueryView { + fn get_request(&self) -> String { + format!( + "SELECT EXISTS(SELECT 1 FROM {} WHERE id = $1 AND owner_id = $2)", + self.ressource_type + ) + .to_string() + } +} + +impl Display for IsOwnerQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "IsOwnerQueryView: owner_id = {}, ressource_id = {}, ressource_type = {}", + self.owner_id, self.ressource_id, self.ressource_type + ) + } +} diff --git a/src/database/ressources/mod.rs b/src/database/ressources/mod.rs new file mode 100644 index 0000000..e227f5c --- /dev/null +++ b/src/database/ressources/mod.rs @@ -0,0 +1,5 @@ +pub mod add_access_to_user; +pub mod can_add_access; +pub mod get_ressource_type_id; +pub mod is_owner; +pub mod remove_access; diff --git a/src/database/ressources/remove_access/mod.rs b/src/database/ressources/remove_access/mod.rs new file mode 100644 index 0000000..b579596 --- /dev/null +++ b/src/database/ressources/remove_access/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::remove_access_query; + +mod view; +pub use view::RemoveAccessQueryView; diff --git a/src/database/ressources/remove_access/query.rs b/src/database/ressources/remove_access/query.rs new file mode 100644 index 0000000..6a16335 --- /dev/null +++ b/src/database/ressources/remove_access/query.rs @@ -0,0 +1,16 @@ +use crate::database::ressources::remove_access::view::RemoveAccessQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn remove_access_query( + view: RemoveAccessQueryView, + pool: PgPool, +) -> Result<(), DatabaseError> { + sqlx::query(&view.get_request()) + .bind(view.id() as i64) + .fetch_one(&pool) + .await?; + + Ok(()) +} diff --git a/src/database/ressources/remove_access/view.rs b/src/database/ressources/remove_access/view.rs new file mode 100644 index 0000000..65fbb9c --- /dev/null +++ b/src/database/ressources/remove_access/view.rs @@ -0,0 +1,28 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use std::fmt::Display; + +pub struct RemoveAccessQueryView { + id: u64, +} + +impl RemoveAccessQueryView { + pub fn new(id: u64) -> Self { + Self { id } + } + + pub fn id(&self) -> u64 { + self.id + } +} + +impl DatabaseQueryView for RemoveAccessQueryView { + fn get_request(&self) -> String { + "DELETE FROM access_control WHERE id = $1".to_string() + } +} + +impl Display for RemoveAccessQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "DeleteAccess: id = {}", self.id) + } +} diff --git a/src/database/rights/get_permission_id/mod.rs b/src/database/rights/get_permission_id/mod.rs new file mode 100644 index 0000000..65c3ee9 --- /dev/null +++ b/src/database/rights/get_permission_id/mod.rs @@ -0,0 +1,6 @@ +mod query; +pub use query::get_permission_id_query; +pub use view::PermissionAction; + +mod view; +pub use view::GetPermissionIdQueryView; diff --git a/src/database/rights/get_permission_id/query.rs b/src/database/rights/get_permission_id/query.rs new file mode 100644 index 0000000..195095c --- /dev/null +++ b/src/database/rights/get_permission_id/query.rs @@ -0,0 +1,17 @@ +use crate::database::rights::get_permission_id::view::GetPermissionIdQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn get_permission_id_query( + view: GetPermissionIdQueryView, + pool: PgPool, +) -> Result { + let result: i32 = sqlx::query_scalar(&view.get_request()) + .bind(view.resource_id() as i32) + .bind(view.action().to_string()) + .fetch_one(&pool) + .await?; + + Ok(result as u64) +} diff --git a/src/database/rights/get_permission_id/view.rs b/src/database/rights/get_permission_id/view.rs new file mode 100644 index 0000000..5363a09 --- /dev/null +++ b/src/database/rights/get_permission_id/view.rs @@ -0,0 +1,89 @@ +use std::fmt::Display; + +use mairie360_api_lib::database::db_interface::DatabaseQueryView; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PermissionAction { + Create, + Read, + Update, + Delete, + ReadAll, + UpdateAll, + DeleteAll, + Error, +} + +impl PermissionAction { + fn to_string(&self) -> &str { + match self { + PermissionAction::Create => "create", + PermissionAction::Read => "read", + PermissionAction::Update => "update", + PermissionAction::Delete => "delete", + PermissionAction::ReadAll => "read_all", + PermissionAction::UpdateAll => "update_all", + PermissionAction::DeleteAll => "delete_all", + PermissionAction::Error => "error", + } + } +} + +impl From for PermissionAction { + fn from(s: String) -> Self { + match s.as_str() { + "create" => PermissionAction::Create, + "read" => PermissionAction::Read, + "update" => PermissionAction::Update, + "delete" => PermissionAction::Delete, + "read_all" => PermissionAction::ReadAll, + "update_all" => PermissionAction::UpdateAll, + "delete_all" => PermissionAction::DeleteAll, + _ => PermissionAction::Error, + } + } +} + +impl Display for PermissionAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_string()) + } +} + +pub struct GetPermissionIdQueryView { + resource_id: u64, + action: PermissionAction, +} + +impl GetPermissionIdQueryView { + pub fn new(resource_id: u64, action: PermissionAction) -> Self { + Self { + resource_id, + action, + } + } + + pub fn resource_id(&self) -> u64 { + self.resource_id + } + + pub fn action(&self) -> PermissionAction { + self.action + } +} + +impl DatabaseQueryView for GetPermissionIdQueryView { + fn get_request(&self) -> String { + "SELECT id FROM permissions WHERE resource_id = $1 AND action = $2".to_string() + } +} + +impl Display for GetPermissionIdQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "GetPermissionIdQueryView: resource_id = {}, action = {}", + self.resource_id, self.action + ) + } +} diff --git a/src/database/rights/mod.rs b/src/database/rights/mod.rs new file mode 100644 index 0000000..06f4d44 --- /dev/null +++ b/src/database/rights/mod.rs @@ -0,0 +1 @@ +pub mod get_permission_id; diff --git a/src/endpoints/v1/doc.rs b/src/endpoints/v1/doc.rs index 3ad9d10..fa1d55c 100644 --- a/src/endpoints/v1/doc.rs +++ b/src/endpoints/v1/doc.rs @@ -1,5 +1,6 @@ use super::admin::doc::AdminDoc; use super::auth::doc::AuthDoc; +use super::ressources::doc::RessourcesDoc; use super::roles::doc::RolesDoc; use super::sessions::doc::SessionsDoc; use super::user::doc::UserDoc; @@ -9,6 +10,7 @@ use utoipa::OpenApi; #[openapi(nest( (path = "/admin", api = AdminDoc), (path = "/auth", api = AuthDoc), + (path = "/ressources", api = RessourcesDoc), (path = "/roles", api = RolesDoc), (path = "/sessions", api = SessionsDoc), (path = "/user", api = UserDoc), diff --git a/src/endpoints/v1/mod.rs b/src/endpoints/v1/mod.rs index d3548e6..d9991ec 100644 --- a/src/endpoints/v1/mod.rs +++ b/src/endpoints/v1/mod.rs @@ -1,6 +1,7 @@ pub mod admin; pub mod auth; pub mod doc; +pub mod ressources; pub mod roles; pub mod sessions; pub mod user; diff --git a/src/endpoints/v1/ressources/add_access/endpoint.rs b/src/endpoints/v1/ressources/add_access/endpoint.rs new file mode 100644 index 0000000..078dea1 --- /dev/null +++ b/src/endpoints/v1/ressources/add_access/endpoint.rs @@ -0,0 +1,73 @@ +use crate::endpoints::v1::ressources::add_access::view::AddAccessView; +use actix_web::http::StatusCode; +use actix_web::{post, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; + +#[derive(Debug, Clone, PartialEq)] +enum GetError { + BadRequest, + DatabaseError, + Unauthorized, +} + +impl std::fmt::Display for GetError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GetError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + GetError::BadRequest => { + write!(f, "Bad request.") + } + GetError::Unauthorized => { + write!(f, "Unauthorized.") + } + } + } +} + +impl ResponseError for GetError { + fn status_code(&self) -> StatusCode { + match self { + GetError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + GetError::BadRequest => StatusCode::BAD_REQUEST, + GetError::Unauthorized => StatusCode::UNAUTHORIZED, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn add_access_to_ressource( + state: web::Data, + view: AddAccessView, +) -> Result<(), GetError> { + Ok(()) +} + +#[utoipa::path( + post, + path = "/add_access", + responses( + (status = 200, description = "Access added successfully"), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ), + tag = "Ressources", + security( + ("jwt" = []) + ) +)] +#[post("/add_access")] +pub async fn add_access( + state: web::Data, + view: web::Json, +) -> Result { + match add_access_to_ressource(state, view.into_inner()).await { + Ok(_) => Ok(HttpResponse::Ok()), + Err(e) => Err(e), + } +} diff --git a/src/endpoints/v1/ressources/add_access/mod.rs b/src/endpoints/v1/ressources/add_access/mod.rs new file mode 100644 index 0000000..cbeb677 --- /dev/null +++ b/src/endpoints/v1/ressources/add_access/mod.rs @@ -0,0 +1,2 @@ +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/ressources/add_access/view.rs b/src/endpoints/v1/ressources/add_access/view.rs new file mode 100644 index 0000000..b12b948 --- /dev/null +++ b/src/endpoints/v1/ressources/add_access/view.rs @@ -0,0 +1,71 @@ +use utoipa::ToSchema; + +#[derive(Debug, Clone, Copy, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] +pub enum AccessType { + Delete, + Error, + Read, + Write, +} + +impl AccessType { + pub fn as_str(&self) -> &'static str { + match self { + AccessType::Read => "read", + AccessType::Write => "write", + AccessType::Delete => "delete", + AccessType::Error => "error", + } + } +} + +impl From<&str> for AccessType { + fn from(s: &str) -> Self { + match s { + "read" => AccessType::Read, + "write" => AccessType::Write, + "delete" => AccessType::Delete, + _ => AccessType::Error, + } + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct AddAccessView { + user_id: u64, + resource_id: u64, + ressource_type: String, + access_type: AccessType, +} + +impl AddAccessView { + pub fn new( + user_id: u64, + resource_id: u64, + ressource_type: &str, + access_type: AccessType, + ) -> Self { + Self { + user_id, + resource_id, + ressource_type: ressource_type.to_string(), + access_type, + } + } + + pub fn user_id(&self) -> u64 { + self.user_id + } + + pub fn resource_id(&self) -> u64 { + self.resource_id + } + + pub fn ressource_type(&self) -> &str { + &self.ressource_type + } + + pub fn access_type(&self) -> AccessType { + self.access_type + } +} diff --git a/src/endpoints/v1/ressources/doc.rs b/src/endpoints/v1/ressources/doc.rs new file mode 100644 index 0000000..670b503 --- /dev/null +++ b/src/endpoints/v1/ressources/doc.rs @@ -0,0 +1,14 @@ +use utoipa::OpenApi; + +use crate::endpoints::v1::ressources::add_access::endpoint as add_access_endpoint; +use crate::endpoints::v1::ressources::remove_access::endpoint as remove_access_endpoint; + +#[derive(OpenApi)] +#[openapi( + paths(add_access_endpoint::add_access, remove_access_endpoint::remove_access), + components(schemas( + super::add_access::view::AddAccessView, + super::remove_access::view::RemoveAccessView + )) +)] +pub struct RessourcesDoc; diff --git a/src/endpoints/v1/ressources/mod.rs b/src/endpoints/v1/ressources/mod.rs new file mode 100644 index 0000000..9f5598d --- /dev/null +++ b/src/endpoints/v1/ressources/mod.rs @@ -0,0 +1,14 @@ +mod add_access; +pub mod doc; +mod remove_access; +pub use add_access::view::AccessType; + +use actix_web::web; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/ressources") + .service(add_access::endpoint::add_access) + .service(remove_access::endpoint::remove_access), + ); +} diff --git a/src/endpoints/v1/ressources/remove_access/endpoint.rs b/src/endpoints/v1/ressources/remove_access/endpoint.rs new file mode 100644 index 0000000..621969f --- /dev/null +++ b/src/endpoints/v1/ressources/remove_access/endpoint.rs @@ -0,0 +1,73 @@ +use crate::endpoints::v1::ressources::remove_access::view::RemoveAccessView; +use actix_web::http::StatusCode; +use actix_web::{post, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; + +#[derive(Debug, Clone, PartialEq)] +enum GetError { + BadRequest, + DatabaseError, + Unauthorized, +} + +impl std::fmt::Display for GetError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GetError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + GetError::BadRequest => { + write!(f, "Bad request.") + } + GetError::Unauthorized => { + write!(f, "Unauthorized.") + } + } + } +} + +impl ResponseError for GetError { + fn status_code(&self) -> StatusCode { + match self { + GetError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + GetError::BadRequest => StatusCode::BAD_REQUEST, + GetError::Unauthorized => StatusCode::UNAUTHORIZED, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn remove_access_to_ressource( + state: web::Data, + view: RemoveAccessView, +) -> Result<(), GetError> { + Ok(()) +} + +#[utoipa::path( + post, + path = "/remove_access", + responses( + (status = 200, description = "Access removed successfully"), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ), + tag = "Ressources", + security( + ("jwt" = []) + ) +)] +#[post("/remove_access")] +pub async fn remove_access( + state: web::Data, + view: web::Json, +) -> Result { + match remove_access_to_ressource(state, view.into_inner()).await { + Ok(_) => Ok(HttpResponse::Ok()), + Err(e) => Err(e), + } +} diff --git a/src/endpoints/v1/ressources/remove_access/mod.rs b/src/endpoints/v1/ressources/remove_access/mod.rs new file mode 100644 index 0000000..cbeb677 --- /dev/null +++ b/src/endpoints/v1/ressources/remove_access/mod.rs @@ -0,0 +1,2 @@ +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/ressources/remove_access/view.rs b/src/endpoints/v1/ressources/remove_access/view.rs new file mode 100644 index 0000000..e0a88ee --- /dev/null +++ b/src/endpoints/v1/ressources/remove_access/view.rs @@ -0,0 +1,16 @@ +use utoipa::ToSchema; + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct RemoveAccessView { + access_id: u64, +} + +impl RemoveAccessView { + pub fn new(access_id: u64) -> Self { + Self { access_id } + } + + pub fn access_id(&self) -> u64 { + self.access_id + } +} diff --git a/tests/common/get_pool.rs b/tests/common/get_pool.rs new file mode 100644 index 0000000..a354112 --- /dev/null +++ b/tests/common/get_pool.rs @@ -0,0 +1,10 @@ +use sqlx::{postgres::PgPoolOptions, PgPool}; + +pub async fn get_pool(url: String) -> PgPool { + PgPoolOptions::new() + .max_connections(5) + .acquire_timeout(std::time::Duration::from_secs(3)) + .connect(&url) // On passe l'URL construite ici + .await + .expect("Failed to create Postgres pool") +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..16b22ea --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,3 @@ +mod get_pool; +pub use get_pool::get_pool; +pub mod roles; diff --git a/tests/common/roles.rs b/tests/common/roles.rs new file mode 100644 index 0000000..0d18959 --- /dev/null +++ b/tests/common/roles.rs @@ -0,0 +1,44 @@ +use crate::common::get_pool; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use sqlx::Row; +use tokio::sync::OnceCell; + +pub static COUNT: OnceCell = OnceCell::const_new(); +pub static DELETE_ID: OnceCell = OnceCell::const_new(); +pub static PATCH_ID: OnceCell = OnceCell::const_new(); +pub static PATCH_MUTEX: OnceCell> = OnceCell::const_new(); + +pub async fn setup_tests() { + if COUNT.get().is_none() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + COUNT.set(0).unwrap(); + let _ = sqlx::query( + "INSERT INTO roles (name, description, can_be_deleted) VALUES ($1, $2, true)", + ) + .bind("Delete") + .bind("Delete role") + .execute(&pool) + .await; + let delete_id = sqlx::query("SELECT id FROM roles WHERE name = 'Delete'") + .fetch_one(&pool) + .await + .unwrap() + .get::(0); + DELETE_ID.set(delete_id as u64).unwrap(); + let _ = sqlx::query( + "INSERT INTO roles (name, description, can_be_deleted) VALUES ($1, $2, true)", + ) + .bind("Patch") + .bind("Patch role") + .execute(&pool) + .await; + let patch_id = sqlx::query("SELECT id FROM roles WHERE name = 'Patch'") + .fetch_one(&pool) + .await + .unwrap() + .get::(0); + PATCH_ID.set(patch_id as u64).unwrap(); + _ = PATCH_MUTEX.set(tokio::sync::Mutex::new(())); + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..164148c --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,2 @@ +mod common; // Accès à ton pool +mod queries; diff --git a/tests/queries/auth/injection.rs b/tests/queries/auth/injection.rs new file mode 100644 index 0000000..29551f2 --- /dev/null +++ b/tests/queries/auth/injection.rs @@ -0,0 +1,68 @@ +use crate::common::get_pool; +use core_api::database::auth::login::{login_query, LoginUserQueryView}; +use core_api::database::auth::register::{register_query, RegisterUserQueryView}; +use mairie360_api_lib::database::queries::does_user_exist_by_id_query; +use mairie360_api_lib::database::query_views::DoesUserExistByIdQueryView; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; +use sqlx::PgPool; + +async fn sync_user_sequence(pool: &PgPool) -> Result<(), sqlx::Error> { + // Cette requête récupère le nom de la séquence associée à la colonne 'id' + // de la table 'users' et la met à jour avec le MAX(id) actuel. + let sync_query = r#" + SELECT setval( + pg_get_serial_sequence('users', 'id'), + COALESCE(MAX(id), 1), + max(id) IS NOT NULL + ) FROM users; + "#; + + sqlx::query(sync_query).execute(pool).await?; + + Ok(()) +} + +#[tokio::test] +#[serial] +async fn test_injection_login_email() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let malicious_email = "' OR 1=1 --"; + + let result = login_query( + LoginUserQueryView::new(malicious_email.to_string(), "any_password".to_string()), + pool, + ) + .await; + + assert_eq!(result, Ok(None)); +} + +#[tokio::test] +#[serial] +async fn test_injection_register_fields() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + sync_user_sequence(&pool).await.unwrap(); + + let malicious_name = "John'); DROP TABLE users; --"; + + let unique_email = format!("test_{}@test.com", uuid::Uuid::new_v4()); + + let result = register_query( + RegisterUserQueryView::new(malicious_name, "Doe", &unique_email, "pass", None), + pool.clone(), + ) + .await + .unwrap(); + + assert_eq!(result, true); + + let check_result = does_user_exist_by_id_query(DoesUserExistByIdQueryView::new(1), pool) + .await + .unwrap(); + + assert_eq!(check_result, true); +} diff --git a/tests/queries/auth/login.rs b/tests/queries/auth/login.rs new file mode 100644 index 0000000..8736a09 --- /dev/null +++ b/tests/queries/auth/login.rs @@ -0,0 +1,55 @@ +use crate::common::get_pool; +use core_api::database::auth::login::{login_query, LoginUserQueryResultView, LoginUserQueryView}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn test_login_user_success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let result = login_query( + LoginUserQueryView::new("alice@example.com".to_string(), "password123".to_string()), + pool, + ) + .await + .unwrap(); + + assert_eq!( + result.unwrap(), + LoginUserQueryResultView::new(1, "password123".to_string()) + ); +} + +#[tokio::test] +#[serial] +async fn test_login_user_wrong_password() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let result = login_query( + LoginUserQueryView::new("alice@example.com".to_string(), "wrong_pass".to_string()), + pool, + ) + .await; + + assert!(result.is_ok()); +} + +#[tokio::test] +#[serial] +async fn test_login_user_unknown_email() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let result = login_query( + LoginUserQueryView::new( + "stranger@danger.com".to_string(), + "any_password".to_string(), + ), + pool, + ) + .await; + + assert_eq!(result, Ok(None)); +} diff --git a/tests/queries/auth/mod.rs b/tests/queries/auth/mod.rs new file mode 100644 index 0000000..c53c43d --- /dev/null +++ b/tests/queries/auth/mod.rs @@ -0,0 +1,3 @@ +mod injection; +mod login; +mod register; diff --git a/tests/queries/auth/register.rs b/tests/queries/auth/register.rs new file mode 100644 index 0000000..ac67218 --- /dev/null +++ b/tests/queries/auth/register.rs @@ -0,0 +1,83 @@ +use crate::common::get_pool; +use core_api::database::auth::register::register_query; +use core_api::database::auth::register::RegisterUserQueryView; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; +use sqlx::PgPool; + +async fn sync_user_sequence(pool: &PgPool) -> Result<(), sqlx::Error> { + // Cette requête récupère le nom de la séquence associée à la colonne 'id' + // de la table 'users' et la met à jour avec le MAX(id) actuel. + let sync_query = r#" + SELECT setval( + pg_get_serial_sequence('users', 'id'), + COALESCE(MAX(id), 1), + max(id) IS NOT NULL + ) FROM users; + "#; + + sqlx::query(sync_query).execute(pool).await?; + + Ok(()) +} + +#[tokio::test] +#[serial] +async fn test_register_user_success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + sync_user_sequence(&pool).await.unwrap(); + + let unique_email = format!("test_{}@test.com", uuid::Uuid::new_v4()); + + let register_result = register_query( + RegisterUserQueryView::new( + "John", + "Doe", + &unique_email, + "secure_password", + Some("0601020304"), + ), + pool, + ) + .await + .unwrap(); + + assert_eq!(register_result, true); +} + +#[tokio::test] +#[serial] +async fn test_register_user_duplicate_email() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + sync_user_sequence(&pool).await.unwrap(); + + let unique_email = format!("test_{}@test.com", uuid::Uuid::new_v4()); + + let _ = register_query( + RegisterUserQueryView::new( + "John", + "Doe", + &unique_email, + "secure_password", + Some("0601020304"), + ), + pool.clone(), + ) + .await; + + let register_result = register_query( + RegisterUserQueryView::new( + "John", + "Doe", + &unique_email, + "secure_password", + Some("0601020304"), + ), + pool, + ) + .await; + + assert!(register_result.is_err()); +} diff --git a/tests/queries/mod.rs b/tests/queries/mod.rs new file mode 100644 index 0000000..3e2cdea --- /dev/null +++ b/tests/queries/mod.rs @@ -0,0 +1,6 @@ +mod auth; +mod ressources; +mod rights; +mod roles; +mod session; +// mod users; diff --git a/tests/queries/ressources/add_access_to_user.rs b/tests/queries/ressources/add_access_to_user.rs new file mode 100644 index 0000000..d516daa --- /dev/null +++ b/tests/queries/ressources/add_access_to_user.rs @@ -0,0 +1,81 @@ +use crate::common::get_pool; +use core_api::database::ressources::{ + add_access_to_user::{add_access_to_user_query, AddAccessToUserQueryView}, + get_ressource_type_id::{get_ressource_type_id_query, GetRessourceTypeIdQueryView}, +}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetRessourceTypeIdQueryView::new("groups"); + let id = get_ressource_type_id_query(view, pool.clone()) + .await + .unwrap(); + let view = AddAccessToUserQueryView::new(2, id, 1, 23); + assert!(add_access_to_user_query(view, pool).await.is_ok()); +} + +#[tokio::test] +#[serial] +async fn failure_add_all_right() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetRessourceTypeIdQueryView::new("groups"); + let id = get_ressource_type_id_query(view, pool.clone()) + .await + .unwrap(); + let view = AddAccessToUserQueryView::new(2, id, 1, 1); + assert!(add_access_to_user_query(view, pool).await.is_err()); +} + +#[tokio::test] +#[serial] +async fn failure_bad_target_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetRessourceTypeIdQueryView::new("groups"); + let id = get_ressource_type_id_query(view, pool.clone()) + .await + .unwrap(); + let view = AddAccessToUserQueryView::new(10, id, 1, 1); + assert!(add_access_to_user_query(view, pool).await.is_err()); +} + +#[tokio::test] +#[serial] +async fn failure_bad_ressource_type_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = AddAccessToUserQueryView::new(10, 100, 1, 1); + assert!(add_access_to_user_query(view, pool).await.is_err()); +} + +#[tokio::test] +#[serial] +async fn failure_bad_ressource_instance_type_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetRessourceTypeIdQueryView::new("groups"); + let id = get_ressource_type_id_query(view, pool.clone()) + .await + .unwrap(); + let view = AddAccessToUserQueryView::new(10, id, 100, 1); + assert!(add_access_to_user_query(view, pool).await.is_err()); +} + +#[tokio::test] +#[serial] +async fn failure_bad_access_type_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetRessourceTypeIdQueryView::new("groups"); + let id = get_ressource_type_id_query(view, pool.clone()) + .await + .unwrap(); + let view = AddAccessToUserQueryView::new(10, id, 1, 100); + assert!(add_access_to_user_query(view, pool).await.is_err()); +} diff --git a/tests/queries/ressources/can_add_access.rs b/tests/queries/ressources/can_add_access.rs new file mode 100644 index 0000000..1fd7497 --- /dev/null +++ b/tests/queries/ressources/can_add_access.rs @@ -0,0 +1,65 @@ +use crate::common::get_pool; +use core_api::database::ressources::can_add_access::{can_add_access_query, CanAddAccessQueryView}; +use core_api::endpoints::v1::ressources::AccessType; +use mairie360_api_lib::test_setup::queries_setup::{get_shared_db, GROUP_OWNER_ID}; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = CanAddAccessQueryView::new( + *GROUP_OWNER_ID.get().unwrap() as u64, + 2, + 1, + "groups", + AccessType::Read, + ); + assert!(can_add_access_query(view, pool).await.unwrap()); +} + +#[tokio::test] +#[serial] +async fn failure_bad_owner_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = CanAddAccessQueryView::new( + (*GROUP_OWNER_ID.get().unwrap() as u64) + 1, + 2, + 1, + "groups", + AccessType::Read, + ); + assert!(!can_add_access_query(view, pool).await.unwrap()); +} + +#[tokio::test] +#[serial] +async fn failure_bad_ressource_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = CanAddAccessQueryView::new( + *GROUP_OWNER_ID.get().unwrap() as u64, + 2, + 2, + "groups", + AccessType::Read, + ); + assert!(!can_add_access_query(view, pool).await.unwrap()); +} + +#[tokio::test] +#[serial] +async fn failure_access_type_error() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = CanAddAccessQueryView::new( + *GROUP_OWNER_ID.get().unwrap() as u64, + 1, + 1, + "groups", + AccessType::Error, + ); + assert!(!can_add_access_query(view, pool).await.unwrap()); +} diff --git a/tests/queries/ressources/get_ressource_type_id.rs b/tests/queries/ressources/get_ressource_type_id.rs new file mode 100644 index 0000000..38453b3 --- /dev/null +++ b/tests/queries/ressources/get_ressource_type_id.rs @@ -0,0 +1,25 @@ +use crate::common::get_pool; +use core_api::database::ressources::get_ressource_type_id::{ + get_ressource_type_id_query, GetRessourceTypeIdQueryView, +}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetRessourceTypeIdQueryView::new("users"); + let result = get_ressource_type_id_query(view, pool).await.unwrap(); + assert_eq!(result, 1, "{}", format!("Expected 1, got {}", result)); +} + +#[tokio::test] +#[serial] +async fn failure() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetRessourceTypeIdQueryView::new("invalid"); + assert!(get_ressource_type_id_query(view, pool).await.is_err()); +} diff --git a/tests/queries/ressources/is_owner.rs b/tests/queries/ressources/is_owner.rs new file mode 100644 index 0000000..1765e5a --- /dev/null +++ b/tests/queries/ressources/is_owner.rs @@ -0,0 +1,31 @@ +use crate::common::get_pool; +use core_api::database::ressources::is_owner::{is_owner_query, IsOwnerQueryView}; +use mairie360_api_lib::test_setup::queries_setup::{get_shared_db, GROUP_OWNER_ID}; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn true_result() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = IsOwnerQueryView::new(*GROUP_OWNER_ID.get().unwrap() as u64, 1, "groups"); + assert!(is_owner_query(view, pool).await.unwrap()); +} + +#[tokio::test] +#[serial] +async fn false_bad_ressource_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = IsOwnerQueryView::new(*GROUP_OWNER_ID.get().unwrap() as u64, 2, "groups"); + assert!(!is_owner_query(view, pool).await.unwrap()); +} + +#[tokio::test] +#[serial] +async fn false_bad_owner_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = IsOwnerQueryView::new((*GROUP_OWNER_ID.get().unwrap() as u64) + 1, 1, "groups"); + assert!(!is_owner_query(view, pool).await.unwrap()); +} diff --git a/tests/queries/ressources/mod.rs b/tests/queries/ressources/mod.rs new file mode 100644 index 0000000..e227f5c --- /dev/null +++ b/tests/queries/ressources/mod.rs @@ -0,0 +1,5 @@ +pub mod add_access_to_user; +pub mod can_add_access; +pub mod get_ressource_type_id; +pub mod is_owner; +pub mod remove_access; diff --git a/tests/queries/ressources/remove_access.rs b/tests/queries/ressources/remove_access.rs new file mode 100644 index 0000000..5d5b8d5 --- /dev/null +++ b/tests/queries/ressources/remove_access.rs @@ -0,0 +1,12 @@ +use crate::common::get_pool; +use core_api::database::ressources::remove_access::{remove_access_query, RemoveAccessQueryView}; +use mairie360_api_lib::test_setup::queries_setup::{get_shared_db, GROUP_OWNER_ID}; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn success() {} + +#[tokio::test] +#[serial] +async fn failure() {} diff --git a/tests/queries/rights/get_permission_id.rs b/tests/queries/rights/get_permission_id.rs new file mode 100644 index 0000000..8ccb696 --- /dev/null +++ b/tests/queries/rights/get_permission_id.rs @@ -0,0 +1,64 @@ +use crate::common::get_pool; +use core_api::database::{ + ressources::get_ressource_type_id::{get_ressource_type_id_query, GetRessourceTypeIdQueryView}, + rights::get_permission_id::{ + get_permission_id_query, GetPermissionIdQueryView, PermissionAction, + }, +}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn good_id_and_action() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetRessourceTypeIdQueryView::new("users"); + let view = GetPermissionIdQueryView::new( + get_ressource_type_id_query(view, pool.clone()) + .await + .unwrap(), + PermissionAction::ReadAll, + ); + let result = get_permission_id_query(view, pool).await.unwrap(); + assert_eq!(result, 1, "{}", format!("Expected 1, got {}", result)); +} + +#[tokio::test] +#[serial] +async fn fail_invalid_resource_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetPermissionIdQueryView::new(100, PermissionAction::Read); + assert!(get_permission_id_query(view, pool).await.is_err()); +} + +#[tokio::test] +#[serial] +async fn fail_invalid_action() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetRessourceTypeIdQueryView::new("users"); + let view = GetPermissionIdQueryView::new( + get_ressource_type_id_query(view, pool.clone()) + .await + .unwrap(), + PermissionAction::DeleteAll, + ); + assert!(get_permission_id_query(view, pool).await.is_err()); +} + +#[tokio::test] +#[serial] +async fn fail_error_action() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetRessourceTypeIdQueryView::new("groups"); + let view = GetPermissionIdQueryView::new( + get_ressource_type_id_query(view, pool.clone()) + .await + .unwrap(), + PermissionAction::Error, + ); + assert!(get_permission_id_query(view, pool).await.is_err()); +} diff --git a/tests/queries/rights/mod.rs b/tests/queries/rights/mod.rs new file mode 100644 index 0000000..687b733 --- /dev/null +++ b/tests/queries/rights/mod.rs @@ -0,0 +1 @@ +mod get_permission_id; diff --git a/tests/queries/roles/can_delete_role.rs b/tests/queries/roles/can_delete_role.rs new file mode 100644 index 0000000..4fa61f5 --- /dev/null +++ b/tests/queries/roles/can_delete_role.rs @@ -0,0 +1,36 @@ +use crate::common::get_pool; +use crate::common::roles::{setup_tests, DELETE_ID}; +use core_api::database::roles::can_delete_role::{can_delete_role_query, CanDeleteRoleQueryView}; +use mairie360_api_lib::database::errors::DatabaseError; +use mairie360_api_lib::database::queries::QueryError; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn test_can_delete_role_success() { + setup_tests().await; + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let result = + can_delete_role_query(CanDeleteRoleQueryView::new(*DELETE_ID.get().unwrap()), pool) + .await + .unwrap(); + + assert!(result); +} + +#[tokio::test] +#[serial] +async fn test_can_delete_role_bad_id() { + setup_tests().await; + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let result = can_delete_role_query(CanDeleteRoleQueryView::new(999), pool).await; + + assert!(result.is_err()); + let err = result.err().unwrap(); + assert_eq!(err, DatabaseError::Query(QueryError::NoResults)); +} diff --git a/tests/queries/roles/change_role.rs b/tests/queries/roles/change_role.rs new file mode 100644 index 0000000..998bed9 --- /dev/null +++ b/tests/queries/roles/change_role.rs @@ -0,0 +1,72 @@ +use crate::common::get_pool; +use crate::common::roles::setup_tests; +use core_api::database::roles::change_role::{change_role_query, ChangeRoleQueryView}; +use core_api::database::roles::create_role::{create_role_query, CreateRoleQueryView}; +use core_api::database::roles::get_roles::{get_roles_query, GetRolesQueryView}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn test_change_role_success() { + setup_tests().await; + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = CreateRoleQueryView::new("Change_test", "Change_test role", Some(true)); + let result = create_role_query(view, pool.clone()).await; + + assert!(result.is_ok()); + + let roles = get_roles_query(GetRolesQueryView {}, pool.clone()) + .await + .unwrap(); + + let mut new_role_id: i32 = 0; + for role in roles { + if role.name() == "Change_test" { + new_role_id = role.id(); + break; + } + } + + let view = ChangeRoleQueryView::new( + new_role_id as u64, + "Change_Admin", + "Change_Administrateur", + Some(true), + ); + let result = change_role_query(view, pool.clone()).await; + + assert!(result.is_ok()); + + let roles = get_roles_query(GetRolesQueryView {}, pool.clone()) + .await + .unwrap(); + + for role in roles { + if role + .description() + .is_some_and(|d| d == "Change_Administrateur".to_string()) + { + assert_eq!(role.id(), new_role_id); + assert_eq!(role.name(), "Change_Admin"); + assert_eq!(role.description().unwrap(), "Change_Administrateur"); + assert!(role.updated_at().is_some()); + assert!(role.can_be_deleted()); + } + } +} + +#[tokio::test] +#[serial] +async fn test_change_role_bad_id() { + setup_tests().await; + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = ChangeRoleQueryView::new(999, "Admin", "Administrateur", Some(false)); + let result = change_role_query(view, pool.clone()).await; + + assert!(result.is_err()); +} diff --git a/tests/queries/roles/create_role.rs b/tests/queries/roles/create_role.rs new file mode 100644 index 0000000..f2e7673 --- /dev/null +++ b/tests/queries/roles/create_role.rs @@ -0,0 +1,39 @@ +use crate::common::get_pool; +use crate::common::roles::setup_tests; +use core_api::database::roles::create_role::{create_role_query, CreateRoleQueryView}; +use core_api::database::roles::get_roles::{get_roles_query, GetRolesQueryView}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn test_create_role_success() { + setup_tests().await; + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let nb_roles = get_roles_query(GetRolesQueryView {}, pool.clone()) + .await + .unwrap() + .len(); + + let view = CreateRoleQueryView::new("Create", "Create role", Some(false)); + let result = create_role_query(view, pool.clone()).await; + + assert!(result.is_ok()); + + let roles = get_roles_query(GetRolesQueryView {}, pool.clone()) + .await + .unwrap(); + assert!(roles.len() >= nb_roles); + + for role in roles { + if role.description().is_some_and(|d| d == "Create role") { + assert!(role.id() > 2); + assert_eq!(role.name(), "Create"); + assert_eq!(role.description().unwrap(), "Create role"); + assert!(role.updated_at().is_some()); + assert!(!role.can_be_deleted()); + } + } +} diff --git a/tests/queries/roles/delete_role.rs b/tests/queries/roles/delete_role.rs new file mode 100644 index 0000000..671baa4 --- /dev/null +++ b/tests/queries/roles/delete_role.rs @@ -0,0 +1,31 @@ +use crate::common::roles::setup_tests; +use crate::common::{get_pool, roles::DELETE_ID}; +use core_api::database::roles::delete_role::{delete_role_query, DeleteRoleQueryView}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn test_delete_role() { + setup_tests().await; + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = DeleteRoleQueryView::new(*DELETE_ID.get().unwrap()); + let result = delete_role_query(view, pool.clone()).await; + + assert!(result.is_ok()); +} + +#[tokio::test] +#[serial] +async fn test_delete_role_bad_id() { + setup_tests().await; + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = DeleteRoleQueryView::new(999); + let result = delete_role_query(view, pool.clone()).await; + + assert!(result.is_ok()); +} diff --git a/tests/queries/roles/does_role_exist.rs b/tests/queries/roles/does_role_exist.rs new file mode 100644 index 0000000..6c71f8d --- /dev/null +++ b/tests/queries/roles/does_role_exist.rs @@ -0,0 +1,32 @@ +use crate::common::{get_pool, roles::setup_tests}; +use core_api::database::roles::does_role_exist::{does_role_exist_query, DoesRoleExistQueryView}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn test_does_role_exist_true() { + setup_tests().await; + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let result = does_role_exist_query(DoesRoleExistQueryView::new(1), pool) + .await + .unwrap(); + + assert!(result); +} + +#[tokio::test] +#[serial] +async fn test_does_role_exist_false() { + setup_tests().await; + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let result = does_role_exist_query(DoesRoleExistQueryView::new(999), pool) + .await + .unwrap(); + + assert!(!result); +} diff --git a/tests/queries/roles/get_roles.rs b/tests/queries/roles/get_roles.rs new file mode 100644 index 0000000..74d228c --- /dev/null +++ b/tests/queries/roles/get_roles.rs @@ -0,0 +1,17 @@ +use core_api::database::roles::get_roles::{get_roles_query, GetRolesQueryView}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +use crate::common::{get_pool, roles::setup_tests}; + +#[tokio::test] +#[serial] +async fn test_get_roles() { + setup_tests().await; + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let roles = get_roles_query(GetRolesQueryView {}, pool).await.unwrap(); + + assert!(roles.len() >= 1); +} diff --git a/tests/queries/roles/mod.rs b/tests/queries/roles/mod.rs new file mode 100644 index 0000000..6839595 --- /dev/null +++ b/tests/queries/roles/mod.rs @@ -0,0 +1,7 @@ +mod can_delete_role; +mod change_role; +mod create_role; +mod delete_role; +mod does_role_exist; +mod get_roles; +mod patch_role; diff --git a/tests/queries/roles/patch_role.rs b/tests/queries/roles/patch_role.rs new file mode 100644 index 0000000..4f5acdb --- /dev/null +++ b/tests/queries/roles/patch_role.rs @@ -0,0 +1,86 @@ +use crate::common::{ + get_pool, + roles::{PATCH_ID, PATCH_MUTEX}, +}; +use core_api::database::roles::{ + get_roles::{get_roles_query, GetRolesQueryView}, + patch_role::{patch_role_query, PatchRoleQueryView}, +}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn test_patch_role_name() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = PatchRoleQueryView::new( + *PATCH_ID.get().unwrap(), + Some("Patch".to_string()), + None, + None, + ); + let _guard = PATCH_MUTEX.get().unwrap().lock().await; + let result = patch_role_query(view, pool.clone()).await; + + let roles = get_roles_query(GetRolesQueryView {}, pool.clone()) + .await + .unwrap(); + + assert!(result.is_ok()); + for role in roles { + if role.id() == *PATCH_ID.get().unwrap() as i32 { + assert_eq!(role.name(), "Patch"); + } + } +} + +#[tokio::test] +#[serial] +async fn test_patch_role_description() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = PatchRoleQueryView::new( + *PATCH_ID.get().unwrap(), + None, + Some("Patch description".to_string()), + None, + ); + let _guard = PATCH_MUTEX.get().unwrap().lock().await; + let result = patch_role_query(view, pool.clone()).await; + + let roles = get_roles_query(GetRolesQueryView {}, pool.clone()) + .await + .unwrap(); + + assert!(result.is_ok()); + for role in roles { + if role.id() == *PATCH_ID.get().unwrap() as i32 { + assert_eq!(role.description().unwrap(), "Patch description"); + } + } +} + +#[tokio::test] +#[serial] +async fn test_patch_role_can_be_deleted_to_false() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = PatchRoleQueryView::new(*PATCH_ID.get().unwrap(), None, None, Some(Some(false))); + let _guard = PATCH_MUTEX.get().unwrap().lock().await; + let result = patch_role_query(view, pool.clone()).await; + + let roles = get_roles_query(GetRolesQueryView {}, pool.clone()) + .await + .unwrap(); + + assert!(result.is_ok()); + for role in roles { + if role.id() == *PATCH_ID.get().unwrap() as i32 { + assert_eq!(role.can_be_deleted(), false); + } + } +} diff --git a/tests/queries/session/create_session.rs b/tests/queries/session/create_session.rs new file mode 100644 index 0000000..3bdc4cf --- /dev/null +++ b/tests/queries/session/create_session.rs @@ -0,0 +1,67 @@ +use crate::common::get_pool; +use core_api::database::sessions::create_session::{create_session_query, CreateSessionQueryView}; +use mairie360_api_lib::{ + database::{ + errors::DatabaseError, queries::is_session_token_valid_query, + query_views::IsSessionTokenValidQueryView, + }, + test_setup::queries_setup::get_shared_db, +}; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn test_create_session() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + // Create a session + let result: Result<(), DatabaseError> = create_session_query( + CreateSessionQueryView::new( + 1, + "test_create_session", + "any_device", + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool.clone(), + ) + .await; + + assert!(result.is_ok()); + + let is_valid = is_session_token_valid_query( + IsSessionTokenValidQueryView::new( + 1, + "test_create_session".to_string(), + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool, + ) + .await + .unwrap(); + + assert!(is_valid); +} + +#[tokio::test] +#[serial] +async fn test_injection_create_session() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let malicious_token = "' OR 1=1 --"; + + // Create a session + let result = create_session_query( + CreateSessionQueryView::new( + 1, + malicious_token, + "any_device", + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool, + ) + .await; + + assert_eq!(result, Ok(())); +} diff --git a/tests/queries/session/get_session_by_token.rs b/tests/queries/session/get_session_by_token.rs new file mode 100644 index 0000000..2e61761 --- /dev/null +++ b/tests/queries/session/get_session_by_token.rs @@ -0,0 +1,88 @@ +use crate::common::get_pool; +use core_api::database::sessions::{ + create_session::{create_session_query, CreateSessionQueryView}, + get_session_by_token::{get_session_by_token_query, GetSessionByTokenQueryView}, +}; +use mairie360_api_lib::{ + database::{queries::is_session_token_valid_query, query_views::IsSessionTokenValidQueryView}, + test_setup::queries_setup::get_shared_db, +}; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn test_get_session_by_token() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + // Create a session + let _ = create_session_query( + CreateSessionQueryView::new( + 1, + "test_get_session_by_token", + "any_device", + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool.clone(), + ) + .await; + + let _ = is_session_token_valid_query( + IsSessionTokenValidQueryView::new( + 1, + "test_get_session_by_token".to_string(), + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool.clone(), + ) + .await + .unwrap(); + + let result = get_session_by_token_query( + GetSessionByTokenQueryView::new("test_get_session_by_token".to_string()), + pool, + ) + .await + .unwrap(); + + assert!(result.is_some()); +} + +#[tokio::test] +#[serial] +async fn test_get_session_by_unknow_token() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + // Create a session + let _ = create_session_query( + CreateSessionQueryView::new( + 1, + "test_get_session_by_unknow_token", + "any_device", + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool.clone(), + ) + .await; + + let _ = is_session_token_valid_query( + IsSessionTokenValidQueryView::new( + 1, + "test_get_session_by_unknow_token".to_string(), + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool.clone(), + ) + .await + .unwrap(); + + let result = get_session_by_token_query( + GetSessionByTokenQueryView::new("unknow_token".to_string()), + pool, + ) + .await + .unwrap(); + + assert!(result.is_none()); +} diff --git a/tests/queries/session/get_sessions_by_user.rs b/tests/queries/session/get_sessions_by_user.rs new file mode 100644 index 0000000..f581d34 --- /dev/null +++ b/tests/queries/session/get_sessions_by_user.rs @@ -0,0 +1,83 @@ +use core_api::database::sessions::{ + create_session::{create_session_query, CreateSessionQueryView}, + get_sessions_by_user::{get_sessions_by_user_query, GetSessionsByUserQueryView}, +}; +use mairie360_api_lib::{ + database::{queries::is_session_token_valid_query, query_views::IsSessionTokenValidQueryView}, + test_setup::queries_setup::get_shared_db, +}; +use serial_test::serial; + +use crate::common::get_pool; + +#[tokio::test] +#[serial] +async fn test_get_sessions_by_user() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + // Create a session + let _ = create_session_query( + CreateSessionQueryView::new( + 1, + "test_get_sessions_by_user", + "any_device", + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool.clone(), + ) + .await; + + let _ = is_session_token_valid_query( + IsSessionTokenValidQueryView::new( + 1, + "test_get_sessions_by_user".to_string(), + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool.clone(), + ) + .await + .unwrap(); + + let result = get_sessions_by_user_query(GetSessionsByUserQueryView::new(1), pool) + .await + .unwrap(); + + assert!(result.len() > 0); +} + +#[tokio::test] +#[serial] +async fn test_get_sessions_by_unknow_user() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + // Create a session + let _ = create_session_query( + CreateSessionQueryView::new( + 1, + "test_get_sessions_by_user", + "any_device", + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool.clone(), + ) + .await; + + let _ = is_session_token_valid_query( + IsSessionTokenValidQueryView::new( + 1, + "test_get_sessions_by_user".to_string(), + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool.clone(), + ) + .await + .unwrap(); + + let result = get_sessions_by_user_query(GetSessionsByUserQueryView::new(2), pool) + .await + .unwrap(); + + assert!(result.is_empty()); +} diff --git a/tests/queries/session/mod.rs b/tests/queries/session/mod.rs new file mode 100644 index 0000000..af77006 --- /dev/null +++ b/tests/queries/session/mod.rs @@ -0,0 +1,7 @@ +pub mod create_session; +pub mod get_session_by_token; +pub mod get_sessions_by_user; +pub mod revoke_previous_session; +pub mod revoke_session; +pub mod revoke_session_by_id; +pub mod revoke_session_by_token; diff --git a/tests/queries/session/revoke_previous_session.rs b/tests/queries/session/revoke_previous_session.rs new file mode 100644 index 0000000..f991c40 --- /dev/null +++ b/tests/queries/session/revoke_previous_session.rs @@ -0,0 +1,38 @@ +use crate::common::get_pool; +use core_api::database::sessions::{ + get_sessions_by_user::{get_sessions_by_user_query, GetSessionsByUserQueryView}, + revoke_previous_session::{revoke_previous_session_query, RevokePreviousSessionQueryView}, +}; +use mairie360_api_lib::{ + database::errors::DatabaseError, test_setup::queries_setup::get_shared_db, +}; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn test_revoke_previous_session() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let sessions = get_sessions_by_user_query(GetSessionsByUserQueryView::new(1), pool.clone()) + .await + .unwrap(); + + let result: Result<(), DatabaseError> = revoke_previous_session_query( + RevokePreviousSessionQueryView::new( + 1, + std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), + "", + ), + pool.clone(), + ) + .await; + + assert!(result.is_ok()); + + let sessions_2 = get_sessions_by_user_query(GetSessionsByUserQueryView::new(1), pool) + .await + .unwrap(); + + assert!(sessions_2.len() <= sessions.len()); +} diff --git a/tests/queries/session/revoke_session.rs b/tests/queries/session/revoke_session.rs new file mode 100644 index 0000000..827044a --- /dev/null +++ b/tests/queries/session/revoke_session.rs @@ -0,0 +1,35 @@ +use crate::common::get_pool; +use core_api::database::sessions::{ + get_sessions_by_user::{get_sessions_by_user_query, GetSessionsByUserQueryView}, + revoke_session::{revoke_session_query, RevokeSessionQueryView}, +}; +use mairie360_api_lib::{ + database::errors::DatabaseError, test_setup::queries_setup::get_shared_db, +}; +use serial_test::serial; +use uuid::Uuid; + +#[tokio::test] +#[serial] +async fn test_revoke_unknowed_session_with_token_and_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let sessions = get_sessions_by_user_query(GetSessionsByUserQueryView::new(1), pool.clone()) + .await + .unwrap(); + + let result: Result<(), DatabaseError> = revoke_session_query( + RevokeSessionQueryView::new(1, Uuid::new_v4(), "a"), + pool.clone(), + ) + .await; + + assert!(result.is_ok()); + + let sessions_2 = get_sessions_by_user_query(GetSessionsByUserQueryView::new(1), pool) + .await + .unwrap(); + + assert!(sessions_2.len() >= sessions.len()); +} diff --git a/tests/queries/session/revoke_session_by_id.rs b/tests/queries/session/revoke_session_by_id.rs new file mode 100644 index 0000000..5e91c0e --- /dev/null +++ b/tests/queries/session/revoke_session_by_id.rs @@ -0,0 +1,76 @@ +use core_api::database::sessions::{ + create_session::{create_session_query, CreateSessionQueryView}, + get_session_by_token::{get_session_by_token_query, GetSessionByTokenQueryView}, + revoke_session_by_id::{revoke_session_by_id_query, RevokeSessionByIdQueryView}, +}; +use mairie360_api_lib::{ + database::{ + errors::DatabaseError, queries::is_session_token_valid_query, + query_views::IsSessionTokenValidQueryView, + }, + test_setup::queries_setup::get_shared_db, +}; +use serial_test::serial; + +use crate::common::get_pool; + +#[tokio::test] +#[serial] +async fn test_revoke_session_with_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + // Create a session + let _ = create_session_query( + CreateSessionQueryView::new( + 1, + "test_revoke_session_with_id", + "any_device", + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool.clone(), + ) + .await; + + let session = get_session_by_token_query( + GetSessionByTokenQueryView::new("test_revoke_session_with_id".to_string()), + pool.clone(), + ) + .await + .unwrap() + .unwrap(); + + let is_valid = is_session_token_valid_query( + IsSessionTokenValidQueryView::new( + 1, + "test_revoke_session_with_id".to_string(), + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool.clone(), + ) + .await + .unwrap(); + + assert!(is_valid); + + let session_id = session.id().clone(); + + let result: Result<(), DatabaseError> = + revoke_session_by_id_query(RevokeSessionByIdQueryView::new(1, session_id), pool.clone()) + .await; + + assert!(result.is_ok()); + + let is_valid = is_session_token_valid_query( + IsSessionTokenValidQueryView::new( + 1, + "test_revoke_session_with_id".to_string(), + std::net::IpAddr::from([0, 0, 0, 0]), + ), + pool.clone(), + ) + .await + .unwrap(); + + assert!(!is_valid); +} diff --git a/tests/queries/session/revoke_session_by_token.rs b/tests/queries/session/revoke_session_by_token.rs new file mode 100644 index 0000000..14ae8f1 --- /dev/null +++ b/tests/queries/session/revoke_session_by_token.rs @@ -0,0 +1,67 @@ +use core_api::database::sessions::{ + create_session::{create_session_query, CreateSessionQueryView}, + revoke_session_by_token::{revoke_session_by_token_query, RevokeSessionByTokenQueryView}, +}; +use mairie360_api_lib::{ + database::{ + errors::DatabaseError, queries::is_session_token_valid_query, + query_views::IsSessionTokenValidQueryView, + }, + test_setup::queries_setup::get_shared_db, +}; +use serial_test::serial; + +use crate::common::get_pool; + +#[tokio::test] +#[serial] +async fn test_revoke_session_with_token() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + // Create a session + let _ = create_session_query( + CreateSessionQueryView::new( + 1, + "test_revoke_session_with_token", + "any_device", + std::net::IpAddr::from([0, 0, 0, 1]), + ), + pool.clone(), + ) + .await; + + let is_valid = is_session_token_valid_query( + IsSessionTokenValidQueryView::new( + 1, + "test_revoke_session_with_token".to_string(), + std::net::IpAddr::from([0, 0, 0, 1]), + ), + pool.clone(), + ) + .await + .unwrap(); + + assert!(is_valid); + + let result: Result<(), DatabaseError> = revoke_session_by_token_query( + RevokeSessionByTokenQueryView::new(1, "test_revoke_session_with_token"), + pool.clone(), + ) + .await; + + assert!(result.is_ok()); + + let is_valid = is_session_token_valid_query( + IsSessionTokenValidQueryView::new( + 1, + "test_revoke_session_with_token".to_string(), + std::net::IpAddr::from([0, 0, 0, 1]), + ), + pool.clone(), + ) + .await + .unwrap(); + + assert!(!is_valid); +} diff --git a/tests/queries/users/mod.rs b/tests/queries/users/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/tests/queries/users/test_users_queries.rs b/tests/queries/users/test_users_queries.rs new file mode 100644 index 0000000..64e1c59 --- /dev/null +++ b/tests/queries/users/test_users_queries.rs @@ -0,0 +1,60 @@ +// use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +// use serial_test::serial; +// use sqlx::postgres::PgPoolOptions; +// use sqlx::PgPool; + +// async fn get_pool(url: String) -> PgPool { +// PgPoolOptions::new() +// .max_connections(5) +// .acquire_timeout(std::time::Duration::from_secs(3)) +// .connect(&url) // On passe l'URL construite ici +// .await +// .expect("Failed to create Postgres pool") +// } + +// #[cfg(test)] +// mod queries_tests { +// use core_api::database::users::about::{about_user_query, AboutUserQueryView}; +// use mairie360_api_lib::database::errors::DatabaseError; +// use mairie360_api_lib::database::queries::QueryError; +// use serde_json::json; + +// use super::*; + +// #[tokio::test] +// #[serial] +// async fn test_about_user_success() { +// let (_container, host) = get_shared_db().await; +// let pool = get_pool(host.to_string()).await; + +// let result = about_user_query(AboutUserQueryView::new(1), pool) +// .await +// .unwrap(); + +// let expected_json = json!({ +// "first_name": "Alice", +// "last_name": "Smith", +// "email": "alice@example.com", +// "phone_number": "0102030405", +// "status": "active" +// }); + +// assert_eq!(result.json(), expected_json); +// } + +// #[tokio::test] +// #[serial] +// async fn test_about_user_fail() { +// let (_container, host) = get_shared_db().await; +// let pool = get_pool(host.to_string()).await; + +// let result = about_user_query(AboutUserQueryView::new(999), pool).await; + +// assert!(result.is_err()); +// let err = result.err().unwrap(); +// assert_eq!( +// err, +// DatabaseError::Query(QueryError::InvalidId("User ID not found".to_string())) +// ); +// } +// } diff --git a/tests/test_auth_queries.rs b/tests/test_auth_queries.rs deleted file mode 100644 index 126ab61..0000000 --- a/tests/test_auth_queries.rs +++ /dev/null @@ -1,226 +0,0 @@ -use mairie360_api_lib::test_setup::queries_setup::get_shared_db; -use serial_test::serial; -use sqlx::postgres::PgPoolOptions; -use sqlx::PgPool; - -async fn sync_user_sequence(pool: &PgPool) -> Result<(), sqlx::Error> { - // Cette requête récupère le nom de la séquence associée à la colonne 'id' - // de la table 'users' et la met à jour avec le MAX(id) actuel. - let sync_query = r#" - SELECT setval( - pg_get_serial_sequence('users', 'id'), - COALESCE(MAX(id), 1), - max(id) IS NOT NULL - ) FROM users; - "#; - - sqlx::query(sync_query).execute(pool).await?; - - Ok(()) -} - -async fn get_pool(url: String) -> PgPool { - PgPoolOptions::new() - .max_connections(5) - .acquire_timeout(std::time::Duration::from_secs(3)) - .connect(&url) // On passe l'URL construite ici - .await - .expect("Failed to create Postgres pool") -} - -#[cfg(test)] -mod queries_tests { - use super::*; - - #[cfg(test)] - mod login { - use super::*; - - use core_api::database::auth::login::{ - login_query, LoginUserQueryResultView, LoginUserQueryView, - }; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_login_user_success() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - let result = login_query( - LoginUserQueryView::new("alice@example.com".to_string(), "password123".to_string()), - pool, - ) - .await - .unwrap(); - - assert_eq!( - result.unwrap(), - LoginUserQueryResultView::new(1, "password123".to_string()) - ); - } - - #[tokio::test] - #[serial] - async fn test_login_user_wrong_password() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let result = login_query( - LoginUserQueryView::new("alice@example.com".to_string(), "wrong_pass".to_string()), - pool, - ) - .await; - - assert!(result.is_ok()); - } - - #[tokio::test] - #[serial] - async fn test_login_user_unknown_email() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let result = login_query( - LoginUserQueryView::new( - "stranger@danger.com".to_string(), - "any_password".to_string(), - ), - pool, - ) - .await; - - assert_eq!(result, Ok(None)); - } - } - - #[cfg(test)] - mod register { - use super::*; - use core_api::database::auth::register::register_query; - use core_api::database::auth::register::RegisterUserQueryView; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_register_user_success() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - sync_user_sequence(&pool).await.unwrap(); - - let unique_email = format!("test_{}@test.com", uuid::Uuid::new_v4()); - - let register_result = register_query( - RegisterUserQueryView::new( - "John", - "Doe", - &unique_email, - "secure_password", - Some("0601020304"), - ), - pool, - ) - .await - .unwrap(); - - assert_eq!(register_result, true); - } - - #[tokio::test] - #[serial] - async fn test_register_user_duplicate_email() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - sync_user_sequence(&pool).await.unwrap(); - - let unique_email = format!("test_{}@test.com", uuid::Uuid::new_v4()); - - let _ = register_query( - RegisterUserQueryView::new( - "John", - "Doe", - &unique_email, - "secure_password", - Some("0601020304"), - ), - pool.clone(), - ) - .await; - - let register_result = register_query( - RegisterUserQueryView::new( - "John", - "Doe", - &unique_email, - "secure_password", - Some("0601020304"), - ), - pool, - ) - .await; - - assert!(register_result.is_err()); - } - } -} - -#[cfg(test)] -mod sql_injection_tests { - use super::*; - use core_api::database::auth::login::{login_query, LoginUserQueryView}; - use core_api::database::auth::register::{register_query, RegisterUserQueryView}; - use mairie360_api_lib::database::queries::does_user_exist_by_id_query; - use mairie360_api_lib::database::query_views::DoesUserExistByIdQueryView; - - async fn get_pool(url: String) -> PgPool { - PgPoolOptions::new() - .max_connections(5) - .acquire_timeout(std::time::Duration::from_secs(3)) - .connect(&url) - .await - .expect("Failed to create Postgres pool") - } - - #[tokio::test] - #[serial] - async fn test_injection_login_email() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let malicious_email = "' OR 1=1 --"; - - let result = login_query( - LoginUserQueryView::new(malicious_email.to_string(), "any_password".to_string()), - pool, - ) - .await; - - assert_eq!(result, Ok(None)); - } - - #[tokio::test] - #[serial] - async fn test_injection_register_fields() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - sync_user_sequence(&pool).await.unwrap(); - - let malicious_name = "John'); DROP TABLE users; --"; - - let unique_email = format!("test_{}@test.com", uuid::Uuid::new_v4()); - - let result = register_query( - RegisterUserQueryView::new(malicious_name, "Doe", &unique_email, "pass", None), - pool.clone(), - ) - .await - .unwrap(); - - assert_eq!(result, true); - - let check_result = does_user_exist_by_id_query(DoesUserExistByIdQueryView::new(1), pool) - .await - .unwrap(); - - assert_eq!(check_result, true); - } -} diff --git a/tests/test_roles_queries.rs b/tests/test_roles_queries.rs deleted file mode 100644 index 65bf655..0000000 --- a/tests/test_roles_queries.rs +++ /dev/null @@ -1,344 +0,0 @@ -use mairie360_api_lib::test_setup::queries_setup::get_shared_db; -use serial_test::serial; -use sqlx::postgres::PgPoolOptions; -use sqlx::{PgPool, Row}; -use tokio::sync::OnceCell; - -static COUNT: OnceCell = OnceCell::const_new(); -static DELETE_ID: OnceCell = OnceCell::const_new(); -static PATCH_ID: OnceCell = OnceCell::const_new(); - -static PATCH_MUTEX: OnceCell> = OnceCell::const_new(); - -async fn setup_tests() { - if COUNT.get().is_none() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - COUNT.set(0).unwrap(); - let _ = sqlx::query( - "INSERT INTO roles (name, description, can_be_deleted) VALUES ($1, $2, true)", - ) - .bind("Delete") - .bind("Delete role") - .execute(&pool) - .await; - let delete_id = sqlx::query("SELECT id FROM roles WHERE name = 'Delete'") - .fetch_one(&pool) - .await - .unwrap() - .get::(0); - DELETE_ID.set(delete_id as u64).unwrap(); - let _ = sqlx::query( - "INSERT INTO roles (name, description, can_be_deleted) VALUES ($1, $2, true)", - ) - .bind("Patch") - .bind("Patch role") - .execute(&pool) - .await; - let patch_id = sqlx::query("SELECT id FROM roles WHERE name = 'Patch'") - .fetch_one(&pool) - .await - .unwrap() - .get::(0); - PATCH_ID.set(patch_id as u64).unwrap(); - _ = PATCH_MUTEX.set(tokio::sync::Mutex::new(())); - } -} - -async fn get_pool(url: String) -> PgPool { - PgPoolOptions::new() - .max_connections(5) - .acquire_timeout(std::time::Duration::from_secs(3)) - .connect(&url) // On passe l'URL construite ici - .await - .expect("Failed to create Postgres pool") -} - -#[cfg(test)] -mod queries_tests { - use core_api::database::roles::can_delete_role::{ - can_delete_role_query, CanDeleteRoleQueryView, - }; - use core_api::database::roles::change_role::{change_role_query, ChangeRoleQueryView}; - use core_api::database::roles::create_role::{create_role_query, CreateRoleQueryView}; - use core_api::database::roles::delete_role::{delete_role_query, DeleteRoleQueryView}; - use core_api::database::roles::does_role_exist::{ - does_role_exist_query, DoesRoleExistQueryView, - }; - use core_api::database::roles::get_roles::{get_roles_query, GetRolesQueryView}; - use core_api::database::roles::patch_role::{patch_role_query, PatchRoleQueryView}; - use mairie360_api_lib::database::errors::DatabaseError; - use mairie360_api_lib::database::queries::QueryError; - - use super::*; - - #[tokio::test] - #[serial] - async fn test_does_role_exist_true() { - setup_tests().await; - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let result = does_role_exist_query(DoesRoleExistQueryView::new(1), pool) - .await - .unwrap(); - - assert!(result); - } - - #[tokio::test] - #[serial] - async fn test_does_role_exist_false() { - setup_tests().await; - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let result = does_role_exist_query(DoesRoleExistQueryView::new(999), pool) - .await - .unwrap(); - - assert!(!result); - } - - #[tokio::test] - #[serial] - async fn test_can_delete_role_success() { - setup_tests().await; - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let result = - can_delete_role_query(CanDeleteRoleQueryView::new(*DELETE_ID.get().unwrap()), pool) - .await - .unwrap(); - - assert!(result); - } - - #[tokio::test] - #[serial] - async fn test_can_delete_role_bad_id() { - setup_tests().await; - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let result = can_delete_role_query(CanDeleteRoleQueryView::new(999), pool).await; - - assert!(result.is_err()); - let err = result.err().unwrap(); - assert_eq!(err, DatabaseError::Query(QueryError::NoResults)); - } - - //test get roles - #[tokio::test] - #[serial] - async fn test_get_roles() { - setup_tests().await; - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let roles = get_roles_query(GetRolesQueryView {}, pool).await.unwrap(); - - assert!(roles.len() >= 1); - } - - #[tokio::test] - #[serial] - async fn test_create_role_success() { - setup_tests().await; - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let nb_roles = get_roles_query(GetRolesQueryView {}, pool.clone()) - .await - .unwrap() - .len(); - - let view = CreateRoleQueryView::new("Create", "Create role", Some(false)); - let result = create_role_query(view, pool.clone()).await; - - assert!(result.is_ok()); - - let roles = get_roles_query(GetRolesQueryView {}, pool.clone()) - .await - .unwrap(); - assert!(roles.len() >= nb_roles); - - for role in roles { - if role.description().is_some_and(|d| d == "Create role") { - assert!(role.id() > 2); - assert_eq!(role.name(), "Create"); - assert_eq!(role.description().unwrap(), "Create role"); - assert!(role.updated_at().is_some()); - assert!(!role.can_be_deleted()); - } - } - } - - #[tokio::test] - #[serial] - async fn test_change_role_success() { - setup_tests().await; - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let view = CreateRoleQueryView::new("Change_test", "Change_test role", Some(true)); - let result = create_role_query(view, pool.clone()).await; - - assert!(result.is_ok()); - - let roles = get_roles_query(GetRolesQueryView {}, pool.clone()) - .await - .unwrap(); - - let mut new_role_id: i32 = 0; - for role in roles { - if role.name() == "Change_test" { - new_role_id = role.id(); - break; - } - } - - let view = ChangeRoleQueryView::new( - new_role_id as u64, - "Change_Admin", - "Change_Administrateur", - Some(true), - ); - let result = change_role_query(view, pool.clone()).await; - - assert!(result.is_ok()); - - let roles = get_roles_query(GetRolesQueryView {}, pool.clone()) - .await - .unwrap(); - - for role in roles { - if role - .description() - .is_some_and(|d| d == "Change_Administrateur".to_string()) - { - assert_eq!(role.id(), new_role_id); - assert_eq!(role.name(), "Change_Admin"); - assert_eq!(role.description().unwrap(), "Change_Administrateur"); - assert!(role.updated_at().is_some()); - assert!(role.can_be_deleted()); - } - } - } - - #[tokio::test] - #[serial] - async fn test_change_role_bad_id() { - setup_tests().await; - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let view = ChangeRoleQueryView::new(999, "Admin", "Administrateur", Some(false)); - let result = change_role_query(view, pool.clone()).await; - - assert!(result.is_err()); - } - - #[tokio::test] - #[serial] - async fn test_patch_role_name() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let view = PatchRoleQueryView::new( - *PATCH_ID.get().unwrap(), - Some("Patch".to_string()), - None, - None, - ); - let _guard = PATCH_MUTEX.get().unwrap().lock().await; - let result = patch_role_query(view, pool.clone()).await; - - let roles = get_roles_query(GetRolesQueryView {}, pool.clone()) - .await - .unwrap(); - - assert!(result.is_ok()); - for role in roles { - if role.id() == *PATCH_ID.get().unwrap() as i32 { - assert_eq!(role.name(), "Patch"); - } - } - } - - #[tokio::test] - #[serial] - async fn test_patch_role_description() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let view = PatchRoleQueryView::new( - *PATCH_ID.get().unwrap(), - None, - Some("Patch description".to_string()), - None, - ); - let _guard = PATCH_MUTEX.get().unwrap().lock().await; - let result = patch_role_query(view, pool.clone()).await; - - let roles = get_roles_query(GetRolesQueryView {}, pool.clone()) - .await - .unwrap(); - - assert!(result.is_ok()); - for role in roles { - if role.id() == *PATCH_ID.get().unwrap() as i32 { - assert_eq!(role.description().unwrap(), "Patch description"); - } - } - } - - #[tokio::test] - #[serial] - async fn test_patch_role_can_be_deleted_to_false() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let view = PatchRoleQueryView::new(*PATCH_ID.get().unwrap(), None, None, Some(Some(false))); - let _guard = PATCH_MUTEX.get().unwrap().lock().await; - let result = patch_role_query(view, pool.clone()).await; - - let roles = get_roles_query(GetRolesQueryView {}, pool.clone()) - .await - .unwrap(); - - assert!(result.is_ok()); - for role in roles { - if role.id() == *PATCH_ID.get().unwrap() as i32 { - assert_eq!(role.can_be_deleted(), false); - } - } - } - - #[tokio::test] - #[serial] - async fn test_delete_role() { - setup_tests().await; - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let view = DeleteRoleQueryView::new(*DELETE_ID.get().unwrap()); - let result = delete_role_query(view, pool.clone()).await; - - assert!(result.is_ok()); - } - - #[tokio::test] - #[serial] - async fn test_delete_role_bad_id() { - setup_tests().await; - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let view = DeleteRoleQueryView::new(999); - let result = delete_role_query(view, pool.clone()).await; - - assert!(result.is_ok()); - } -} diff --git a/tests/test_session_queries.rs b/tests/test_session_queries.rs deleted file mode 100644 index 88e80e2..0000000 --- a/tests/test_session_queries.rs +++ /dev/null @@ -1,437 +0,0 @@ -use mairie360_api_lib::test_setup::queries_setup::get_shared_db; -use serial_test::serial; -use sqlx::postgres::PgPoolOptions; -use sqlx::PgPool; - -async fn get_pool(url: String) -> PgPool { - PgPoolOptions::new() - .max_connections(5) - .acquire_timeout(std::time::Duration::from_secs(3)) - .connect(&url) // On passe l'URL construite ici - .await - .expect("Failed to create Postgres pool") -} - -#[cfg(test)] -mod queries_tests { - use super::*; - use core_api::database::sessions::create_session::{ - create_session_query, CreateSessionQueryView, - }; - use core_api::database::sessions::get_session_by_token::{ - get_session_by_token_query, GetSessionByTokenQueryView, - }; - use core_api::database::sessions::get_sessions_by_user::{ - get_sessions_by_user_query, GetSessionsByUserQueryView, - }; - use core_api::database::sessions::revoke_previous_session::{ - revoke_previous_session_query, RevokePreviousSessionQueryView, - }; - use core_api::database::sessions::revoke_session::{ - revoke_session_query, RevokeSessionQueryView, - }; - use core_api::database::sessions::revoke_session_by_id::{ - revoke_session_by_id_query, RevokeSessionByIdQueryView, - }; - use core_api::database::sessions::revoke_session_by_token::{ - revoke_session_by_token_query, RevokeSessionByTokenQueryView, - }; - use mairie360_api_lib::database::errors::DatabaseError; - use mairie360_api_lib::database::queries::is_session_token_valid_query; - use mairie360_api_lib::database::query_views::IsSessionTokenValidQueryView; - use uuid::Uuid; - - #[tokio::test] - #[serial] - async fn test_create_session() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - // Create a session - let result: Result<(), DatabaseError> = create_session_query( - CreateSessionQueryView::new( - 1, - "test_create_session", - "any_device", - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool.clone(), - ) - .await; - - assert!(result.is_ok()); - - let is_valid = is_session_token_valid_query( - IsSessionTokenValidQueryView::new( - 1, - "test_create_session".to_string(), - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool, - ) - .await - .unwrap(); - - assert!(is_valid); - } - - #[tokio::test] - #[serial] - async fn test_get_sessions_by_user() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - // Create a session - let _ = create_session_query( - CreateSessionQueryView::new( - 1, - "test_get_sessions_by_user", - "any_device", - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool.clone(), - ) - .await; - - let _ = is_session_token_valid_query( - IsSessionTokenValidQueryView::new( - 1, - "test_get_sessions_by_user".to_string(), - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool.clone(), - ) - .await - .unwrap(); - - let result = get_sessions_by_user_query(GetSessionsByUserQueryView::new(1), pool) - .await - .unwrap(); - - assert!(result.len() > 0); - } - - #[tokio::test] - #[serial] - async fn test_get_sessions_by_unknow_user() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - // Create a session - let _ = create_session_query( - CreateSessionQueryView::new( - 1, - "test_get_sessions_by_user", - "any_device", - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool.clone(), - ) - .await; - - let _ = is_session_token_valid_query( - IsSessionTokenValidQueryView::new( - 1, - "test_get_sessions_by_user".to_string(), - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool.clone(), - ) - .await - .unwrap(); - - let result = get_sessions_by_user_query(GetSessionsByUserQueryView::new(2), pool) - .await - .unwrap(); - - assert!(result.is_empty()); - } - - #[tokio::test] - #[serial] - async fn test_get_session_by_token() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - // Create a session - let _ = create_session_query( - CreateSessionQueryView::new( - 1, - "test_get_session_by_token", - "any_device", - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool.clone(), - ) - .await; - - let _ = is_session_token_valid_query( - IsSessionTokenValidQueryView::new( - 1, - "test_get_session_by_token".to_string(), - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool.clone(), - ) - .await - .unwrap(); - - let result = get_session_by_token_query( - GetSessionByTokenQueryView::new("test_get_session_by_token".to_string()), - pool, - ) - .await - .unwrap(); - - assert!(result.is_some()); - } - - #[tokio::test] - #[serial] - async fn test_get_session_by_unknow_token() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - // Create a session - let _ = create_session_query( - CreateSessionQueryView::new( - 1, - "test_get_session_by_unknow_token", - "any_device", - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool.clone(), - ) - .await; - - let _ = is_session_token_valid_query( - IsSessionTokenValidQueryView::new( - 1, - "test_get_session_by_unknow_token".to_string(), - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool.clone(), - ) - .await - .unwrap(); - - let result = get_session_by_token_query( - GetSessionByTokenQueryView::new("unknow_token".to_string()), - pool, - ) - .await - .unwrap(); - - assert!(result.is_none()); - } - - #[tokio::test] - #[serial] - async fn test_revoke_session_with_id() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - // Create a session - let _ = create_session_query( - CreateSessionQueryView::new( - 1, - "test_revoke_session_with_id", - "any_device", - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool.clone(), - ) - .await; - - let session = get_session_by_token_query( - GetSessionByTokenQueryView::new("test_revoke_session_with_id".to_string()), - pool.clone(), - ) - .await - .unwrap() - .unwrap(); - - let is_valid = is_session_token_valid_query( - IsSessionTokenValidQueryView::new( - 1, - "test_revoke_session_with_id".to_string(), - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool.clone(), - ) - .await - .unwrap(); - - assert!(is_valid); - - let session_id = session.id().clone(); - - let result: Result<(), DatabaseError> = revoke_session_by_id_query( - RevokeSessionByIdQueryView::new(1, session_id), - pool.clone(), - ) - .await; - - assert!(result.is_ok()); - - let is_valid = is_session_token_valid_query( - IsSessionTokenValidQueryView::new( - 1, - "test_revoke_session_with_id".to_string(), - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool.clone(), - ) - .await - .unwrap(); - - assert!(!is_valid); - } - - #[tokio::test] - #[serial] - async fn test_revoke_session_with_token() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - // Create a session - let _ = create_session_query( - CreateSessionQueryView::new( - 1, - "test_revoke_session_with_token", - "any_device", - std::net::IpAddr::from([0, 0, 0, 1]), - ), - pool.clone(), - ) - .await; - - let is_valid = is_session_token_valid_query( - IsSessionTokenValidQueryView::new( - 1, - "test_revoke_session_with_token".to_string(), - std::net::IpAddr::from([0, 0, 0, 1]), - ), - pool.clone(), - ) - .await - .unwrap(); - - assert!(is_valid); - - let result: Result<(), DatabaseError> = revoke_session_by_token_query( - RevokeSessionByTokenQueryView::new(1, "test_revoke_session_with_token"), - pool.clone(), - ) - .await; - - assert!(result.is_ok()); - - let is_valid = is_session_token_valid_query( - IsSessionTokenValidQueryView::new( - 1, - "test_revoke_session_with_token".to_string(), - std::net::IpAddr::from([0, 0, 0, 1]), - ), - pool.clone(), - ) - .await - .unwrap(); - - assert!(!is_valid); - } - - #[tokio::test] - #[serial] - async fn test_revoke_unknowed_session_with_token_and_id() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let sessions = get_sessions_by_user_query(GetSessionsByUserQueryView::new(1), pool.clone()) - .await - .unwrap(); - - let result: Result<(), DatabaseError> = revoke_session_query( - RevokeSessionQueryView::new(1, Uuid::new_v4(), "a"), - pool.clone(), - ) - .await; - - assert!(result.is_ok()); - - let sessions_2 = get_sessions_by_user_query(GetSessionsByUserQueryView::new(1), pool) - .await - .unwrap(); - - assert!(sessions_2.len() >= sessions.len()); - } - - #[tokio::test] - #[serial] - async fn test_revoke_previous_session() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let sessions = get_sessions_by_user_query(GetSessionsByUserQueryView::new(1), pool.clone()) - .await - .unwrap(); - - let result: Result<(), DatabaseError> = revoke_previous_session_query( - RevokePreviousSessionQueryView::new( - 1, - std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), - "", - ), - pool.clone(), - ) - .await; - - assert!(result.is_ok()); - - let sessions_2 = get_sessions_by_user_query(GetSessionsByUserQueryView::new(1), pool) - .await - .unwrap(); - - assert!(sessions_2.len() <= sessions.len()); - } -} - -#[cfg(test)] -mod sql_injection_tests { - use super::*; - use core_api::database::sessions::create_session::{ - create_session_query, CreateSessionQueryView, - }; - - async fn get_pool(url: String) -> PgPool { - PgPoolOptions::new() - .max_connections(5) - .acquire_timeout(std::time::Duration::from_secs(3)) - .connect(&url) // On passe l'URL construite ici - .await - .expect("Failed to create Postgres pool") - } - - #[tokio::test] - #[serial] - async fn test_injection_create_session() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let malicious_token = "' OR 1=1 --"; - - // Create a session - let result = create_session_query( - CreateSessionQueryView::new( - 1, - malicious_token, - "any_device", - std::net::IpAddr::from([0, 0, 0, 0]), - ), - pool, - ) - .await; - - assert_eq!(result, Ok(())); - } -} diff --git a/tests/test_users_queries.rs b/tests/test_users_queries.rs deleted file mode 100644 index 7fcb1b7..0000000 --- a/tests/test_users_queries.rs +++ /dev/null @@ -1,60 +0,0 @@ -use mairie360_api_lib::test_setup::queries_setup::get_shared_db; -use serial_test::serial; -use sqlx::postgres::PgPoolOptions; -use sqlx::PgPool; - -async fn get_pool(url: String) -> PgPool { - PgPoolOptions::new() - .max_connections(5) - .acquire_timeout(std::time::Duration::from_secs(3)) - .connect(&url) // On passe l'URL construite ici - .await - .expect("Failed to create Postgres pool") -} - -#[cfg(test)] -mod queries_tests { - use core_api::database::users::about::{about_user_query, AboutUserQueryView}; - use mairie360_api_lib::database::errors::DatabaseError; - use mairie360_api_lib::database::queries::QueryError; - use serde_json::json; - - use super::*; - - #[tokio::test] - #[serial] - async fn test_about_user_success() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let result = about_user_query(AboutUserQueryView::new(1), pool) - .await - .unwrap(); - - let expected_json = json!({ - "first_name": "Alice", - "last_name": "Smith", - "email": "alice@example.com", - "phone_number": "0102030405", - "status": "active" - }); - - assert_eq!(result.json(), expected_json); - } - - #[tokio::test] - #[serial] - async fn test_about_user_fail() { - let (_container, host) = get_shared_db().await; - let pool = get_pool(host.to_string()).await; - - let result = about_user_query(AboutUserQueryView::new(999), pool).await; - - assert!(result.is_err()); - let err = result.err().unwrap(); - assert_eq!( - err, - DatabaseError::Query(QueryError::InvalidId("User ID not found".to_string())) - ); - } -} From 6a82f48b085fa63c0555a11ed818fed8a112647f Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Fri, 8 May 2026 16:49:32 +0800 Subject: [PATCH 05/16] fix: fix tests --- .../ressources/add_access_to_user/query.rs | 2 +- tests/common/roles.rs | 14 ++++++++++ .../queries/ressources/add_access_to_user.rs | 18 +++++++++---- tests/queries/roles/can_delete_role.rs | 12 +++++---- tests/queries/roles/change_role.rs | 27 +++++++++++-------- tests/queries/roles/create_role.rs | 14 +++++----- 6 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/database/ressources/add_access_to_user/query.rs b/src/database/ressources/add_access_to_user/query.rs index 7f32e47..e03c7ba 100644 --- a/src/database/ressources/add_access_to_user/query.rs +++ b/src/database/ressources/add_access_to_user/query.rs @@ -12,7 +12,7 @@ pub async fn add_access_to_user_query( .bind(view.ressource_type_id() as i64) .bind(view.ressource_instance_id() as i64) .bind(view.access_type_id() as i64) - .fetch_one(&pool) + .execute(&pool) .await?; Ok(()) diff --git a/tests/common/roles.rs b/tests/common/roles.rs index 0d18959..8ed32d7 100644 --- a/tests/common/roles.rs +++ b/tests/common/roles.rs @@ -5,6 +5,7 @@ use tokio::sync::OnceCell; pub static COUNT: OnceCell = OnceCell::const_new(); pub static DELETE_ID: OnceCell = OnceCell::const_new(); +pub static CAN_DELETE_ID: OnceCell = OnceCell::const_new(); pub static PATCH_ID: OnceCell = OnceCell::const_new(); pub static PATCH_MUTEX: OnceCell> = OnceCell::const_new(); @@ -29,6 +30,19 @@ pub async fn setup_tests() { let _ = sqlx::query( "INSERT INTO roles (name, description, can_be_deleted) VALUES ($1, $2, true)", ) + .bind("Can Delete") + .bind("Can Delete role") + .execute(&pool) + .await; + let can_delete_id = sqlx::query("SELECT id FROM roles WHERE name = 'Can Delete'") + .fetch_one(&pool) + .await + .unwrap() + .get::(0); + CAN_DELETE_ID.set(can_delete_id as u64).unwrap(); + let _ = sqlx::query( + "INSERT INTO roles (name, description, can_be_deleted) VALUES ($1, $2, true)", + ) .bind("Patch") .bind("Patch role") .execute(&pool) diff --git a/tests/queries/ressources/add_access_to_user.rs b/tests/queries/ressources/add_access_to_user.rs index d516daa..33d958c 100644 --- a/tests/queries/ressources/add_access_to_user.rs +++ b/tests/queries/ressources/add_access_to_user.rs @@ -1,7 +1,12 @@ use crate::common::get_pool; -use core_api::database::ressources::{ - add_access_to_user::{add_access_to_user_query, AddAccessToUserQueryView}, - get_ressource_type_id::{get_ressource_type_id_query, GetRessourceTypeIdQueryView}, +use core_api::database::{ + ressources::{ + add_access_to_user::{add_access_to_user_query, AddAccessToUserQueryView}, + get_ressource_type_id::{get_ressource_type_id_query, GetRessourceTypeIdQueryView}, + }, + rights::get_permission_id::{ + get_permission_id_query, GetPermissionIdQueryView, PermissionAction, + }, }; use mairie360_api_lib::test_setup::queries_setup::get_shared_db; use serial_test::serial; @@ -15,8 +20,11 @@ async fn success() { let id = get_ressource_type_id_query(view, pool.clone()) .await .unwrap(); - let view = AddAccessToUserQueryView::new(2, id, 1, 23); - assert!(add_access_to_user_query(view, pool).await.is_ok()); + let view = GetPermissionIdQueryView::new(id, PermissionAction::Read); + let result = get_permission_id_query(view, pool.clone()).await.unwrap(); + let view = AddAccessToUserQueryView::new(2, id, 1, result); + let result = add_access_to_user_query(view, pool).await; + assert!(result.is_ok(), "{:?}", result); } #[tokio::test] diff --git a/tests/queries/roles/can_delete_role.rs b/tests/queries/roles/can_delete_role.rs index 4fa61f5..e237f4d 100644 --- a/tests/queries/roles/can_delete_role.rs +++ b/tests/queries/roles/can_delete_role.rs @@ -1,5 +1,5 @@ use crate::common::get_pool; -use crate::common::roles::{setup_tests, DELETE_ID}; +use crate::common::roles::{setup_tests, CAN_DELETE_ID}; use core_api::database::roles::can_delete_role::{can_delete_role_query, CanDeleteRoleQueryView}; use mairie360_api_lib::database::errors::DatabaseError; use mairie360_api_lib::database::queries::QueryError; @@ -13,10 +13,12 @@ async fn test_can_delete_role_success() { let (_container, host) = get_shared_db().await; let pool = get_pool(host.to_string()).await; - let result = - can_delete_role_query(CanDeleteRoleQueryView::new(*DELETE_ID.get().unwrap()), pool) - .await - .unwrap(); + let result = can_delete_role_query( + CanDeleteRoleQueryView::new(*CAN_DELETE_ID.get().unwrap()), + pool, + ) + .await + .unwrap(); assert!(result); } diff --git a/tests/queries/roles/change_role.rs b/tests/queries/roles/change_role.rs index 998bed9..fb7cc6f 100644 --- a/tests/queries/roles/change_role.rs +++ b/tests/queries/roles/change_role.rs @@ -4,16 +4,20 @@ use core_api::database::roles::change_role::{change_role_query, ChangeRoleQueryV use core_api::database::roles::create_role::{create_role_query, CreateRoleQueryView}; use core_api::database::roles::get_roles::{get_roles_query, GetRolesQueryView}; use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use rand::random; use serial_test::serial; #[tokio::test] #[serial] async fn test_change_role_success() { - setup_tests().await; let (_container, host) = get_shared_db().await; let pool = get_pool(host.to_string()).await; - let view = CreateRoleQueryView::new("Change_test", "Change_test role", Some(true)); + let name = "test_change_role_success".to_string() + random::().to_string().as_str(); + let description = + "test_change_role_success_description".to_string() + random::().to_string().as_str(); + + let view = CreateRoleQueryView::new(&name, &description, Some(true)); let result = create_role_query(view, pool.clone()).await; assert!(result.is_ok()); @@ -24,16 +28,20 @@ async fn test_change_role_success() { let mut new_role_id: i32 = 0; for role in roles { - if role.name() == "Change_test" { + if role.name() == name { new_role_id = role.id(); break; } } + let change_name = "Change_Admin".to_string() + random::().to_string().as_str(); + let change_description = + "Change_Administrateur".to_string() + random::().to_string().as_str(); + let view = ChangeRoleQueryView::new( new_role_id as u64, - "Change_Admin", - "Change_Administrateur", + &change_name, + &change_description, Some(true), ); let result = change_role_query(view, pool.clone()).await; @@ -45,13 +53,10 @@ async fn test_change_role_success() { .unwrap(); for role in roles { - if role - .description() - .is_some_and(|d| d == "Change_Administrateur".to_string()) - { + if role.description().is_some_and(|d| d == change_description) { assert_eq!(role.id(), new_role_id); - assert_eq!(role.name(), "Change_Admin"); - assert_eq!(role.description().unwrap(), "Change_Administrateur"); + assert_eq!(role.name(), change_name); + assert_eq!(role.description().unwrap(), change_description); assert!(role.updated_at().is_some()); assert!(role.can_be_deleted()); } diff --git a/tests/queries/roles/create_role.rs b/tests/queries/roles/create_role.rs index f2e7673..202d283 100644 --- a/tests/queries/roles/create_role.rs +++ b/tests/queries/roles/create_role.rs @@ -1,14 +1,13 @@ use crate::common::get_pool; -use crate::common::roles::setup_tests; use core_api::database::roles::create_role::{create_role_query, CreateRoleQueryView}; use core_api::database::roles::get_roles::{get_roles_query, GetRolesQueryView}; use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use rand::random; use serial_test::serial; #[tokio::test] #[serial] async fn test_create_role_success() { - setup_tests().await; let (_container, host) = get_shared_db().await; let pool = get_pool(host.to_string()).await; @@ -17,7 +16,10 @@ async fn test_create_role_success() { .unwrap() .len(); - let view = CreateRoleQueryView::new("Create", "Create role", Some(false)); + let name = "create_role_success".to_string() + random::().to_string().as_str(); + let description = + "create_role_success_description".to_string() + random::().to_string().as_str(); + let view = CreateRoleQueryView::new(&name, &description, Some(false)); let result = create_role_query(view, pool.clone()).await; assert!(result.is_ok()); @@ -28,10 +30,10 @@ async fn test_create_role_success() { assert!(roles.len() >= nb_roles); for role in roles { - if role.description().is_some_and(|d| d == "Create role") { + if role.description().is_some_and(|d| d == description) { assert!(role.id() > 2); - assert_eq!(role.name(), "Create"); - assert_eq!(role.description().unwrap(), "Create role"); + assert_eq!(role.name(), name); + assert_eq!(role.description().unwrap(), description); assert!(role.updated_at().is_some()); assert!(!role.can_be_deleted()); } From 5b77a82a6aee2f4a7e8b027f549284b8cb3c7671 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Fri, 8 May 2026 17:35:12 +0800 Subject: [PATCH 06/16] feat: add get accesses by ressource id --- .../ressources/get_access_by_ressource/mod.rs | 5 ++ .../get_access_by_ressource/query.rs | 16 ++++ .../get_access_by_ressource/view.rs | 87 +++++++++++++++++++ src/database/ressources/mod.rs | 1 + .../ressources/remove_access/query.rs | 2 +- .../ressources/get_access_by_ressource.rs | 41 +++++++++ tests/queries/ressources/mod.rs | 1 + tests/queries/ressources/remove_access.rs | 37 +++++++- 8 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 src/database/ressources/get_access_by_ressource/mod.rs create mode 100644 src/database/ressources/get_access_by_ressource/query.rs create mode 100644 src/database/ressources/get_access_by_ressource/view.rs create mode 100644 tests/queries/ressources/get_access_by_ressource.rs diff --git a/src/database/ressources/get_access_by_ressource/mod.rs b/src/database/ressources/get_access_by_ressource/mod.rs new file mode 100644 index 0000000..9cbea3a --- /dev/null +++ b/src/database/ressources/get_access_by_ressource/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::get_access_by_ressource; + +mod view; +pub use view::{Access, GetAccessByRessourceQueryView}; diff --git a/src/database/ressources/get_access_by_ressource/query.rs b/src/database/ressources/get_access_by_ressource/query.rs new file mode 100644 index 0000000..398203b --- /dev/null +++ b/src/database/ressources/get_access_by_ressource/query.rs @@ -0,0 +1,16 @@ +use crate::database::ressources::get_access_by_ressource::{Access, GetAccessByRessourceQueryView}; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn get_access_by_ressource( + view: GetAccessByRessourceQueryView, + pool: PgPool, +) -> Result, DatabaseError> { + let result: Vec = sqlx::query_as(&view.get_request()) + .bind(view.resource_id() as i32) + .fetch_all(&pool) + .await?; + + Ok(result) +} diff --git a/src/database/ressources/get_access_by_ressource/view.rs b/src/database/ressources/get_access_by_ressource/view.rs new file mode 100644 index 0000000..0a8d292 --- /dev/null +++ b/src/database/ressources/get_access_by_ressource/view.rs @@ -0,0 +1,87 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +#[derive(Debug, Deserialize, Eq, PartialEq, Serialize, sqlx::FromRow)] +pub struct Access { + id: i32, + user_id: Option, + group_id: Option, + resource_id: i32, + resource_instance_id: i32, + permission_id: i32, +} + +impl Access { + pub fn new( + id: i32, + user_id: Option, + group_id: Option, + resource_id: i32, + resource_instance_id: i32, + permission_id: i32, + ) -> Self { + Self { + id, + user_id, + group_id, + resource_id, + resource_instance_id, + permission_id, + } + } + + pub fn id(&self) -> i32 { + self.id + } + + pub fn user_id(&self) -> Option { + self.user_id + } + + pub fn group_id(&self) -> Option { + self.group_id + } + + pub fn resource_id(&self) -> i32 { + self.resource_id + } + + pub fn resource_instance_id(&self) -> i32 { + self.resource_instance_id + } + + pub fn permission_id(&self) -> i32 { + self.permission_id + } +} + +pub struct GetAccessByRessourceQueryView { + resource_id: u64, +} + +impl GetAccessByRessourceQueryView { + pub fn new(resource_id: u64) -> Self { + Self { resource_id } + } + + pub fn resource_id(&self) -> u64 { + self.resource_id + } +} + +impl DatabaseQueryView for GetAccessByRessourceQueryView { + fn get_request(&self) -> String { + "SELECT * FROM access_control WHERE resource_instance_id = $1".to_string() + } +} + +impl Display for GetAccessByRessourceQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "GetAccessByRessource: resource_id = {}", + self.resource_id, + ) + } +} diff --git a/src/database/ressources/mod.rs b/src/database/ressources/mod.rs index e227f5c..d4bc55c 100644 --- a/src/database/ressources/mod.rs +++ b/src/database/ressources/mod.rs @@ -1,5 +1,6 @@ pub mod add_access_to_user; pub mod can_add_access; +pub mod get_access_by_ressource; pub mod get_ressource_type_id; pub mod is_owner; pub mod remove_access; diff --git a/src/database/ressources/remove_access/query.rs b/src/database/ressources/remove_access/query.rs index 6a16335..ef1472d 100644 --- a/src/database/ressources/remove_access/query.rs +++ b/src/database/ressources/remove_access/query.rs @@ -9,7 +9,7 @@ pub async fn remove_access_query( ) -> Result<(), DatabaseError> { sqlx::query(&view.get_request()) .bind(view.id() as i64) - .fetch_one(&pool) + .execute(&pool) .await?; Ok(()) diff --git a/tests/queries/ressources/get_access_by_ressource.rs b/tests/queries/ressources/get_access_by_ressource.rs new file mode 100644 index 0000000..38b20b3 --- /dev/null +++ b/tests/queries/ressources/get_access_by_ressource.rs @@ -0,0 +1,41 @@ +use crate::common::get_pool; +use core_api::database::{ + ressources::{ + add_access_to_user::{add_access_to_user_query, AddAccessToUserQueryView}, + get_access_by_ressource::{get_access_by_ressource, GetAccessByRessourceQueryView}, + get_ressource_type_id::{get_ressource_type_id_query, GetRessourceTypeIdQueryView}, + }, + rights::get_permission_id::{ + get_permission_id_query, GetPermissionIdQueryView, PermissionAction, + }, +}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetRessourceTypeIdQueryView::new("groups"); + let id = get_ressource_type_id_query(view, pool.clone()) + .await + .unwrap(); + let view = GetPermissionIdQueryView::new(id, PermissionAction::Read); + let result = get_permission_id_query(view, pool.clone()).await.unwrap(); + let view = AddAccessToUserQueryView::new(4, id, 1, result); + let _ = add_access_to_user_query(view, pool.clone()).await; + let view = GetAccessByRessourceQueryView::new(1); + let result = get_access_by_ressource(view, pool).await.unwrap(); + assert!(!result.is_empty(), "{:?}", result); +} + +#[tokio::test] +#[serial] +async fn unknow_ressource() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetAccessByRessourceQueryView::new(2); + let result = get_access_by_ressource(view, pool).await.unwrap(); + assert!(result.is_empty(), "{:?}", result); +} diff --git a/tests/queries/ressources/mod.rs b/tests/queries/ressources/mod.rs index e227f5c..d4bc55c 100644 --- a/tests/queries/ressources/mod.rs +++ b/tests/queries/ressources/mod.rs @@ -1,5 +1,6 @@ pub mod add_access_to_user; pub mod can_add_access; +pub mod get_access_by_ressource; pub mod get_ressource_type_id; pub mod is_owner; pub mod remove_access; diff --git a/tests/queries/ressources/remove_access.rs b/tests/queries/ressources/remove_access.rs index 5d5b8d5..87155ff 100644 --- a/tests/queries/ressources/remove_access.rs +++ b/tests/queries/ressources/remove_access.rs @@ -1,12 +1,41 @@ use crate::common::get_pool; -use core_api::database::ressources::remove_access::{remove_access_query, RemoveAccessQueryView}; -use mairie360_api_lib::test_setup::queries_setup::{get_shared_db, GROUP_OWNER_ID}; +use core_api::database::{ + ressources::{ + add_access_to_user::{add_access_to_user_query, AddAccessToUserQueryView}, + get_ressource_type_id::{get_ressource_type_id_query, GetRessourceTypeIdQueryView}, + remove_access::{remove_access_query, RemoveAccessQueryView}, + }, + rights::get_permission_id::{ + get_permission_id_query, GetPermissionIdQueryView, PermissionAction, + }, +}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; use serial_test::serial; #[tokio::test] #[serial] -async fn success() {} +async fn success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = GetRessourceTypeIdQueryView::new("groups"); + let id = get_ressource_type_id_query(view, pool.clone()) + .await + .unwrap(); + let view = GetPermissionIdQueryView::new(id, PermissionAction::Read); + let result = get_permission_id_query(view, pool.clone()).await.unwrap(); + let view = AddAccessToUserQueryView::new(3, id, 1, result); + let _ = add_access_to_user_query(view, pool.clone()).await; + let view = RemoveAccessQueryView::new(2); + let result = remove_access_query(view, pool).await; + assert!(result.is_ok(), "{:?}", result); +} #[tokio::test] #[serial] -async fn failure() {} +async fn bad_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = RemoveAccessQueryView::new(3); + let result = remove_access_query(view, pool).await; + assert!(result.is_ok(), "{:?}", result); +} From e12945c3d89f843644a6c68250f3bbae38daa4d5 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Fri, 8 May 2026 18:28:09 +0800 Subject: [PATCH 07/16] feat: add ressources access endpoints --- .../ressources/can_add_access/query.rs | 1 - .../get_access_by_ressource/view.rs | 16 +++- .../v1/ressources/add_access/endpoint.rs | 83 +++++++++++++++---- .../v1/ressources/add_access/view.rs | 6 +- src/endpoints/v1/ressources/doc.rs | 8 +- .../v1/ressources/get_access/endpoint.rs | 79 ++++++++++++++++++ src/endpoints/v1/ressources/get_access/mod.rs | 2 + .../v1/ressources/get_access/view.rs | 29 +++++++ src/endpoints/v1/ressources/mod.rs | 7 +- .../v1/ressources/remove_access/endpoint.rs | 38 +++++---- .../v1/ressources/remove_access/view.rs | 4 - 11 files changed, 225 insertions(+), 48 deletions(-) create mode 100644 src/endpoints/v1/ressources/get_access/endpoint.rs create mode 100644 src/endpoints/v1/ressources/get_access/mod.rs create mode 100644 src/endpoints/v1/ressources/get_access/view.rs diff --git a/src/database/ressources/can_add_access/query.rs b/src/database/ressources/can_add_access/query.rs index 4fda54a..6817638 100644 --- a/src/database/ressources/can_add_access/query.rs +++ b/src/database/ressources/can_add_access/query.rs @@ -1,7 +1,6 @@ use crate::database::ressources::can_add_access::CanAddAccessQueryView; use crate::database::ressources::is_owner::{is_owner_query, IsOwnerQueryView}; use crate::endpoints::v1::ressources::AccessType; -use mairie360_api_lib::database::db_interface::DatabaseQueryView; use mairie360_api_lib::database::errors::DatabaseError; use sqlx::PgPool; diff --git a/src/database/ressources/get_access_by_ressource/view.rs b/src/database/ressources/get_access_by_ressource/view.rs index 0a8d292..978f5de 100644 --- a/src/database/ressources/get_access_by_ressource/view.rs +++ b/src/database/ressources/get_access_by_ressource/view.rs @@ -1,8 +1,9 @@ use mairie360_api_lib::database::db_interface::DatabaseQueryView; use serde::{Deserialize, Serialize}; use std::fmt::Display; +use utoipa::ToSchema; -#[derive(Debug, Deserialize, Eq, PartialEq, Serialize, sqlx::FromRow)] +#[derive(Debug, Deserialize, Eq, PartialEq, Serialize, sqlx::FromRow, ToSchema)] pub struct Access { id: i32, user_id: Option, @@ -56,6 +57,19 @@ impl Access { } } +impl Display for Access { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Access:")?; + writeln!(f, " id: {}", self.id)?; + writeln!(f, " user_id: {:?}", self.user_id)?; + writeln!(f, " group_id: {:?}", self.group_id)?; + writeln!(f, " resource_id: {}", self.resource_id)?; + writeln!(f, " resource_instance_id: {}", self.resource_instance_id)?; + writeln!(f, " permission_id: {}", self.permission_id)?; + Ok(()) + } +} + pub struct GetAccessByRessourceQueryView { resource_id: u64, } diff --git a/src/endpoints/v1/ressources/add_access/endpoint.rs b/src/endpoints/v1/ressources/add_access/endpoint.rs index 078dea1..4e5052d 100644 --- a/src/endpoints/v1/ressources/add_access/endpoint.rs +++ b/src/endpoints/v1/ressources/add_access/endpoint.rs @@ -1,37 +1,43 @@ +use crate::database::ressources::add_access_to_user::{ + add_access_to_user_query, AddAccessToUserQueryView, +}; +use crate::database::ressources::get_ressource_type_id::{ + get_ressource_type_id_query, GetRessourceTypeIdQueryView, +}; +use crate::database::rights::get_permission_id::{ + get_permission_id_query, GetPermissionIdQueryView, PermissionAction, +}; use crate::endpoints::v1::ressources::add_access::view::AddAccessView; use actix_web::http::StatusCode; use actix_web::{post, web, HttpResponse, Responder, ResponseError}; use mairie360_api_lib::pool::AppState; +use mairie360_api_lib::security::AuthenticatedUser; +use sqlx::PgPool; #[derive(Debug, Clone, PartialEq)] -enum GetError { +enum AddAccessError { BadRequest, DatabaseError, - Unauthorized, } -impl std::fmt::Display for GetError { +impl std::fmt::Display for AddAccessError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - GetError::DatabaseError => { + AddAccessError::DatabaseError => { write!(f, "An error occurred while accessing the database.") } - GetError::BadRequest => { + AddAccessError::BadRequest => { write!(f, "Bad request.") } - GetError::Unauthorized => { - write!(f, "Unauthorized.") - } } } } -impl ResponseError for GetError { +impl ResponseError for AddAccessError { fn status_code(&self) -> StatusCode { match self { - GetError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, - GetError::BadRequest => StatusCode::BAD_REQUEST, - GetError::Unauthorized => StatusCode::UNAUTHORIZED, + AddAccessError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + AddAccessError::BadRequest => StatusCode::BAD_REQUEST, } } @@ -40,10 +46,52 @@ impl ResponseError for GetError { } } +async fn get_request_view( + pool: PgPool, + request_view: AddAccessView, +) -> Result { + let ressource_type_id = get_ressource_type_id_query( + GetRessourceTypeIdQueryView::new(request_view.ressource_type()), + pool.clone(), + ) + .await + .map_err(|_| AddAccessError::BadRequest)?; + + let access_type_id = get_permission_id_query( + GetPermissionIdQueryView::new( + ressource_type_id, + PermissionAction::from(request_view.access_type().as_str().to_string()), + ), + pool.clone(), + ) + .await + .map_err(|_| AddAccessError::BadRequest)?; + + Ok(AddAccessToUserQueryView::new( + request_view.user_id(), + request_view.resource_id(), + ressource_type_id, + access_type_id, + )) +} + async fn add_access_to_ressource( state: web::Data, view: AddAccessView, -) -> Result<(), GetError> { +) -> Result<(), AddAccessError> { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(AddAccessError::DatabaseError), + }; + + let view = get_request_view(pool.clone(), view) + .await + .map_err(|_| AddAccessError::BadRequest)?; + + add_access_to_user_query(view, pool) + .await + .map_err(|_| AddAccessError::BadRequest)?; + Ok(()) } @@ -63,11 +111,10 @@ async fn add_access_to_ressource( )] #[post("/add_access")] pub async fn add_access( + _: AuthenticatedUser, state: web::Data, view: web::Json, -) -> Result { - match add_access_to_ressource(state, view.into_inner()).await { - Ok(_) => Ok(HttpResponse::Ok()), - Err(e) => Err(e), - } +) -> Result { + add_access_to_ressource(state, view.into_inner()).await?; + Ok(HttpResponse::Ok().body("Access added successfully")) } diff --git a/src/endpoints/v1/ressources/add_access/view.rs b/src/endpoints/v1/ressources/add_access/view.rs index b12b948..1035681 100644 --- a/src/endpoints/v1/ressources/add_access/view.rs +++ b/src/endpoints/v1/ressources/add_access/view.rs @@ -5,14 +5,14 @@ pub enum AccessType { Delete, Error, Read, - Write, + Update, } impl AccessType { pub fn as_str(&self) -> &'static str { match self { AccessType::Read => "read", - AccessType::Write => "write", + AccessType::Update => "update", AccessType::Delete => "delete", AccessType::Error => "error", } @@ -23,7 +23,7 @@ impl From<&str> for AccessType { fn from(s: &str) -> Self { match s { "read" => AccessType::Read, - "write" => AccessType::Write, + "update" => AccessType::Update, "delete" => AccessType::Delete, _ => AccessType::Error, } diff --git a/src/endpoints/v1/ressources/doc.rs b/src/endpoints/v1/ressources/doc.rs index 670b503..91adb4c 100644 --- a/src/endpoints/v1/ressources/doc.rs +++ b/src/endpoints/v1/ressources/doc.rs @@ -1,13 +1,19 @@ use utoipa::OpenApi; use crate::endpoints::v1::ressources::add_access::endpoint as add_access_endpoint; +use crate::endpoints::v1::ressources::get_access::endpoint as get_access_endpoint; use crate::endpoints::v1::ressources::remove_access::endpoint as remove_access_endpoint; #[derive(OpenApi)] #[openapi( - paths(add_access_endpoint::add_access, remove_access_endpoint::remove_access), + paths( + add_access_endpoint::add_access, + get_access_endpoint::get_access, + remove_access_endpoint::remove_access, + ), components(schemas( super::add_access::view::AddAccessView, + super::get_access::view::GetAccessResultView, super::remove_access::view::RemoveAccessView )) )] diff --git a/src/endpoints/v1/ressources/get_access/endpoint.rs b/src/endpoints/v1/ressources/get_access/endpoint.rs new file mode 100644 index 0000000..15d48c6 --- /dev/null +++ b/src/endpoints/v1/ressources/get_access/endpoint.rs @@ -0,0 +1,79 @@ +use crate::database::ressources::get_access_by_ressource::{ + get_access_by_ressource, GetAccessByRessourceQueryView, +}; +use crate::endpoints::v1::ressources::GetAccessResultView; +use actix_web::http::StatusCode; +use actix_web::{post, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; + +#[derive(Debug, Clone, PartialEq)] +enum GetError { + BadRequest, + DatabaseError, +} + +impl std::fmt::Display for GetError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GetError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + GetError::BadRequest => { + write!(f, "Bad request.") + } + } + } +} + +impl ResponseError for GetError { + fn status_code(&self) -> StatusCode { + match self { + GetError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + GetError::BadRequest => StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn get_access_from_ressource( + state: web::Data, + ressource_id: u64, +) -> Result { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(GetError::DatabaseError), + }; + + let view = GetAccessByRessourceQueryView::new(ressource_id); + let result = get_access_by_ressource(view, pool) + .await + .map_err(|_| GetError::BadRequest)?; + + Ok(GetAccessResultView::new(result)) +} + +#[utoipa::path( + post, + path = "/{id}/access", + responses( + (status = 200, body = GetAccessResultView), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ), + tag = "Ressources", + security( + ("jwt" = []) + ) +)] +#[post("/{id}/access")] +pub async fn get_access( + state: web::Data, + id: web::Path, +) -> Result { + let response = get_access_from_ressource(state, id.into_inner()).await?; + Ok(HttpResponse::Ok().json(response)) +} diff --git a/src/endpoints/v1/ressources/get_access/mod.rs b/src/endpoints/v1/ressources/get_access/mod.rs new file mode 100644 index 0000000..cbeb677 --- /dev/null +++ b/src/endpoints/v1/ressources/get_access/mod.rs @@ -0,0 +1,2 @@ +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/ressources/get_access/view.rs b/src/endpoints/v1/ressources/get_access/view.rs new file mode 100644 index 0000000..7a7d119 --- /dev/null +++ b/src/endpoints/v1/ressources/get_access/view.rs @@ -0,0 +1,29 @@ +use crate::database::ressources::get_access_by_ressource::Access; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GetAccessResultView { + accesses: Vec, +} + +impl GetAccessResultView { + pub fn new(accesses: Vec) -> Self { + Self { accesses } + } + + pub fn accesses(&self) -> &[Access] { + &self.accesses + } +} + +impl Display for GetAccessResultView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Accesses:")?; + for access in &self.accesses { + writeln!(f, " {}", access)?; + } + Ok(()) + } +} diff --git a/src/endpoints/v1/ressources/mod.rs b/src/endpoints/v1/ressources/mod.rs index 9f5598d..bdbb8b3 100644 --- a/src/endpoints/v1/ressources/mod.rs +++ b/src/endpoints/v1/ressources/mod.rs @@ -1,5 +1,7 @@ -mod add_access; +pub mod add_access; pub mod doc; +mod get_access; +pub use get_access::view::GetAccessResultView; mod remove_access; pub use add_access::view::AccessType; @@ -9,6 +11,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/ressources") .service(add_access::endpoint::add_access) - .service(remove_access::endpoint::remove_access), + .service(remove_access::endpoint::remove_access) + .service(get_access::endpoint::get_access), ); } diff --git a/src/endpoints/v1/ressources/remove_access/endpoint.rs b/src/endpoints/v1/ressources/remove_access/endpoint.rs index 621969f..0e7b5b6 100644 --- a/src/endpoints/v1/ressources/remove_access/endpoint.rs +++ b/src/endpoints/v1/ressources/remove_access/endpoint.rs @@ -1,37 +1,33 @@ +use crate::database::ressources::remove_access::{remove_access_query, RemoveAccessQueryView}; use crate::endpoints::v1::ressources::remove_access::view::RemoveAccessView; use actix_web::http::StatusCode; use actix_web::{post, web, HttpResponse, Responder, ResponseError}; use mairie360_api_lib::pool::AppState; #[derive(Debug, Clone, PartialEq)] -enum GetError { +enum RemoveAccessError { BadRequest, DatabaseError, - Unauthorized, } -impl std::fmt::Display for GetError { +impl std::fmt::Display for RemoveAccessError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - GetError::DatabaseError => { + RemoveAccessError::DatabaseError => { write!(f, "An error occurred while accessing the database.") } - GetError::BadRequest => { + RemoveAccessError::BadRequest => { write!(f, "Bad request.") } - GetError::Unauthorized => { - write!(f, "Unauthorized.") - } } } } -impl ResponseError for GetError { +impl ResponseError for RemoveAccessError { fn status_code(&self) -> StatusCode { match self { - GetError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, - GetError::BadRequest => StatusCode::BAD_REQUEST, - GetError::Unauthorized => StatusCode::UNAUTHORIZED, + RemoveAccessError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + RemoveAccessError::BadRequest => StatusCode::BAD_REQUEST, } } @@ -43,7 +39,15 @@ impl ResponseError for GetError { async fn remove_access_to_ressource( state: web::Data, view: RemoveAccessView, -) -> Result<(), GetError> { +) -> Result<(), RemoveAccessError> { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(RemoveAccessError::DatabaseError), + }; + let request_view = RemoveAccessQueryView::new(view.access_id()); + remove_access_query(request_view, pool) + .await + .map_err(|_| RemoveAccessError::BadRequest)?; Ok(()) } @@ -65,9 +69,7 @@ async fn remove_access_to_ressource( pub async fn remove_access( state: web::Data, view: web::Json, -) -> Result { - match remove_access_to_ressource(state, view.into_inner()).await { - Ok(_) => Ok(HttpResponse::Ok()), - Err(e) => Err(e), - } +) -> Result { + remove_access_to_ressource(state, view.into_inner()).await?; + Ok(HttpResponse::Ok().body("Access removed successfully")) } diff --git a/src/endpoints/v1/ressources/remove_access/view.rs b/src/endpoints/v1/ressources/remove_access/view.rs index e0a88ee..f3dccdd 100644 --- a/src/endpoints/v1/ressources/remove_access/view.rs +++ b/src/endpoints/v1/ressources/remove_access/view.rs @@ -6,10 +6,6 @@ pub struct RemoveAccessView { } impl RemoveAccessView { - pub fn new(access_id: u64) -> Self { - Self { access_id } - } - pub fn access_id(&self) -> u64 { self.access_id } From 522a63ab1186d6f425cb0ac77b7246ebf625ab80 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Fri, 8 May 2026 20:38:16 +0800 Subject: [PATCH 08/16] feat: add groups doc --- src/endpoints/v1/doc.rs | 2 + src/endpoints/v1/groups/doc.rs | 22 ++++++ src/endpoints/v1/groups/get/endpoint.rs | 72 ++++++++++++++++++ src/endpoints/v1/groups/get/mod.rs | 2 + src/endpoints/v1/groups/get/view.rs | 14 ++++ src/endpoints/v1/groups/id/delete/endpoint.rs | 73 ++++++++++++++++++ src/endpoints/v1/groups/id/delete/mod.rs | 1 + src/endpoints/v1/groups/id/doc.rs | 18 +++++ src/endpoints/v1/groups/id/get/endpoint.rs | 74 ++++++++++++++++++ src/endpoints/v1/groups/id/get/mod.rs | 2 + src/endpoints/v1/groups/id/get/view.rs | 7 ++ src/endpoints/v1/groups/id/mod.rs | 15 ++++ src/endpoints/v1/groups/id/users/doc.rs | 21 ++++++ .../v1/groups/id/users/get/endpoint.rs | 73 ++++++++++++++++++ src/endpoints/v1/groups/id/users/get/mod.rs | 2 + src/endpoints/v1/groups/id/users/get/view.rs | 12 +++ .../v1/groups/id/users/id/delete/endpoint.rs | 74 ++++++++++++++++++ .../v1/groups/id/users/id/delete/mod.rs | 1 + src/endpoints/v1/groups/id/users/id/doc.rs | 6 ++ src/endpoints/v1/groups/id/users/id/mod.rs | 8 ++ src/endpoints/v1/groups/id/users/mod.rs | 15 ++++ .../v1/groups/id/users/post/endpoint.rs | 74 ++++++++++++++++++ src/endpoints/v1/groups/id/users/post/mod.rs | 2 + src/endpoints/v1/groups/id/users/post/view.rs | 21 ++++++ src/endpoints/v1/groups/mod.rs | 15 ++++ src/endpoints/v1/groups/post/endpoint.rs | 75 +++++++++++++++++++ src/endpoints/v1/groups/post/mod.rs | 2 + src/endpoints/v1/groups/post/view.rs | 24 ++++++ src/endpoints/v1/mod.rs | 2 + 29 files changed, 729 insertions(+) create mode 100644 src/endpoints/v1/groups/doc.rs create mode 100644 src/endpoints/v1/groups/get/endpoint.rs create mode 100644 src/endpoints/v1/groups/get/mod.rs create mode 100644 src/endpoints/v1/groups/get/view.rs create mode 100644 src/endpoints/v1/groups/id/delete/endpoint.rs create mode 100644 src/endpoints/v1/groups/id/delete/mod.rs create mode 100644 src/endpoints/v1/groups/id/doc.rs create mode 100644 src/endpoints/v1/groups/id/get/endpoint.rs create mode 100644 src/endpoints/v1/groups/id/get/mod.rs create mode 100644 src/endpoints/v1/groups/id/get/view.rs create mode 100644 src/endpoints/v1/groups/id/mod.rs create mode 100644 src/endpoints/v1/groups/id/users/doc.rs create mode 100644 src/endpoints/v1/groups/id/users/get/endpoint.rs create mode 100644 src/endpoints/v1/groups/id/users/get/mod.rs create mode 100644 src/endpoints/v1/groups/id/users/get/view.rs create mode 100644 src/endpoints/v1/groups/id/users/id/delete/endpoint.rs create mode 100644 src/endpoints/v1/groups/id/users/id/delete/mod.rs create mode 100644 src/endpoints/v1/groups/id/users/id/doc.rs create mode 100644 src/endpoints/v1/groups/id/users/id/mod.rs create mode 100644 src/endpoints/v1/groups/id/users/mod.rs create mode 100644 src/endpoints/v1/groups/id/users/post/endpoint.rs create mode 100644 src/endpoints/v1/groups/id/users/post/mod.rs create mode 100644 src/endpoints/v1/groups/id/users/post/view.rs create mode 100644 src/endpoints/v1/groups/mod.rs create mode 100644 src/endpoints/v1/groups/post/endpoint.rs create mode 100644 src/endpoints/v1/groups/post/mod.rs create mode 100644 src/endpoints/v1/groups/post/view.rs diff --git a/src/endpoints/v1/doc.rs b/src/endpoints/v1/doc.rs index fa1d55c..d73fa41 100644 --- a/src/endpoints/v1/doc.rs +++ b/src/endpoints/v1/doc.rs @@ -1,5 +1,6 @@ use super::admin::doc::AdminDoc; use super::auth::doc::AuthDoc; +use super::groups::doc::GroupsDoc; use super::ressources::doc::RessourcesDoc; use super::roles::doc::RolesDoc; use super::sessions::doc::SessionsDoc; @@ -10,6 +11,7 @@ use utoipa::OpenApi; #[openapi(nest( (path = "/admin", api = AdminDoc), (path = "/auth", api = AuthDoc), + (path = "/groups", api = GroupsDoc), (path = "/ressources", api = RessourcesDoc), (path = "/roles", api = RolesDoc), (path = "/sessions", api = SessionsDoc), diff --git a/src/endpoints/v1/groups/doc.rs b/src/endpoints/v1/groups/doc.rs new file mode 100644 index 0000000..053be4e --- /dev/null +++ b/src/endpoints/v1/groups/doc.rs @@ -0,0 +1,22 @@ +use super::get::endpoint::__path_get; +use super::id::doc::GroupsIdDoc; +use super::post::endpoint::__path_post; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi(nest( + (path = "/", api = Doc, tags = ["Groups"]), + (path = "/{group_id}", api = GroupsIdDoc, tags = ["Groups"]), + +))] +pub struct GroupsDoc; + +#[derive(OpenApi)] +#[openapi( + paths(get, post), + components(schemas( + super::get::view::GetGroupsResultView, + super::post::view::PostGroupView, + )) +)] +struct Doc; diff --git a/src/endpoints/v1/groups/get/endpoint.rs b/src/endpoints/v1/groups/get/endpoint.rs new file mode 100644 index 0000000..e2b66c9 --- /dev/null +++ b/src/endpoints/v1/groups/get/endpoint.rs @@ -0,0 +1,72 @@ +use crate::endpoints::v1::groups::get::view::GetGroupsResultView; +use actix_web::http::StatusCode; +use actix_web::{get, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; +use mairie360_api_lib::security::AuthenticatedUser; + +#[derive(Debug, Clone, PartialEq)] +enum GetGroupsError { + BadRequest, + DatabaseError, +} + +impl std::fmt::Display for GetGroupsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GetGroupsError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + GetGroupsError::BadRequest => { + write!(f, "Bad request.") + } + } + } +} + +impl ResponseError for GetGroupsError { + fn status_code(&self) -> StatusCode { + match self { + GetGroupsError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + GetGroupsError::BadRequest => StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn get_groups( + user: AuthenticatedUser, + state: web::Data, +) -> Result<(), GetGroupsError> { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(GetGroupsError::DatabaseError), + }; + + Ok(()) +} + +#[utoipa::path( + get, + path = "", + responses( + (status = 200, body = GetGroupsResultView), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ), + tag = "Groups", + security( + ("jwt" = []) + ) +)] +#[get("/")] +pub async fn get( + user: AuthenticatedUser, + state: web::Data, +) -> Result { + let result = get_groups(user, state).await?; + Ok(HttpResponse::Ok().body(result)) +} diff --git a/src/endpoints/v1/groups/get/mod.rs b/src/endpoints/v1/groups/get/mod.rs new file mode 100644 index 0000000..cbeb677 --- /dev/null +++ b/src/endpoints/v1/groups/get/mod.rs @@ -0,0 +1,2 @@ +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/groups/get/view.rs b/src/endpoints/v1/groups/get/view.rs new file mode 100644 index 0000000..de62f0c --- /dev/null +++ b/src/endpoints/v1/groups/get/view.rs @@ -0,0 +1,14 @@ +use utoipa::ToSchema; + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct Group { + id: u64, + owner_id: u64, + name: String, + description: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct GetGroupsResultView { + groups: Vec, +} diff --git a/src/endpoints/v1/groups/id/delete/endpoint.rs b/src/endpoints/v1/groups/id/delete/endpoint.rs new file mode 100644 index 0000000..2c7c6d8 --- /dev/null +++ b/src/endpoints/v1/groups/id/delete/endpoint.rs @@ -0,0 +1,73 @@ +use actix_web::http::StatusCode; +use actix_web::{delete, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; +use mairie360_api_lib::security::AuthenticatedUser; + +#[derive(Debug, Clone, PartialEq)] +enum DeleteGroupError { + BadRequest, + DatabaseError, +} + +impl std::fmt::Display for DeleteGroupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DeleteGroupError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + DeleteGroupError::BadRequest => { + write!(f, "Bad request.") + } + } + } +} + +impl ResponseError for DeleteGroupError { + fn status_code(&self) -> StatusCode { + match self { + DeleteGroupError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + DeleteGroupError::BadRequest => StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn delete_group( + user: AuthenticatedUser, + state: web::Data, + id: u64, +) -> Result<(), DeleteGroupError> { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(DeleteGroupError::DatabaseError), + }; + + Ok(()) +} + +#[utoipa::path( + delete, + path = "", + responses( + (status = 204, description = "Group deleted successfully"), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ), + tag = "Groups", + security( + ("jwt" = []) + ) +)] +#[delete("/")] +pub async fn delete( + user: AuthenticatedUser, + state: web::Data, + id: web::Path, +) -> Result { + delete_group(user, state, id.into_inner()).await?; + Ok(HttpResponse::NoContent().finish()) +} diff --git a/src/endpoints/v1/groups/id/delete/mod.rs b/src/endpoints/v1/groups/id/delete/mod.rs new file mode 100644 index 0000000..657213d --- /dev/null +++ b/src/endpoints/v1/groups/id/delete/mod.rs @@ -0,0 +1 @@ +pub mod endpoint; diff --git a/src/endpoints/v1/groups/id/doc.rs b/src/endpoints/v1/groups/id/doc.rs new file mode 100644 index 0000000..7bc1b55 --- /dev/null +++ b/src/endpoints/v1/groups/id/doc.rs @@ -0,0 +1,18 @@ +use super::delete::endpoint::__path_delete; +use super::get::endpoint::__path_get; +use super::users::doc::GroupsUsersDoc; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi(nest( + (path = "/", api = Doc, tags = ["Groups"]), + (path = "/users", api = GroupsUsersDoc, tags = ["Groups"]), +))] +pub struct GroupsIdDoc; + +#[derive(OpenApi)] +#[openapi( + paths(get, delete), + components(schemas(super::get::view::GetGroupResultView,)) +)] +struct Doc; diff --git a/src/endpoints/v1/groups/id/get/endpoint.rs b/src/endpoints/v1/groups/id/get/endpoint.rs new file mode 100644 index 0000000..afbc57b --- /dev/null +++ b/src/endpoints/v1/groups/id/get/endpoint.rs @@ -0,0 +1,74 @@ +use crate::endpoints::v1::groups::id::get::view::GetGroupResultView; +use actix_web::http::StatusCode; +use actix_web::{get, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; +use mairie360_api_lib::security::AuthenticatedUser; + +#[derive(Debug, Clone, PartialEq)] +enum GetGroupError { + BadRequest, + DatabaseError, +} + +impl std::fmt::Display for GetGroupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GetGroupError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + GetGroupError::BadRequest => { + write!(f, "Bad request.") + } + } + } +} + +impl ResponseError for GetGroupError { + fn status_code(&self) -> StatusCode { + match self { + GetGroupError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + GetGroupError::BadRequest => StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn get_group( + user: AuthenticatedUser, + state: web::Data, + id: u64, +) -> Result<(), GetGroupError> { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(GetGroupError::DatabaseError), + }; + + Ok(()) +} + +#[utoipa::path( + get, + path = "", + responses( + (status = 200, body = GetGroupResultView), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ), + tag = "Groups", + security( + ("jwt" = []) + ) +)] +#[get("/")] +pub async fn get( + user: AuthenticatedUser, + state: web::Data, + id: web::Path, +) -> Result { + let result = get_group(user, state, id.into_inner()).await?; + Ok(HttpResponse::Ok().body(result)) +} diff --git a/src/endpoints/v1/groups/id/get/mod.rs b/src/endpoints/v1/groups/id/get/mod.rs new file mode 100644 index 0000000..cbeb677 --- /dev/null +++ b/src/endpoints/v1/groups/id/get/mod.rs @@ -0,0 +1,2 @@ +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/groups/id/get/view.rs b/src/endpoints/v1/groups/id/get/view.rs new file mode 100644 index 0000000..735f101 --- /dev/null +++ b/src/endpoints/v1/groups/id/get/view.rs @@ -0,0 +1,7 @@ +use crate::endpoints::v1::groups::get::view::Group; +use utoipa::ToSchema; + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct GetGroupResultView { + group: Group, +} diff --git a/src/endpoints/v1/groups/id/mod.rs b/src/endpoints/v1/groups/id/mod.rs new file mode 100644 index 0000000..b6f3f5a --- /dev/null +++ b/src/endpoints/v1/groups/id/mod.rs @@ -0,0 +1,15 @@ +mod delete; +pub mod doc; +mod get; +mod users; + +use actix_web::web; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/{group_id}") + .service(delete::endpoint::delete) + .service(get::endpoint::get) + .configure(users::config), + ); +} diff --git a/src/endpoints/v1/groups/id/users/doc.rs b/src/endpoints/v1/groups/id/users/doc.rs new file mode 100644 index 0000000..727bb06 --- /dev/null +++ b/src/endpoints/v1/groups/id/users/doc.rs @@ -0,0 +1,21 @@ +use super::get::endpoint::__path_get; +use super::id::doc::GroupsUsersIdDoc; +use super::post::endpoint::__path_post; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi( + paths(get, post), + components(schemas( + super::get::view::GetGroupUsersResultView, + super::post::view::PostUserGroupView + )) +)] +pub struct Doc; + +#[derive(OpenApi)] +#[openapi(nest( + (path = "/", api = Doc, tags = ["Groups"]), + (path = "/{user_id}", api = GroupsUsersIdDoc, tags = ["Groups"]), +))] +pub struct GroupsUsersDoc; diff --git a/src/endpoints/v1/groups/id/users/get/endpoint.rs b/src/endpoints/v1/groups/id/users/get/endpoint.rs new file mode 100644 index 0000000..c67839d --- /dev/null +++ b/src/endpoints/v1/groups/id/users/get/endpoint.rs @@ -0,0 +1,73 @@ +use crate::endpoints::v1::groups::id::users::get::view::GetGroupUsersResultView; +use actix_web::http::StatusCode; +use actix_web::{get, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; +use mairie360_api_lib::security::AuthenticatedUser; + +#[derive(Debug, Clone, PartialEq)] +enum GetUsersGroupError { + BadRequest, + DatabaseError, +} + +impl std::fmt::Display for GetUsersGroupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GetUsersGroupError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + GetUsersGroupError::BadRequest => { + write!(f, "Bad request.") + } + } + } +} + +impl ResponseError for GetUsersGroupError { + fn status_code(&self) -> StatusCode { + match self { + GetUsersGroupError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + GetUsersGroupError::BadRequest => StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn get_group_users( + state: web::Data, + group_id: i32, +) -> Result { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(GetUsersGroupError::DatabaseError), + }; + + Ok(GetGroupUsersResultView::new(vec![])) +} + +#[utoipa::path( + get, + path = "", + responses( + (status = 200, body = GetGroupUsersResultView), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ), + tag = "Groups", + security( + ("jwt" = []) + ) +)] +#[get("/")] +pub async fn get( + _: AuthenticatedUser, + state: web::Data, + group_id: web::Path, +) -> Result { + let result = get_group_users(state, group_id.into_inner()).await?; + Ok(HttpResponse::Ok().json(result)) +} diff --git a/src/endpoints/v1/groups/id/users/get/mod.rs b/src/endpoints/v1/groups/id/users/get/mod.rs new file mode 100644 index 0000000..cbeb677 --- /dev/null +++ b/src/endpoints/v1/groups/id/users/get/mod.rs @@ -0,0 +1,2 @@ +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/groups/id/users/get/view.rs b/src/endpoints/v1/groups/id/users/get/view.rs new file mode 100644 index 0000000..1c42bfb --- /dev/null +++ b/src/endpoints/v1/groups/id/users/get/view.rs @@ -0,0 +1,12 @@ +use utoipa::ToSchema; + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct GetGroupUsersResultView { + users: Vec, +} + +impl GetGroupUsersResultView { + pub fn new(users: Vec) -> Self { + Self { users } + } +} diff --git a/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs b/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs new file mode 100644 index 0000000..489e9f4 --- /dev/null +++ b/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs @@ -0,0 +1,74 @@ +use actix_web::http::StatusCode; +use actix_web::{delete, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; +use mairie360_api_lib::security::AuthenticatedUser; + +#[derive(Debug, Clone, PartialEq)] +enum AddAccessError { + BadRequest, + DatabaseError, +} + +impl std::fmt::Display for AddAccessError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AddAccessError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + AddAccessError::BadRequest => { + write!(f, "Bad request.") + } + } + } +} + +impl ResponseError for AddAccessError { + fn status_code(&self) -> StatusCode { + match self { + AddAccessError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + AddAccessError::BadRequest => StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn delete_user_from_group( + state: web::Data, + group_id: u64, + user_id: u64, +) -> Result<(), AddAccessError> { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(AddAccessError::DatabaseError), + }; + + Ok(()) +} + +#[utoipa::path( + delete, + path = "/", + responses( + (status = 204, description = "User deleted from group successfully"), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ), + tag = "Groups", + security( + ("jwt" = []) + ) +)] +#[delete("/")] +pub async fn delete( + _: AuthenticatedUser, + state: web::Data, + path: web::Path<(u64, u64)>, +) -> Result { + let (group_id, user_id) = path.into_inner(); + delete_user_from_group(state, group_id, user_id).await?; + Ok(HttpResponse::NoContent().finish()) +} diff --git a/src/endpoints/v1/groups/id/users/id/delete/mod.rs b/src/endpoints/v1/groups/id/users/id/delete/mod.rs new file mode 100644 index 0000000..657213d --- /dev/null +++ b/src/endpoints/v1/groups/id/users/id/delete/mod.rs @@ -0,0 +1 @@ +pub mod endpoint; diff --git a/src/endpoints/v1/groups/id/users/id/doc.rs b/src/endpoints/v1/groups/id/users/id/doc.rs new file mode 100644 index 0000000..ae16380 --- /dev/null +++ b/src/endpoints/v1/groups/id/users/id/doc.rs @@ -0,0 +1,6 @@ +use super::delete::endpoint::__path_delete; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi(paths(delete), components(schemas()))] +pub struct GroupsUsersIdDoc; diff --git a/src/endpoints/v1/groups/id/users/id/mod.rs b/src/endpoints/v1/groups/id/users/id/mod.rs new file mode 100644 index 0000000..6aa0094 --- /dev/null +++ b/src/endpoints/v1/groups/id/users/id/mod.rs @@ -0,0 +1,8 @@ +mod delete; +pub mod doc; + +use actix_web::web; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(web::scope("/{user_id}").service(delete::endpoint::delete)); +} diff --git a/src/endpoints/v1/groups/id/users/mod.rs b/src/endpoints/v1/groups/id/users/mod.rs new file mode 100644 index 0000000..9afd7b5 --- /dev/null +++ b/src/endpoints/v1/groups/id/users/mod.rs @@ -0,0 +1,15 @@ +pub mod doc; +mod get; +mod id; +mod post; + +use actix_web::web; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/users") + .configure(id::config) + .service(post::endpoint::post) + .service(get::endpoint::get), + ); +} diff --git a/src/endpoints/v1/groups/id/users/post/endpoint.rs b/src/endpoints/v1/groups/id/users/post/endpoint.rs new file mode 100644 index 0000000..cd3d0fa --- /dev/null +++ b/src/endpoints/v1/groups/id/users/post/endpoint.rs @@ -0,0 +1,74 @@ +use crate::endpoints::v1::groups::id::users::post::view::PostUserGroupView; +use actix_web::http::StatusCode; +use actix_web::{post, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; +use mairie360_api_lib::security::AuthenticatedUser; + +#[derive(Debug, Clone, PartialEq)] +enum PostUserGroupError { + BadRequest, + DatabaseError, +} + +impl std::fmt::Display for PostUserGroupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PostUserGroupError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + PostUserGroupError::BadRequest => { + write!(f, "Bad request.") + } + } + } +} + +impl ResponseError for PostUserGroupError { + fn status_code(&self) -> StatusCode { + match self { + PostUserGroupError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + PostUserGroupError::BadRequest => StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn add_user_to_group( + state: web::Data, + view: PostUserGroupView, +) -> Result<(), PostUserGroupError> { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(PostUserGroupError::DatabaseError), + }; + + Ok(()) +} + +#[utoipa::path( + post, + path = "", + request_body = PostUserGroupView, + responses( + (status = 200, description = "User added to group successfully"), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ), + tag = "Groups", + security( + ("jwt" = []) + ) +)] +#[post("/")] +pub async fn post( + _: AuthenticatedUser, + state: web::Data, + view: web::Json, +) -> Result { + add_user_to_group(state, view.into_inner()).await?; + Ok(HttpResponse::Ok().body("User added to group successfully")) +} diff --git a/src/endpoints/v1/groups/id/users/post/mod.rs b/src/endpoints/v1/groups/id/users/post/mod.rs new file mode 100644 index 0000000..cbeb677 --- /dev/null +++ b/src/endpoints/v1/groups/id/users/post/mod.rs @@ -0,0 +1,2 @@ +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/groups/id/users/post/view.rs b/src/endpoints/v1/groups/id/users/post/view.rs new file mode 100644 index 0000000..8b98dba --- /dev/null +++ b/src/endpoints/v1/groups/id/users/post/view.rs @@ -0,0 +1,21 @@ +use utoipa::ToSchema; + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct PostUserGroupView { + user_id: u64, + group_id: u64, +} + +impl PostUserGroupView { + pub fn new(user_id: u64, group_id: u64) -> Self { + Self { user_id, group_id } + } + + pub fn user_id(&self) -> u64 { + self.user_id + } + + pub fn group_id(&self) -> u64 { + self.group_id + } +} diff --git a/src/endpoints/v1/groups/mod.rs b/src/endpoints/v1/groups/mod.rs new file mode 100644 index 0000000..c333886 --- /dev/null +++ b/src/endpoints/v1/groups/mod.rs @@ -0,0 +1,15 @@ +pub mod doc; +mod get; +mod id; +mod post; + +use actix_web::web; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/groups") + .service(get::endpoint::get) + .service(post::endpoint::post) + .configure(id::config), + ); +} diff --git a/src/endpoints/v1/groups/post/endpoint.rs b/src/endpoints/v1/groups/post/endpoint.rs new file mode 100644 index 0000000..1132853 --- /dev/null +++ b/src/endpoints/v1/groups/post/endpoint.rs @@ -0,0 +1,75 @@ +use crate::endpoints::v1::groups::post::view::PostGroupView; +use actix_web::http::StatusCode; +use actix_web::{post, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; +use mairie360_api_lib::security::AuthenticatedUser; + +#[derive(Debug, Clone, PartialEq)] +enum PostGroupError { + BadRequest, + DatabaseError, +} + +impl std::fmt::Display for PostGroupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PostGroupError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + PostGroupError::BadRequest => { + write!(f, "Bad request.") + } + } + } +} + +impl ResponseError for PostGroupError { + fn status_code(&self) -> StatusCode { + match self { + PostGroupError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + PostGroupError::BadRequest => StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn create_group( + user: AuthenticatedUser, + state: web::Data, + view: PostGroupView, +) -> Result<(), PostGroupError> { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(PostGroupError::DatabaseError), + }; + + Ok(()) +} + +#[utoipa::path( + post, + path = "", + request_body = PostGroupView, + responses( + (status = 200, description = "Group created successfully"), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ), + tag = "Groups", + security( + ("jwt" = []) + ) +)] +#[post("/")] +pub async fn post( + user: AuthenticatedUser, + state: web::Data, + view: web::Json, +) -> Result { + create_group(user, state, view.into_inner()).await?; + Ok(HttpResponse::Ok().body("Group created successfully")) +} diff --git a/src/endpoints/v1/groups/post/mod.rs b/src/endpoints/v1/groups/post/mod.rs new file mode 100644 index 0000000..cbeb677 --- /dev/null +++ b/src/endpoints/v1/groups/post/mod.rs @@ -0,0 +1,2 @@ +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/groups/post/view.rs b/src/endpoints/v1/groups/post/view.rs new file mode 100644 index 0000000..5d69dbe --- /dev/null +++ b/src/endpoints/v1/groups/post/view.rs @@ -0,0 +1,24 @@ +use utoipa::ToSchema; + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct PostGroupView { + name: String, + description: String, +} + +impl PostGroupView { + pub fn new(name: &str, description: &str) -> Self { + Self { + name: name.to_string(), + description: description.to_string(), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn description(&self) -> &str { + &self.description + } +} diff --git a/src/endpoints/v1/mod.rs b/src/endpoints/v1/mod.rs index d9991ec..88811a8 100644 --- a/src/endpoints/v1/mod.rs +++ b/src/endpoints/v1/mod.rs @@ -1,6 +1,7 @@ pub mod admin; pub mod auth; pub mod doc; +pub mod groups; pub mod ressources; pub mod roles; pub mod sessions; @@ -13,6 +14,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { web::scope("/v1") .configure(admin::config) .configure(auth::config) + .configure(groups::config) .configure(roles::config) .configure(sessions::config) .configure(user::config), From 0b2605471bbb9b92dcb8f4ce8af09982ab187417 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Thu, 14 May 2026 15:11:18 +0800 Subject: [PATCH 09/16] feat: add groups database queries --- src/database/groups/add_user_to_group/mod.rs | 5 ++ .../groups/add_user_to_group/query.rs | 17 ++++++ src/database/groups/add_user_to_group/view.rs | 38 ++++++++++++ src/database/groups/create_group/mod.rs | 5 ++ src/database/groups/create_group/query.rs | 18 ++++++ src/database/groups/create_group/view.rs | 47 +++++++++++++++ src/database/groups/delete_group/mod.rs | 5 ++ src/database/groups/delete_group/query.rs | 16 +++++ src/database/groups/delete_group/view.rs | 29 ++++++++++ .../groups/delete_user_from_group/mod.rs | 5 ++ .../groups/delete_user_from_group/query.rs | 17 ++++++ .../groups/delete_user_from_group/view.rs | 41 +++++++++++++ src/database/groups/get_group/mod.rs | 6 ++ src/database/groups/get_group/query.rs | 13 +++++ src/database/groups/get_group/view.rs | 58 +++++++++++++++++++ src/database/groups/get_group_users/mod.rs | 5 ++ src/database/groups/get_group_users/query.rs | 16 +++++ src/database/groups/get_group_users/view.rs | 29 ++++++++++ src/database/groups/get_user_groups/mod.rs | 5 ++ src/database/groups/get_user_groups/query.rs | 17 ++++++ src/database/groups/get_user_groups/view.rs | 30 ++++++++++ src/database/groups/mod.rs | 7 +++ src/database/mod.rs | 1 + 23 files changed, 430 insertions(+) create mode 100644 src/database/groups/add_user_to_group/mod.rs create mode 100644 src/database/groups/add_user_to_group/query.rs create mode 100644 src/database/groups/add_user_to_group/view.rs create mode 100644 src/database/groups/create_group/mod.rs create mode 100644 src/database/groups/create_group/query.rs create mode 100644 src/database/groups/create_group/view.rs create mode 100644 src/database/groups/delete_group/mod.rs create mode 100644 src/database/groups/delete_group/query.rs create mode 100644 src/database/groups/delete_group/view.rs create mode 100644 src/database/groups/delete_user_from_group/mod.rs create mode 100644 src/database/groups/delete_user_from_group/query.rs create mode 100644 src/database/groups/delete_user_from_group/view.rs create mode 100644 src/database/groups/get_group/mod.rs create mode 100644 src/database/groups/get_group/query.rs create mode 100644 src/database/groups/get_group/view.rs create mode 100644 src/database/groups/get_group_users/mod.rs create mode 100644 src/database/groups/get_group_users/query.rs create mode 100644 src/database/groups/get_group_users/view.rs create mode 100644 src/database/groups/get_user_groups/mod.rs create mode 100644 src/database/groups/get_user_groups/query.rs create mode 100644 src/database/groups/get_user_groups/view.rs create mode 100644 src/database/groups/mod.rs diff --git a/src/database/groups/add_user_to_group/mod.rs b/src/database/groups/add_user_to_group/mod.rs new file mode 100644 index 0000000..777aec8 --- /dev/null +++ b/src/database/groups/add_user_to_group/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::add_user_to_group_query; + +mod view; +pub use view::AddUserToGroupQueryView; diff --git a/src/database/groups/add_user_to_group/query.rs b/src/database/groups/add_user_to_group/query.rs new file mode 100644 index 0000000..ea4987c --- /dev/null +++ b/src/database/groups/add_user_to_group/query.rs @@ -0,0 +1,17 @@ +use crate::database::groups::add_user_to_group::AddUserToGroupQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn add_user_to_group_query( + view: AddUserToGroupQueryView, + pool: PgPool, +) -> Result<(), DatabaseError> { + sqlx::query(&view.get_request()) + .bind(view.group_id() as i32) + .bind(view.user_id() as i32) + .execute(&pool) + .await?; + + Ok(()) +} diff --git a/src/database/groups/add_user_to_group/view.rs b/src/database/groups/add_user_to_group/view.rs new file mode 100644 index 0000000..6750e95 --- /dev/null +++ b/src/database/groups/add_user_to_group/view.rs @@ -0,0 +1,38 @@ +use std::fmt::Display; + +use mairie360_api_lib::database::db_interface::DatabaseQueryView; + +pub struct AddUserToGroupQueryView { + group_id: u64, + user_id: u64, +} + +impl AddUserToGroupQueryView { + pub fn new(group_id: u64, user_id: u64) -> Self { + Self { group_id, user_id } + } + + pub fn group_id(&self) -> u64 { + self.group_id + } + + pub fn user_id(&self) -> u64 { + self.user_id + } +} + +impl DatabaseQueryView for AddUserToGroupQueryView { + fn get_request(&self) -> String { + "INSERT INTO group_users (group_id, user_id) VALUES ($1, $2)".to_string() + } +} + +impl Display for AddUserToGroupQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "AddUserToGroupQueryView: group_id = {}, user_id = {}", + self.group_id, self.user_id + ) + } +} diff --git a/src/database/groups/create_group/mod.rs b/src/database/groups/create_group/mod.rs new file mode 100644 index 0000000..5c8fa8d --- /dev/null +++ b/src/database/groups/create_group/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::create_group_query; + +mod view; +pub use view::CreateGroupQueryView; diff --git a/src/database/groups/create_group/query.rs b/src/database/groups/create_group/query.rs new file mode 100644 index 0000000..4d02759 --- /dev/null +++ b/src/database/groups/create_group/query.rs @@ -0,0 +1,18 @@ +use crate::database::groups::create_group::view::CreateGroupQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn create_group_query( + view: CreateGroupQueryView, + pool: PgPool, +) -> Result { + let result: i32 = sqlx::query_scalar(&view.get_request()) + .bind(view.owner_id() as i32) + .bind(view.name()) + .bind(view.description()) + .fetch_one(&pool) + .await?; + + Ok(result) +} diff --git a/src/database/groups/create_group/view.rs b/src/database/groups/create_group/view.rs new file mode 100644 index 0000000..90d96bf --- /dev/null +++ b/src/database/groups/create_group/view.rs @@ -0,0 +1,47 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use std::fmt::Display; + +pub struct CreateGroupQueryView { + owner_id: u64, + name: String, + description: String, +} + +impl CreateGroupQueryView { + pub fn new(owner_id: u64, name: &str, description: &str) -> Self { + Self { + owner_id, + name: name.to_string(), + description: description.to_string(), + } + } + + pub fn owner_id(&self) -> u64 { + self.owner_id + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn description(&self) -> &str { + &self.description + } +} + +impl DatabaseQueryView for CreateGroupQueryView { + fn get_request(&self) -> String { + "INSERT INTO groups (owner_id, name, description) VALUES ($1, $2, $3) RETURNING id" + .to_string() + } +} + +impl Display for CreateGroupQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "CreateGroupQueryView: owner_id = {}, name = {}, description = {}", + self.owner_id, self.name, self.description + ) + } +} diff --git a/src/database/groups/delete_group/mod.rs b/src/database/groups/delete_group/mod.rs new file mode 100644 index 0000000..f8335ec --- /dev/null +++ b/src/database/groups/delete_group/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::delete_group_query; + +mod view; +pub use view::DeleteGroupQueryView; diff --git a/src/database/groups/delete_group/query.rs b/src/database/groups/delete_group/query.rs new file mode 100644 index 0000000..89b5dae --- /dev/null +++ b/src/database/groups/delete_group/query.rs @@ -0,0 +1,16 @@ +use crate::database::groups::delete_group::view::DeleteGroupQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn delete_group_query( + view: DeleteGroupQueryView, + pool: PgPool, +) -> Result<(), DatabaseError> { + sqlx::query(&view.get_request()) + .bind(view.group_id() as i32) + .execute(&pool) + .await?; + + Ok(()) +} diff --git a/src/database/groups/delete_group/view.rs b/src/database/groups/delete_group/view.rs new file mode 100644 index 0000000..b5bd07c --- /dev/null +++ b/src/database/groups/delete_group/view.rs @@ -0,0 +1,29 @@ +use std::fmt::Display; + +use mairie360_api_lib::database::db_interface::DatabaseQueryView; + +pub struct DeleteGroupQueryView { + group_id: u64, +} + +impl DeleteGroupQueryView { + pub fn new(group_id: u64) -> Self { + Self { group_id } + } + + pub fn group_id(&self) -> u64 { + self.group_id + } +} + +impl DatabaseQueryView for DeleteGroupQueryView { + fn get_request(&self) -> String { + "DELETE FROM groups WHERE id = $1".to_string() + } +} + +impl Display for DeleteGroupQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "DeleteGroupQueryView: group_id = {}", self.group_id) + } +} diff --git a/src/database/groups/delete_user_from_group/mod.rs b/src/database/groups/delete_user_from_group/mod.rs new file mode 100644 index 0000000..3c8bdcc --- /dev/null +++ b/src/database/groups/delete_user_from_group/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::delete_user_from_group_query; + +mod view; +pub use view::DeleteUserFromGroupQueryView; diff --git a/src/database/groups/delete_user_from_group/query.rs b/src/database/groups/delete_user_from_group/query.rs new file mode 100644 index 0000000..9b51e8c --- /dev/null +++ b/src/database/groups/delete_user_from_group/query.rs @@ -0,0 +1,17 @@ +use crate::database::groups::delete_user_from_group::DeleteUserFromGroupQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn delete_user_from_group_query( + view: DeleteUserFromGroupQueryView, + pool: PgPool, +) -> Result<(), DatabaseError> { + sqlx::query(&view.get_request()) + .bind(view.group_id() as i32) + .bind(view.user_id() as i32) + .execute(&pool) + .await?; + + Ok(()) +} diff --git a/src/database/groups/delete_user_from_group/view.rs b/src/database/groups/delete_user_from_group/view.rs new file mode 100644 index 0000000..22c4183 --- /dev/null +++ b/src/database/groups/delete_user_from_group/view.rs @@ -0,0 +1,41 @@ +use std::fmt::Display; + +use mairie360_api_lib::database::db_interface::DatabaseQueryView; + +pub struct DeleteUserFromGroupQueryView { + group_id: u64, + user_id: u64, +} + +impl DeleteUserFromGroupQueryView { + pub fn new(group_id: u64, user_id: u64) -> Self { + Self { + group_id, + user_id, + } + } + + pub fn group_id(&self) -> u64 { + self.group_id + } + + pub fn user_id(&self) -> u64 { + self.user_id + } +} + +impl DatabaseQueryView for DeleteUserFromGroupQueryView { + fn get_request(&self) -> String { + "DELETE FROM group_users WHERE group_id = $1 AND user_id = $2".to_string() + } +} + +impl Display for DeleteUserFromGroupQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "DeleteUserFromGroupQueryView: group_id = {}, user_id = {}", + self.group_id, self.user_id + ) + } +} diff --git a/src/database/groups/get_group/mod.rs b/src/database/groups/get_group/mod.rs new file mode 100644 index 0000000..a1f2176 --- /dev/null +++ b/src/database/groups/get_group/mod.rs @@ -0,0 +1,6 @@ +mod query; +pub use query::get_group; + +mod view; +pub use view::GetGroupsQuerView; +pub use view::Group; diff --git a/src/database/groups/get_group/query.rs b/src/database/groups/get_group/query.rs new file mode 100644 index 0000000..db2948f --- /dev/null +++ b/src/database/groups/get_group/query.rs @@ -0,0 +1,13 @@ +use crate::database::groups::get_group::view::{GetGroupsQuerView, Group}; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn get_group(view: GetGroupsQuerView, pool: PgPool) -> Result { + let result: Group = sqlx::query_as(&view.get_request()) + .bind(view.group_id() as i32) + .fetch_one(&pool) + .await?; + + Ok(result) +} diff --git a/src/database/groups/get_group/view.rs b/src/database/groups/get_group/view.rs new file mode 100644 index 0000000..1bec1de --- /dev/null +++ b/src/database/groups/get_group/view.rs @@ -0,0 +1,58 @@ +use std::fmt::Display; + +use mairie360_api_lib::database::db_interface::DatabaseQueryView; + +pub struct GetGroupsQuerView { + group_id: u64, +} + +impl GetGroupsQuerView { + pub fn new(group_id: u64) -> Self { + Self { group_id } + } + + pub fn group_id(&self) -> u64 { + self.group_id + } +} + +impl DatabaseQueryView for GetGroupsQuerView { + fn get_request(&self) -> String { + "SELECT * FROM groups WHERE id = $1".to_string() + } +} + +impl Display for GetGroupsQuerView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "GetGroups: group_id = {}", self.group_id) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, sqlx::FromRow)] +pub struct Group { + id: i32, + owner_id: i32, + name: String, + description: String, +} + +impl Group { + pub fn new(id: i32, name: &str, owner_id: i32, description: &str) -> Self { + Self { + id, + name: name.to_string(), + owner_id, + description: description.to_string(), + } + } +} + +impl Display for Group { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Group: id = {}, name = {}, owner_id = {}, description = {}", + self.id, self.name, self.owner_id, self.description, + ) + } +} diff --git a/src/database/groups/get_group_users/mod.rs b/src/database/groups/get_group_users/mod.rs new file mode 100644 index 0000000..077156f --- /dev/null +++ b/src/database/groups/get_group_users/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::get_group_users_query; + +mod view; +pub use view::GetGroupUsersQueryView; diff --git a/src/database/groups/get_group_users/query.rs b/src/database/groups/get_group_users/query.rs new file mode 100644 index 0000000..5ebf45a --- /dev/null +++ b/src/database/groups/get_group_users/query.rs @@ -0,0 +1,16 @@ +use crate::database::groups::get_group_users::view::GetGroupUsersQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn get_group_users_query( + view: GetGroupUsersQueryView, + pool: PgPool, +) -> Result, DatabaseError> { + let result: Vec = sqlx::query_scalar(&view.get_request()) + .bind(view.group_id() as i32) + .fetch_all(&pool) + .await?; + + Ok(result) +} diff --git a/src/database/groups/get_group_users/view.rs b/src/database/groups/get_group_users/view.rs new file mode 100644 index 0000000..c25e707 --- /dev/null +++ b/src/database/groups/get_group_users/view.rs @@ -0,0 +1,29 @@ +use std::fmt::Display; + +use mairie360_api_lib::database::db_interface::DatabaseQueryView; + +pub struct GetGroupUsersQueryView { + group_id: u64, +} + +impl GetGroupUsersQueryView { + pub fn new(group_id: u64) -> Self { + Self { group_id } + } + + pub fn group_id(&self) -> u64 { + self.group_id + } +} + +impl DatabaseQueryView for GetGroupUsersQueryView { + fn get_request(&self) -> String { + "SELECT user_id FROM group_users WHERE group_id = $1".to_string() + } +} + +impl Display for GetGroupUsersQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "GetGroupUsersQueryView: group_id = {}", self.group_id) + } +} diff --git a/src/database/groups/get_user_groups/mod.rs b/src/database/groups/get_user_groups/mod.rs new file mode 100644 index 0000000..999c74c --- /dev/null +++ b/src/database/groups/get_user_groups/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::get_user_groups; + +mod view; +pub use view::GetUserGroupsQuerView; diff --git a/src/database/groups/get_user_groups/query.rs b/src/database/groups/get_user_groups/query.rs new file mode 100644 index 0000000..b74d91f --- /dev/null +++ b/src/database/groups/get_user_groups/query.rs @@ -0,0 +1,17 @@ +use crate::database::groups::get_group::Group; +use crate::database::groups::get_user_groups::view::GetUserGroupsQuerView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn get_user_groups( + view: GetUserGroupsQuerView, + pool: PgPool, +) -> Result, DatabaseError> { + let result: Vec = sqlx::query_as(&view.get_request()) + .bind(view.user_id() as i32) + .fetch_all(&pool) + .await?; + + Ok(result) +} diff --git a/src/database/groups/get_user_groups/view.rs b/src/database/groups/get_user_groups/view.rs new file mode 100644 index 0000000..eecab1c --- /dev/null +++ b/src/database/groups/get_user_groups/view.rs @@ -0,0 +1,30 @@ +use std::fmt::Display; + +use mairie360_api_lib::database::db_interface::DatabaseQueryView; + +pub struct GetUserGroupsQuerView { + user_id: u64, +} + +impl GetUserGroupsQuerView { + pub fn new(user_id: u64) -> Self { + Self { user_id } + } + + pub fn user_id(&self) -> u64 { + self.user_id + } +} + +impl DatabaseQueryView for GetUserGroupsQuerView { + fn get_request(&self) -> String { + "SELECT * FROM groups WHERE id = (Select group_id FROM group_users WHERE user_id = $1)" + .to_string() + } +} + +impl Display for GetUserGroupsQuerView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "GetGroups: user_id = {}", self.user_id) + } +} diff --git a/src/database/groups/mod.rs b/src/database/groups/mod.rs new file mode 100644 index 0000000..5f031ef --- /dev/null +++ b/src/database/groups/mod.rs @@ -0,0 +1,7 @@ +pub mod add_user_to_group; +pub mod create_group; +pub mod delete_group; +pub mod delete_user_from_group; +pub mod get_group; +pub mod get_group_users; +pub mod get_user_groups; diff --git a/src/database/mod.rs b/src/database/mod.rs index af9d34e..5d4dfe7 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod groups; pub mod ressources; pub mod rights; pub mod roles; From f5a8b596f7dfee715999d8a2cfb3c10365c36978 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Mon, 18 May 2026 10:44:17 +0800 Subject: [PATCH 10/16] feat: add group queries tests --- Cargo.lock | 4 +- Cargo.toml | 2 +- src/database/groups/add_user_to_group/view.rs | 3 +- src/database/groups/create_group/view.rs | 1 + src/database/groups/get_group/mod.rs | 4 +- src/database/groups/get_group/query.rs | 4 +- src/database/groups/get_group/view.rs | 20 +++-- tests/queries/groups/add_user_to_group.rs | 85 +++++++++++++++++++ tests/queries/groups/create_group.rs | 37 ++++++++ tests/queries/groups/delete_group.rs | 32 +++++++ .../queries/groups/delete_user_from_group.rs | 77 +++++++++++++++++ tests/queries/groups/get_group.rs | 42 +++++++++ tests/queries/groups/get_group_users.rs | 72 ++++++++++++++++ tests/queries/groups/get_user_groups.rs | 52 ++++++++++++ tests/queries/groups/mod.rs | 7 ++ tests/queries/mod.rs | 1 + 16 files changed, 427 insertions(+), 16 deletions(-) create mode 100644 tests/queries/groups/add_user_to_group.rs create mode 100644 tests/queries/groups/create_group.rs create mode 100644 tests/queries/groups/delete_group.rs create mode 100644 tests/queries/groups/delete_user_from_group.rs create mode 100644 tests/queries/groups/get_group.rs create mode 100644 tests/queries/groups/get_group_users.rs create mode 100644 tests/queries/groups/get_user_groups.rs create mode 100644 tests/queries/groups/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 76f9366..cc18d3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1942,9 +1942,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "mairie360_api_lib" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94515afc879a7d47d82c0d28d54c85d60592c1882f62aa61d3589f74d363f748" +checksum = "6eae04cdd94f636106dd46cdbe6e426b9766c66f26bc0f090e2c10e18795ad4e" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index a32cf60..cc5aa7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ async-trait = "0.1.89" base64 = "0.22.1" chrono = { version = "0.4", features = ["serde"] } futures-util = "0.3" -mairie360_api_lib = "0.5.0" +mairie360_api_lib = "0.6.0" rand = "0.9" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" diff --git a/src/database/groups/add_user_to_group/view.rs b/src/database/groups/add_user_to_group/view.rs index 6750e95..cc42861 100644 --- a/src/database/groups/add_user_to_group/view.rs +++ b/src/database/groups/add_user_to_group/view.rs @@ -23,7 +23,8 @@ impl AddUserToGroupQueryView { impl DatabaseQueryView for AddUserToGroupQueryView { fn get_request(&self) -> String { - "INSERT INTO group_users (group_id, user_id) VALUES ($1, $2)".to_string() + "INSERT INTO group_users (group_id, user_id) VALUES ($1, $2)" + .to_string() } } diff --git a/src/database/groups/create_group/view.rs b/src/database/groups/create_group/view.rs index 90d96bf..a10a38f 100644 --- a/src/database/groups/create_group/view.rs +++ b/src/database/groups/create_group/view.rs @@ -1,6 +1,7 @@ use mairie360_api_lib::database::db_interface::DatabaseQueryView; use std::fmt::Display; +#[derive(Debug, Clone)] pub struct CreateGroupQueryView { owner_id: u64, name: String, diff --git a/src/database/groups/get_group/mod.rs b/src/database/groups/get_group/mod.rs index a1f2176..d74bac7 100644 --- a/src/database/groups/get_group/mod.rs +++ b/src/database/groups/get_group/mod.rs @@ -1,6 +1,6 @@ mod query; -pub use query::get_group; +pub use query::get_group_query; mod view; -pub use view::GetGroupsQuerView; +pub use view::GetGroupQuerView; pub use view::Group; diff --git a/src/database/groups/get_group/query.rs b/src/database/groups/get_group/query.rs index db2948f..61eb340 100644 --- a/src/database/groups/get_group/query.rs +++ b/src/database/groups/get_group/query.rs @@ -1,9 +1,9 @@ -use crate::database::groups::get_group::view::{GetGroupsQuerView, Group}; +use crate::database::groups::get_group::view::{GetGroupQuerView, Group}; use mairie360_api_lib::database::db_interface::DatabaseQueryView; use mairie360_api_lib::database::errors::DatabaseError; use sqlx::PgPool; -pub async fn get_group(view: GetGroupsQuerView, pool: PgPool) -> Result { +pub async fn get_group_query(view: GetGroupQuerView, pool: PgPool) -> Result { let result: Group = sqlx::query_as(&view.get_request()) .bind(view.group_id() as i32) .fetch_one(&pool) diff --git a/src/database/groups/get_group/view.rs b/src/database/groups/get_group/view.rs index 1bec1de..66c6803 100644 --- a/src/database/groups/get_group/view.rs +++ b/src/database/groups/get_group/view.rs @@ -2,11 +2,11 @@ use std::fmt::Display; use mairie360_api_lib::database::db_interface::DatabaseQueryView; -pub struct GetGroupsQuerView { +pub struct GetGroupQuerView { group_id: u64, } -impl GetGroupsQuerView { +impl GetGroupQuerView { pub fn new(group_id: u64) -> Self { Self { group_id } } @@ -16,13 +16,13 @@ impl GetGroupsQuerView { } } -impl DatabaseQueryView for GetGroupsQuerView { +impl DatabaseQueryView for GetGroupQuerView { fn get_request(&self) -> String { "SELECT * FROM groups WHERE id = $1".to_string() } } -impl Display for GetGroupsQuerView { +impl Display for GetGroupQuerView { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "GetGroups: group_id = {}", self.group_id) } @@ -33,25 +33,29 @@ pub struct Group { id: i32, owner_id: i32, name: String, - description: String, + description: Option, } impl Group { - pub fn new(id: i32, name: &str, owner_id: i32, description: &str) -> Self { + pub fn new(id: i32, name: &str, owner_id: i32, description: Option<&str>) -> Self { Self { id, name: name.to_string(), owner_id, - description: description.to_string(), + description: description.map(|d| d.to_string()), } } + + pub fn id(&self) -> i32 { + self.id + } } impl Display for Group { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "Group: id = {}, name = {}, owner_id = {}, description = {}", + "Group: id = {}, name = {}, owner_id = {}, description = {:?}", self.id, self.name, self.owner_id, self.description, ) } diff --git a/tests/queries/groups/add_user_to_group.rs b/tests/queries/groups/add_user_to_group.rs new file mode 100644 index 0000000..b1710b6 --- /dev/null +++ b/tests/queries/groups/add_user_to_group.rs @@ -0,0 +1,85 @@ +use crate::common::get_pool; +use core_api::database::groups::{ + add_user_to_group::{add_user_to_group_query, AddUserToGroupQueryView}, + create_group::{create_group_query, CreateGroupQueryView}, +}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn add_user_to_group_success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = CreateGroupQueryView::new( + 1, + "add_user_to_group_name_success", + "add_user_to_group_description_success", + ); + let result = create_group_query(view, pool.clone()).await.unwrap(); + + let view = AddUserToGroupQueryView::new(result as u64, 2); + let result = add_user_to_group_query(view, pool).await; + assert!(result.is_ok(), "add_user_to_group_success failed: {:?}", result); +} + +#[tokio::test] +#[serial] +async fn add_user_to_group_duplicate_user() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = CreateGroupQueryView::new( + 1, + "add_user_to_group_duplicate_user_name", + "add_user_to_group_duplicate_user_description", + ); + let id = create_group_query(view, pool.clone()).await.unwrap(); + + let view = AddUserToGroupQueryView::new(id as u64, 2); + let _ = add_user_to_group_query(view, pool.clone()).await; + let view = AddUserToGroupQueryView::new(id as u64, 2); + let result = add_user_to_group_query(view, pool.clone()).await; + assert!(result.is_err()); +} + +#[tokio::test] +#[serial] +async fn add_user_to_group_unknow_user() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = CreateGroupQueryView::new( + 1, + "add_user_to_group_unknow_user_name", + "add_user_to_group_unknow_user_description", + ); + let result = create_group_query(view, pool.clone()).await.unwrap(); + + let view = AddUserToGroupQueryView::new(result as u64, 999); + let result = add_user_to_group_query(view, pool).await; + assert!(result.is_err()); +} + +#[tokio::test] +#[serial] +async fn add_user_to_group_unknow_group() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = AddUserToGroupQueryView::new(999, 2); + let result = add_user_to_group_query(view, pool).await; + assert!(result.is_err()); +} + +#[tokio::test] +#[serial] +async fn add_user_to_group_unknow_user_and_group() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = AddUserToGroupQueryView::new(999, 999); + let result = add_user_to_group_query(view, pool).await; + assert!(result.is_err()); +} diff --git a/tests/queries/groups/create_group.rs b/tests/queries/groups/create_group.rs new file mode 100644 index 0000000..dd8dc38 --- /dev/null +++ b/tests/queries/groups/create_group.rs @@ -0,0 +1,37 @@ +use crate::common::get_pool; +use core_api::database::groups::create_group::{create_group_query, CreateGroupQueryView}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn create_group_success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = CreateGroupQueryView::new( + 1, + "create_group_name_success", + "create_group_description_success", + ); + let result = create_group_query(view, pool).await; + assert!(result.is_ok()); +} + +#[tokio::test] +#[serial] +async fn create_group_duplicate_name() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = CreateGroupQueryView::new( + 1, + "create_group_name_duplicate", + "create_group_description_duplicate", + ); + let result = create_group_query(view.clone(), pool.clone()).await; + assert!(result.is_ok()); + + let result = create_group_query(view, pool).await; + assert!(result.is_err()); +} diff --git a/tests/queries/groups/delete_group.rs b/tests/queries/groups/delete_group.rs new file mode 100644 index 0000000..a786c6d --- /dev/null +++ b/tests/queries/groups/delete_group.rs @@ -0,0 +1,32 @@ +use crate::common::get_pool; +use core_api::database::groups::create_group::{create_group_query, CreateGroupQueryView}; +use core_api::database::groups::delete_group::{delete_group_query, DeleteGroupQueryView}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn delete_group_success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = CreateGroupQueryView::new( + 1, + "delete_group_success_name", + "delete_group_success_description", + ); + let id = create_group_query(view, pool.clone()).await.unwrap(); + let view = DeleteGroupQueryView::new(id as u64); + let result = delete_group_query(view, pool).await; + assert!(result.is_ok(), "result should be Ok, got: {:?}", result); +} + +#[tokio::test] +#[serial] +async fn delete_group_bad_group_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + let view = DeleteGroupQueryView::new(999); + let result = delete_group_query(view, pool).await; + assert!(result.is_ok(), "result should be Ok, got: {:?}", result); +} diff --git a/tests/queries/groups/delete_user_from_group.rs b/tests/queries/groups/delete_user_from_group.rs new file mode 100644 index 0000000..e6fed5a --- /dev/null +++ b/tests/queries/groups/delete_user_from_group.rs @@ -0,0 +1,77 @@ +use crate::common::get_pool; +use core_api::database::groups::{ + add_user_to_group::{add_user_to_group_query, AddUserToGroupQueryView}, + create_group::{create_group_query, CreateGroupQueryView}, + delete_user_from_group::{delete_user_from_group_query, DeleteUserFromGroupQueryView}, +}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn delete_user_to_group_success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = CreateGroupQueryView::new( + 1, + "delete_user_to_group_name_success", + "delete_user_to_group_description_success", + ); + let result = create_group_query(view, pool.clone()).await.unwrap(); + + let view = AddUserToGroupQueryView::new(result as u64, 2); + let _ = add_user_to_group_query(view, pool.clone()).await; + let view = DeleteUserFromGroupQueryView::new(result as u64, 2); + let result = delete_user_from_group_query(view, pool).await; + assert!( + result.is_ok(), + "delete_user_from_group_query should succeed, {result:?}", + result = result, + ); +} + +#[tokio::test] +#[serial] +async fn delete_user_to_group_unknow_group() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = DeleteUserFromGroupQueryView::new(999, 2); + let result = delete_user_from_group_query(view, pool).await; + assert!( + result.is_ok(), + "delete_user_from_group_query should succeed, {result:?}", + result = result, + ); +} + +#[tokio::test] +#[serial] +async fn delete_user_to_group_unknow_user() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = DeleteUserFromGroupQueryView::new(1, 999); + let result = delete_user_from_group_query(view, pool).await; + assert!( + result.is_ok(), + "delete_user_from_group_query should succeed, {result:?}", + result = result, + ); +} + +#[tokio::test] +#[serial] +async fn delete_user_to_group_unknow_user_and_group() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = DeleteUserFromGroupQueryView::new(999, 999); + let result = delete_user_from_group_query(view, pool).await; + assert!( + result.is_ok(), + "delete_user_from_group_query should succeed, {result:?}", + result = result, + ); +} diff --git a/tests/queries/groups/get_group.rs b/tests/queries/groups/get_group.rs new file mode 100644 index 0000000..fe6d6c5 --- /dev/null +++ b/tests/queries/groups/get_group.rs @@ -0,0 +1,42 @@ +use crate::common::get_pool; +use core_api::database::groups::create_group::{create_group_query, CreateGroupQueryView}; +use core_api::database::groups::get_group::{get_group_query, GetGroupQuerView, Group}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn get_group_success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = + CreateGroupQueryView::new(1, "get_group_success_name", "get_group_success_description"); + let id = create_group_query(view, pool.clone()).await.unwrap(); + let group = Group::new( + id, + "get_group_success_name", + 1, + Some("get_group_success_description"), + ); + let view = GetGroupQuerView::new(id as u64); + let result = get_group_query(view, pool.clone()).await; + assert!(result.is_ok(), "result should be Ok, got: {:?}", result); + let result = result.unwrap(); + assert_eq!( + result, group, + "result: {:#?}\nexpected: {:#?}", + result, group + ); +} + +#[tokio::test] +#[serial] +async fn get_group_bad_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = GetGroupQuerView::new(0); + let result = get_group_query(view, pool.clone()).await; + assert!(result.is_err(), "result should be Err, got: {:?}", result); +} diff --git a/tests/queries/groups/get_group_users.rs b/tests/queries/groups/get_group_users.rs new file mode 100644 index 0000000..51e6764 --- /dev/null +++ b/tests/queries/groups/get_group_users.rs @@ -0,0 +1,72 @@ +use crate::common::get_pool; +use core_api::database::groups::{ + get_group::Group, + get_group_users::{get_group_users_query, GetGroupUsersQueryView}, + get_user_groups::{get_user_groups, GetUserGroupsQuerView}, +}; +use mairie360_api_lib::test_setup::queries_setup::{get_shared_db, GROUP_OWNER_ID}; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn get_group_users_success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = GetUserGroupsQuerView::new(*GROUP_OWNER_ID.get().unwrap() as u64); + let result: Vec = get_user_groups(view, pool.clone()).await.unwrap(); + + let view = GetGroupUsersQueryView::new(result[0].id() as u64); + let result = get_group_users_query(view, pool).await; + + assert!( + result.clone().is_ok(), + "Result should be Ok, got {:?}", + result.clone() + ); + let result = result.unwrap(); + + assert!( + !result.clone().is_empty(), + "Result should not be empty, got {:?}", + result + ); + + assert_eq!( + result.len(), + 1, + "Result should have 1 element, got {:?}", + result + ); + + assert_eq!( + result[0], + *GROUP_OWNER_ID.get().unwrap(), + "Result should have the owner user_id: {}, got {}", + *GROUP_OWNER_ID.get().unwrap(), + result[0] + ); +} + +#[tokio::test] +#[serial] +async fn get_group_users_bad_group_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = GetGroupUsersQueryView::new(999); + let result = get_group_users_query(view, pool).await; + + assert!( + result.clone().is_ok(), + "Result should be Ok, got {:?}", + result.clone() + ); + let result = result.unwrap(); + + assert!( + result.clone().is_empty(), + "Result should be empty, got {:?}", + result + ); +} diff --git a/tests/queries/groups/get_user_groups.rs b/tests/queries/groups/get_user_groups.rs new file mode 100644 index 0000000..79887b8 --- /dev/null +++ b/tests/queries/groups/get_user_groups.rs @@ -0,0 +1,52 @@ +use crate::common::get_pool; +use core_api::database::groups::get_user_groups::{get_user_groups, GetUserGroupsQuerView}; +use mairie360_api_lib::test_setup::queries_setup::{get_shared_db, GROUP_OWNER_ID}; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn get_user_groups_success() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = GetUserGroupsQuerView::new(*GROUP_OWNER_ID.get().unwrap() as u64); + + let result = get_user_groups(view, pool).await; + assert!(result.clone().is_ok(), "{:?}", result.clone()); + assert!(!result.clone().unwrap().is_empty(), "{:?}", result); +} + +#[tokio::test] +#[serial] +async fn get_user_groups_without_groups() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = GetUserGroupsQuerView::new(3); + let result = get_user_groups(view, pool).await; + assert!(result.clone().is_ok(), "{:?}", result.clone()); + assert!(result.clone().unwrap().is_empty(), "{:?}", result); +} + +#[tokio::test] +#[serial] +async fn get_groups_bad_user_id() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = GetUserGroupsQuerView::new(999); + let result = get_user_groups(view, pool).await; + assert!(result.clone().is_ok(), "{:?}", result.clone()); + assert!(result.clone().unwrap().is_empty(), "{:?}", result); +} + +// #[tokio::test] +// #[serial] +// async fn get_groups_bad_user_id() { +// let (_container, host) = get_shared_db().await; +// let pool = get_pool(host.to_string()).await; + +// let view = GetUserGroupsQuerView::new(999); +// let result = get_user_groups(view, pool).await; +// assert!(result.is_err(), "{:?}", result); +// } diff --git a/tests/queries/groups/mod.rs b/tests/queries/groups/mod.rs new file mode 100644 index 0000000..5f031ef --- /dev/null +++ b/tests/queries/groups/mod.rs @@ -0,0 +1,7 @@ +pub mod add_user_to_group; +pub mod create_group; +pub mod delete_group; +pub mod delete_user_from_group; +pub mod get_group; +pub mod get_group_users; +pub mod get_user_groups; diff --git a/tests/queries/mod.rs b/tests/queries/mod.rs index 3e2cdea..04ae763 100644 --- a/tests/queries/mod.rs +++ b/tests/queries/mod.rs @@ -1,4 +1,5 @@ mod auth; +mod groups; mod ressources; mod rights; mod roles; From c4a8bc4e99ce19f1f445a065416eba0d67322fb2 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Mon, 18 May 2026 13:09:19 +0800 Subject: [PATCH 11/16] feat: add database group check --- src/database/groups/does_group_exist/mod.rs | 5 + src/database/groups/does_group_exist/query.rs | 17 +++ src/database/groups/does_group_exist/view.rs | 28 +++++ src/database/groups/get_group/view.rs | 5 +- src/database/groups/is_user_member/mod.rs | 5 + src/database/groups/is_user_member/query.rs | 17 +++ src/database/groups/is_user_member/view.rs | 38 ++++++ src/database/groups/mod.rs | 2 + src/endpoints/v1/groups/doc.rs | 1 + src/endpoints/v1/groups/get/endpoint.rs | 11 +- src/endpoints/v1/groups/get/view.rs | 21 ++-- src/endpoints/v1/groups/id/delete/endpoint.rs | 17 +-- src/endpoints/v1/groups/id/get/endpoint.rs | 17 ++- src/endpoints/v1/groups/id/get/view.rs | 8 +- .../v1/groups/id/users/get/endpoint.rs | 8 +- src/endpoints/v1/groups/id/users/get/view.rs | 6 + .../v1/groups/id/users/id/delete/endpoint.rs | 9 ++ .../v1/groups/id/users/post/endpoint.rs | 8 ++ src/endpoints/v1/groups/id/users/post/view.rs | 4 - src/endpoints/v1/groups/post/endpoint.rs | 16 ++- src/endpoints/v1/groups/post/view.rs | 18 +-- tests/queries/groups/does_group_exist.rs | 55 ++++++++ tests/queries/groups/is_user_member.rs | 118 ++++++++++++++++++ tests/queries/groups/mod.rs | 2 + 24 files changed, 393 insertions(+), 43 deletions(-) create mode 100644 src/database/groups/does_group_exist/mod.rs create mode 100644 src/database/groups/does_group_exist/query.rs create mode 100644 src/database/groups/does_group_exist/view.rs create mode 100644 src/database/groups/is_user_member/mod.rs create mode 100644 src/database/groups/is_user_member/query.rs create mode 100644 src/database/groups/is_user_member/view.rs create mode 100644 tests/queries/groups/does_group_exist.rs create mode 100644 tests/queries/groups/is_user_member.rs diff --git a/src/database/groups/does_group_exist/mod.rs b/src/database/groups/does_group_exist/mod.rs new file mode 100644 index 0000000..bc5c7a1 --- /dev/null +++ b/src/database/groups/does_group_exist/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::does_group_exist_query; + +mod view; +pub use view::DoesGroupExistQuerView; diff --git a/src/database/groups/does_group_exist/query.rs b/src/database/groups/does_group_exist/query.rs new file mode 100644 index 0000000..b73030b --- /dev/null +++ b/src/database/groups/does_group_exist/query.rs @@ -0,0 +1,17 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +use crate::database::groups::does_group_exist::DoesGroupExistQuerView; + +pub async fn does_group_exist_query( + view: DoesGroupExistQuerView, + pool: PgPool, +) -> Result { + let result: bool = sqlx::query_scalar::<_, bool>(&view.get_request()) + .bind(view.group_id() as i32) + .fetch_one(&pool) + .await?; + + Ok(result) +} diff --git a/src/database/groups/does_group_exist/view.rs b/src/database/groups/does_group_exist/view.rs new file mode 100644 index 0000000..0e3e70b --- /dev/null +++ b/src/database/groups/does_group_exist/view.rs @@ -0,0 +1,28 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use std::fmt::Display; + +pub struct DoesGroupExistQuerView { + group_id: u64, +} + +impl DoesGroupExistQuerView { + pub fn new(group_id: u64) -> Self { + Self { group_id } + } + + pub fn group_id(&self) -> u64 { + self.group_id + } +} + +impl DatabaseQueryView for DoesGroupExistQuerView { + fn get_request(&self) -> String { + "SELECT EXISTS(SELECT 1 FROM groups WHERE id = $1)".to_string() + } +} + +impl Display for DoesGroupExistQuerView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "DoesGroupExists: group_id = {}", self.group_id) + } +} diff --git a/src/database/groups/get_group/view.rs b/src/database/groups/get_group/view.rs index 66c6803..386d2ac 100644 --- a/src/database/groups/get_group/view.rs +++ b/src/database/groups/get_group/view.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use utoipa::ToSchema; pub struct GetGroupQuerView { group_id: u64, @@ -28,7 +29,9 @@ impl Display for GetGroupQuerView { } } -#[derive(Debug, Clone, PartialEq, Eq, sqlx::FromRow)] +#[derive( + Debug, Clone, PartialEq, Eq, sqlx::FromRow, serde::Deserialize, serde::Serialize, ToSchema, +)] pub struct Group { id: i32, owner_id: i32, diff --git a/src/database/groups/is_user_member/mod.rs b/src/database/groups/is_user_member/mod.rs new file mode 100644 index 0000000..274aafc --- /dev/null +++ b/src/database/groups/is_user_member/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::is_user_member_query; + +mod view; +pub use view::IsUserMemberQueryView; diff --git a/src/database/groups/is_user_member/query.rs b/src/database/groups/is_user_member/query.rs new file mode 100644 index 0000000..24431fd --- /dev/null +++ b/src/database/groups/is_user_member/query.rs @@ -0,0 +1,17 @@ +use crate::database::groups::is_user_member::view::IsUserMemberQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn is_user_member_query( + view: IsUserMemberQueryView, + pool: PgPool, +) -> Result { + let result = sqlx::query_scalar::<_, bool>(&view.get_request()) + .bind(view.group_id() as i32) + .bind(view.user_id() as i32) + .fetch_one(&pool) + .await?; + + Ok(result) +} diff --git a/src/database/groups/is_user_member/view.rs b/src/database/groups/is_user_member/view.rs new file mode 100644 index 0000000..6b09883 --- /dev/null +++ b/src/database/groups/is_user_member/view.rs @@ -0,0 +1,38 @@ +use std::fmt::Display; + +use mairie360_api_lib::database::db_interface::DatabaseQueryView; + +pub struct IsUserMemberQueryView { + group_id: u64, + user_id: u64, +} + +impl IsUserMemberQueryView { + pub fn new(group_id: u64, user_id: u64) -> Self { + Self { group_id, user_id } + } + + pub fn group_id(&self) -> u64 { + self.group_id + } + + pub fn user_id(&self) -> u64 { + self.user_id + } +} + +impl DatabaseQueryView for IsUserMemberQueryView { + fn get_request(&self) -> String { + "SELECT EXISTS (SELECT * FROM group_users WHERE group_id = $1 AND user_id = $2)".to_string() + } +} + +impl Display for IsUserMemberQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "IsUserMember: group_id = {}, user_id = {}", + self.group_id, self.user_id + ) + } +} diff --git a/src/database/groups/mod.rs b/src/database/groups/mod.rs index 5f031ef..35ce623 100644 --- a/src/database/groups/mod.rs +++ b/src/database/groups/mod.rs @@ -2,6 +2,8 @@ pub mod add_user_to_group; pub mod create_group; pub mod delete_group; pub mod delete_user_from_group; +pub mod does_group_exist; pub mod get_group; pub mod get_group_users; pub mod get_user_groups; +pub mod is_user_member; diff --git a/src/endpoints/v1/groups/doc.rs b/src/endpoints/v1/groups/doc.rs index 053be4e..181ee68 100644 --- a/src/endpoints/v1/groups/doc.rs +++ b/src/endpoints/v1/groups/doc.rs @@ -17,6 +17,7 @@ pub struct GroupsDoc; components(schemas( super::get::view::GetGroupsResultView, super::post::view::PostGroupView, + super::post::view::PostGroupResultView, )) )] struct Doc; diff --git a/src/endpoints/v1/groups/get/endpoint.rs b/src/endpoints/v1/groups/get/endpoint.rs index e2b66c9..9a31f4c 100644 --- a/src/endpoints/v1/groups/get/endpoint.rs +++ b/src/endpoints/v1/groups/get/endpoint.rs @@ -1,3 +1,4 @@ +use crate::database::groups::get_user_groups::{get_user_groups, GetUserGroupsQuerView}; use crate::endpoints::v1::groups::get::view::GetGroupsResultView; use actix_web::http::StatusCode; use actix_web::{get, web, HttpResponse, Responder, ResponseError}; @@ -39,13 +40,17 @@ impl ResponseError for GetGroupsError { async fn get_groups( user: AuthenticatedUser, state: web::Data, -) -> Result<(), GetGroupsError> { +) -> Result { let pool = match state.db_pool.clone() { Some(pool) => pool, None => return Err(GetGroupsError::DatabaseError), }; - Ok(()) + let groups = get_user_groups(GetUserGroupsQuerView::new(user.id), pool) + .await + .map_err(|_| GetGroupsError::BadRequest)?; + + Ok(groups.into()) } #[utoipa::path( @@ -68,5 +73,5 @@ pub async fn get( state: web::Data, ) -> Result { let result = get_groups(user, state).await?; - Ok(HttpResponse::Ok().body(result)) + Ok(HttpResponse::Ok().json(result)) } diff --git a/src/endpoints/v1/groups/get/view.rs b/src/endpoints/v1/groups/get/view.rs index de62f0c..3b8dc6e 100644 --- a/src/endpoints/v1/groups/get/view.rs +++ b/src/endpoints/v1/groups/get/view.rs @@ -1,14 +1,19 @@ +use crate::database::groups::get_group::Group; use utoipa::ToSchema; -#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] -pub struct Group { - id: u64, - owner_id: u64, - name: String, - description: String, -} - #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GetGroupsResultView { groups: Vec, } + +impl GetGroupsResultView { + pub fn new(groups: Vec) -> Self { + Self { groups } + } +} + +impl From> for GetGroupsResultView { + fn from(groups: Vec) -> Self { + Self::new(groups) + } +} diff --git a/src/endpoints/v1/groups/id/delete/endpoint.rs b/src/endpoints/v1/groups/id/delete/endpoint.rs index 2c7c6d8..87f43ed 100644 --- a/src/endpoints/v1/groups/id/delete/endpoint.rs +++ b/src/endpoints/v1/groups/id/delete/endpoint.rs @@ -3,6 +3,8 @@ use actix_web::{delete, web, HttpResponse, Responder, ResponseError}; use mairie360_api_lib::pool::AppState; use mairie360_api_lib::security::AuthenticatedUser; +use crate::database::groups::delete_group::{delete_group_query, DeleteGroupQueryView}; + #[derive(Debug, Clone, PartialEq)] enum DeleteGroupError { BadRequest, @@ -35,16 +37,17 @@ impl ResponseError for DeleteGroupError { } } -async fn delete_group( - user: AuthenticatedUser, - state: web::Data, - id: u64, -) -> Result<(), DeleteGroupError> { +async fn delete_group(state: web::Data, id: u64) -> Result<(), DeleteGroupError> { let pool = match state.db_pool.clone() { Some(pool) => pool, None => return Err(DeleteGroupError::DatabaseError), }; + let db_view = DeleteGroupQueryView::new(id); + delete_group_query(db_view, pool) + .await + .map_err(|_| DeleteGroupError::BadRequest)?; + Ok(()) } @@ -64,10 +67,10 @@ async fn delete_group( )] #[delete("/")] pub async fn delete( - user: AuthenticatedUser, + _: AuthenticatedUser, state: web::Data, id: web::Path, ) -> Result { - delete_group(user, state, id.into_inner()).await?; + delete_group(state, id.into_inner()).await?; Ok(HttpResponse::NoContent().finish()) } diff --git a/src/endpoints/v1/groups/id/get/endpoint.rs b/src/endpoints/v1/groups/id/get/endpoint.rs index afbc57b..5b5b0e7 100644 --- a/src/endpoints/v1/groups/id/get/endpoint.rs +++ b/src/endpoints/v1/groups/id/get/endpoint.rs @@ -1,3 +1,4 @@ +use crate::database::groups::get_group::{get_group_query, GetGroupQuerView}; use crate::endpoints::v1::groups::id::get::view::GetGroupResultView; use actix_web::http::StatusCode; use actix_web::{get, web, HttpResponse, Responder, ResponseError}; @@ -37,16 +38,20 @@ impl ResponseError for GetGroupError { } async fn get_group( - user: AuthenticatedUser, state: web::Data, id: u64, -) -> Result<(), GetGroupError> { +) -> Result { let pool = match state.db_pool.clone() { Some(pool) => pool, None => return Err(GetGroupError::DatabaseError), }; - Ok(()) + let db_view = GetGroupQuerView::new(id); + let result = get_group_query(db_view, pool) + .await + .map_err(|_| GetGroupError::BadRequest)?; + + Ok(GetGroupResultView::new(result)) } #[utoipa::path( @@ -65,10 +70,10 @@ async fn get_group( )] #[get("/")] pub async fn get( - user: AuthenticatedUser, + _: AuthenticatedUser, state: web::Data, id: web::Path, ) -> Result { - let result = get_group(user, state, id.into_inner()).await?; - Ok(HttpResponse::Ok().body(result)) + let result = get_group(state, id.into_inner()).await?; + Ok(HttpResponse::Ok().json(result)) } diff --git a/src/endpoints/v1/groups/id/get/view.rs b/src/endpoints/v1/groups/id/get/view.rs index 735f101..bb52db5 100644 --- a/src/endpoints/v1/groups/id/get/view.rs +++ b/src/endpoints/v1/groups/id/get/view.rs @@ -1,7 +1,13 @@ -use crate::endpoints::v1::groups::get::view::Group; +use crate::database::groups::get_group::Group; use utoipa::ToSchema; #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GetGroupResultView { group: Group, } + +impl GetGroupResultView { + pub fn new(group: Group) -> Self { + Self { group } + } +} diff --git a/src/endpoints/v1/groups/id/users/get/endpoint.rs b/src/endpoints/v1/groups/id/users/get/endpoint.rs index c67839d..ac5da64 100644 --- a/src/endpoints/v1/groups/id/users/get/endpoint.rs +++ b/src/endpoints/v1/groups/id/users/get/endpoint.rs @@ -1,3 +1,4 @@ +use crate::database::groups::get_group_users::{get_group_users_query, GetGroupUsersQueryView}; use crate::endpoints::v1::groups::id::users::get::view::GetGroupUsersResultView; use actix_web::http::StatusCode; use actix_web::{get, web, HttpResponse, Responder, ResponseError}; @@ -45,7 +46,12 @@ async fn get_group_users( None => return Err(GetUsersGroupError::DatabaseError), }; - Ok(GetGroupUsersResultView::new(vec![])) + let view = GetGroupUsersQueryView::new(group_id as u64); + let result = get_group_users_query(view, pool) + .await + .map_err(|_| GetUsersGroupError::BadRequest)?; + + Ok(result.into()) } #[utoipa::path( diff --git a/src/endpoints/v1/groups/id/users/get/view.rs b/src/endpoints/v1/groups/id/users/get/view.rs index 1c42bfb..9ff8e90 100644 --- a/src/endpoints/v1/groups/id/users/get/view.rs +++ b/src/endpoints/v1/groups/id/users/get/view.rs @@ -10,3 +10,9 @@ impl GetGroupUsersResultView { Self { users } } } + +impl From> for GetGroupUsersResultView { + fn from(users: Vec) -> Self { + Self::new(users.into_iter().map(|u| u as u64).collect()) + } +} diff --git a/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs b/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs index 489e9f4..02c2f69 100644 --- a/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs +++ b/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs @@ -3,6 +3,10 @@ use actix_web::{delete, web, HttpResponse, Responder, ResponseError}; use mairie360_api_lib::pool::AppState; use mairie360_api_lib::security::AuthenticatedUser; +use crate::database::groups::delete_user_from_group::{ + delete_user_from_group_query, DeleteUserFromGroupQueryView, +}; + #[derive(Debug, Clone, PartialEq)] enum AddAccessError { BadRequest, @@ -45,6 +49,11 @@ async fn delete_user_from_group( None => return Err(AddAccessError::DatabaseError), }; + let db_view = DeleteUserFromGroupQueryView::new(group_id, user_id); + delete_user_from_group_query(db_view, pool) + .await + .map_err(|_| AddAccessError::BadRequest)?; + Ok(()) } diff --git a/src/endpoints/v1/groups/id/users/post/endpoint.rs b/src/endpoints/v1/groups/id/users/post/endpoint.rs index cd3d0fa..0643cd4 100644 --- a/src/endpoints/v1/groups/id/users/post/endpoint.rs +++ b/src/endpoints/v1/groups/id/users/post/endpoint.rs @@ -1,3 +1,6 @@ +use crate::database::groups::add_user_to_group::{ + add_user_to_group_query, AddUserToGroupQueryView, +}; use crate::endpoints::v1::groups::id::users::post::view::PostUserGroupView; use actix_web::http::StatusCode; use actix_web::{post, web, HttpResponse, Responder, ResponseError}; @@ -45,6 +48,11 @@ async fn add_user_to_group( None => return Err(PostUserGroupError::DatabaseError), }; + let db_view = AddUserToGroupQueryView::new(view.user_id(), view.group_id()); + add_user_to_group_query(db_view, pool) + .await + .map_err(|_| PostUserGroupError::BadRequest)?; + Ok(()) } diff --git a/src/endpoints/v1/groups/id/users/post/view.rs b/src/endpoints/v1/groups/id/users/post/view.rs index 8b98dba..cb67f42 100644 --- a/src/endpoints/v1/groups/id/users/post/view.rs +++ b/src/endpoints/v1/groups/id/users/post/view.rs @@ -7,10 +7,6 @@ pub struct PostUserGroupView { } impl PostUserGroupView { - pub fn new(user_id: u64, group_id: u64) -> Self { - Self { user_id, group_id } - } - pub fn user_id(&self) -> u64 { self.user_id } diff --git a/src/endpoints/v1/groups/post/endpoint.rs b/src/endpoints/v1/groups/post/endpoint.rs index 1132853..c14bd0b 100644 --- a/src/endpoints/v1/groups/post/endpoint.rs +++ b/src/endpoints/v1/groups/post/endpoint.rs @@ -1,4 +1,5 @@ -use crate::endpoints::v1::groups::post::view::PostGroupView; +use crate::database::groups::create_group::{create_group_query, CreateGroupQueryView}; +use crate::endpoints::v1::groups::post::view::{PostGroupResultView, PostGroupView}; use actix_web::http::StatusCode; use actix_web::{post, web, HttpResponse, Responder, ResponseError}; use mairie360_api_lib::pool::AppState; @@ -40,13 +41,18 @@ async fn create_group( user: AuthenticatedUser, state: web::Data, view: PostGroupView, -) -> Result<(), PostGroupError> { +) -> Result { let pool = match state.db_pool.clone() { Some(pool) => pool, None => return Err(PostGroupError::DatabaseError), }; - Ok(()) + let db_view = CreateGroupQueryView::new(user.id, &view.name(), &view.description()); + let id = create_group_query(db_view, pool) + .await + .map_err(|_| PostGroupError::BadRequest)?; + + Ok(PostGroupResultView::new(id as u64)) } #[utoipa::path( @@ -70,6 +76,6 @@ pub async fn post( state: web::Data, view: web::Json, ) -> Result { - create_group(user, state, view.into_inner()).await?; - Ok(HttpResponse::Ok().body("Group created successfully")) + let result = create_group(user, state, view.into_inner()).await?; + Ok(HttpResponse::Ok().json(result)) } diff --git a/src/endpoints/v1/groups/post/view.rs b/src/endpoints/v1/groups/post/view.rs index 5d69dbe..91a8590 100644 --- a/src/endpoints/v1/groups/post/view.rs +++ b/src/endpoints/v1/groups/post/view.rs @@ -7,13 +7,6 @@ pub struct PostGroupView { } impl PostGroupView { - pub fn new(name: &str, description: &str) -> Self { - Self { - name: name.to_string(), - description: description.to_string(), - } - } - pub fn name(&self) -> &str { &self.name } @@ -22,3 +15,14 @@ impl PostGroupView { &self.description } } + +#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct PostGroupResultView { + id: u64, +} + +impl PostGroupResultView { + pub fn new(id: u64) -> Self { + Self { id } + } +} diff --git a/tests/queries/groups/does_group_exist.rs b/tests/queries/groups/does_group_exist.rs new file mode 100644 index 0000000..8dafba2 --- /dev/null +++ b/tests/queries/groups/does_group_exist.rs @@ -0,0 +1,55 @@ +use crate::common::get_pool; +use core_api::database::groups::{ + create_group::{create_group_query, CreateGroupQueryView}, + does_group_exist::{does_group_exist_query, DoesGroupExistQuerView}, +}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn does_group_exist_true() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = CreateGroupQueryView::new( + 1, + "does_group_exist_true_name", + "does_group_exist_true_description", + ); + let result = create_group_query(view, pool.clone()).await.unwrap(); + let view = DoesGroupExistQuerView::new(result as u64); + let result = does_group_exist_query(view, pool).await; + assert!( + result.is_ok(), + "group should exist, {result:?}", + result = result + ); + let bool_value = result.unwrap(); + assert!( + bool_value, + "result should be true, {bool_value:?}", + bool_value = bool_value + ); +} + +#[tokio::test] +#[serial] +async fn does_group_exist_false() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = DoesGroupExistQuerView::new(999); + let result = does_group_exist_query(view, pool).await; + assert!( + result.is_ok(), + "result should be ok, {result:?}", + result = result + ); + let bool_value = result.unwrap(); + assert!( + !bool_value, + "result should be false, {result:?}", + result = bool_value + ); +} diff --git a/tests/queries/groups/is_user_member.rs b/tests/queries/groups/is_user_member.rs new file mode 100644 index 0000000..2ac3c73 --- /dev/null +++ b/tests/queries/groups/is_user_member.rs @@ -0,0 +1,118 @@ +use crate::common::get_pool; +use core_api::database::groups::{ + add_user_to_group::{add_user_to_group_query, AddUserToGroupQueryView}, + create_group::{create_group_query, CreateGroupQueryView}, + is_user_member::{is_user_member_query, IsUserMemberQueryView}, +}; +use mairie360_api_lib::test_setup::queries_setup::get_shared_db; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn is_user_member_true() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = CreateGroupQueryView::new( + 1, + "is_user_member_true_name", + "is_user_member_true_description", + ); + let result = create_group_query(view, pool.clone()).await.unwrap(); + + let view = AddUserToGroupQueryView::new(result as u64, 2); + let _ = add_user_to_group_query(view, pool.clone()).await; + let view = IsUserMemberQueryView::new(result as u64, 2); + let result = is_user_member_query(view, pool).await; + assert!(result.is_ok(), "is_user_member_true failed: {:?}", result); + assert!( + result.unwrap(), + "is_user_member_true failed: result is false" + ); +} + +#[tokio::test] +#[serial] +async fn is_user_member_false() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = CreateGroupQueryView::new( + 1, + "is_user_member_false_name", + "is_user_member_false_description", + ); + let id = create_group_query(view, pool.clone()).await.unwrap(); + + let view = IsUserMemberQueryView::new(id as u64, 3); + let result = is_user_member_query(view, pool.clone()).await; + assert!(result.is_ok(), "is_user_member_false failed: {:?}", result); + assert!( + !result.unwrap(), + "is_user_member_false failed: result is true" + ); +} + +#[tokio::test] +#[serial] +async fn is_user_member_unknow_user() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = CreateGroupQueryView::new( + 1, + "is_user_member_unknow_user_name", + "is_user_member_unknow_user_description", + ); + let result = create_group_query(view, pool.clone()).await.unwrap(); + + let view = IsUserMemberQueryView::new(result as u64, 999); + let result = is_user_member_query(view, pool.clone()).await; + assert!( + result.is_ok(), + "is_user_member_unknow_user failed: {:?}", + result + ); + assert!( + !result.unwrap(), + "is_user_member_unknow_user failed: result is true" + ); +} + +#[tokio::test] +#[serial] +async fn is_user_member_unknow_group() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = IsUserMemberQueryView::new(999, 2); + let result = is_user_member_query(view, pool.clone()).await; + assert!( + result.is_ok(), + "is_user_member_unknow_group failed: {:?}", + result + ); + assert!( + !result.unwrap(), + "is_user_member_unknow_group failed: result is true" + ); +} + +#[tokio::test] +#[serial] +async fn is_user_member_unknow_user_and_group() { + let (_container, host) = get_shared_db().await; + let pool = get_pool(host.to_string()).await; + + let view = IsUserMemberQueryView::new(999, 999); + let result = is_user_member_query(view, pool).await; + assert!( + result.is_ok(), + "is_user_member_unknow_user_and_group failed: {:?}", + result + ); + assert!( + !result.unwrap(), + "is_user_member_unknow_user_and_group failed: result is true" + ); +} diff --git a/tests/queries/groups/mod.rs b/tests/queries/groups/mod.rs index 5f031ef..35ce623 100644 --- a/tests/queries/groups/mod.rs +++ b/tests/queries/groups/mod.rs @@ -2,6 +2,8 @@ pub mod add_user_to_group; pub mod create_group; pub mod delete_group; pub mod delete_user_from_group; +pub mod does_group_exist; pub mod get_group; pub mod get_group_users; pub mod get_user_groups; +pub mod is_user_member; From 3bcaa6bf932dab741b5e3b2ffc5e2665fd3df5a3 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Mon, 18 May 2026 13:15:56 +0800 Subject: [PATCH 12/16] feat: add better params check --- src/endpoints/v1/groups/id/delete/endpoint.rs | 9 +++++ .../v1/groups/id/users/get/endpoint.rs | 9 +++++ .../v1/groups/id/users/id/delete/endpoint.rs | 40 ++++++++++++++----- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/endpoints/v1/groups/id/delete/endpoint.rs b/src/endpoints/v1/groups/id/delete/endpoint.rs index 87f43ed..1931246 100644 --- a/src/endpoints/v1/groups/id/delete/endpoint.rs +++ b/src/endpoints/v1/groups/id/delete/endpoint.rs @@ -4,6 +4,7 @@ use mairie360_api_lib::pool::AppState; use mairie360_api_lib::security::AuthenticatedUser; use crate::database::groups::delete_group::{delete_group_query, DeleteGroupQueryView}; +use crate::database::groups::does_group_exist::{does_group_exist_query, DoesGroupExistQuerView}; #[derive(Debug, Clone, PartialEq)] enum DeleteGroupError { @@ -43,6 +44,14 @@ async fn delete_group(state: web::Data, id: u64) -> Result<(), DeleteG None => return Err(DeleteGroupError::DatabaseError), }; + let group_check_view = DoesGroupExistQuerView::new(id as u64); + let result = does_group_exist_query(group_check_view, pool.clone()) + .await + .map_err(|_| DeleteGroupError::BadRequest)?; + if !result { + return Err(DeleteGroupError::BadRequest); + } + let db_view = DeleteGroupQueryView::new(id); delete_group_query(db_view, pool) .await diff --git a/src/endpoints/v1/groups/id/users/get/endpoint.rs b/src/endpoints/v1/groups/id/users/get/endpoint.rs index ac5da64..48e83d5 100644 --- a/src/endpoints/v1/groups/id/users/get/endpoint.rs +++ b/src/endpoints/v1/groups/id/users/get/endpoint.rs @@ -1,3 +1,4 @@ +use crate::database::groups::does_group_exist::{does_group_exist_query, DoesGroupExistQuerView}; use crate::database::groups::get_group_users::{get_group_users_query, GetGroupUsersQueryView}; use crate::endpoints::v1::groups::id::users::get::view::GetGroupUsersResultView; use actix_web::http::StatusCode; @@ -46,6 +47,14 @@ async fn get_group_users( None => return Err(GetUsersGroupError::DatabaseError), }; + let check_view = DoesGroupExistQuerView::new(group_id as u64); + let result = does_group_exist_query(check_view, pool.clone()) + .await + .map_err(|_| GetUsersGroupError::BadRequest)?; + if !result { + return Err(GetUsersGroupError::BadRequest); + } + let view = GetGroupUsersQueryView::new(group_id as u64); let result = get_group_users_query(view, pool) .await diff --git a/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs b/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs index 02c2f69..d891d73 100644 --- a/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs +++ b/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs @@ -6,31 +6,33 @@ use mairie360_api_lib::security::AuthenticatedUser; use crate::database::groups::delete_user_from_group::{ delete_user_from_group_query, DeleteUserFromGroupQueryView, }; +use crate::database::groups::does_group_exist::{does_group_exist_query, DoesGroupExistQuerView}; +use crate::database::groups::is_user_member::{is_user_member_query, IsUserMemberQueryView}; #[derive(Debug, Clone, PartialEq)] -enum AddAccessError { +enum DeleteUserFromGroupError { BadRequest, DatabaseError, } -impl std::fmt::Display for AddAccessError { +impl std::fmt::Display for DeleteUserFromGroupError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - AddAccessError::DatabaseError => { + DeleteUserFromGroupError::DatabaseError => { write!(f, "An error occurred while accessing the database.") } - AddAccessError::BadRequest => { + DeleteUserFromGroupError::BadRequest => { write!(f, "Bad request.") } } } } -impl ResponseError for AddAccessError { +impl ResponseError for DeleteUserFromGroupError { fn status_code(&self) -> StatusCode { match self { - AddAccessError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, - AddAccessError::BadRequest => StatusCode::BAD_REQUEST, + DeleteUserFromGroupError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + DeleteUserFromGroupError::BadRequest => StatusCode::BAD_REQUEST, } } @@ -43,16 +45,32 @@ async fn delete_user_from_group( state: web::Data, group_id: u64, user_id: u64, -) -> Result<(), AddAccessError> { +) -> Result<(), DeleteUserFromGroupError> { let pool = match state.db_pool.clone() { Some(pool) => pool, - None => return Err(AddAccessError::DatabaseError), + None => return Err(DeleteUserFromGroupError::DatabaseError), }; + let check_view = DoesGroupExistQuerView::new(group_id as u64); + let result = does_group_exist_query(check_view, pool.clone()) + .await + .map_err(|_| DeleteUserFromGroupError::BadRequest)?; + if !result { + return Err(DeleteUserFromGroupError::BadRequest); + } + + let user_check_view = IsUserMemberQueryView::new(group_id, user_id); + let result = is_user_member_query(user_check_view, pool.clone()) + .await + .map_err(|_| DeleteUserFromGroupError::BadRequest)?; + if !result { + return Err(DeleteUserFromGroupError::BadRequest); + } + let db_view = DeleteUserFromGroupQueryView::new(group_id, user_id); delete_user_from_group_query(db_view, pool) .await - .map_err(|_| AddAccessError::BadRequest)?; + .map_err(|_| DeleteUserFromGroupError::BadRequest)?; Ok(()) } @@ -76,7 +94,7 @@ pub async fn delete( _: AuthenticatedUser, state: web::Data, path: web::Path<(u64, u64)>, -) -> Result { +) -> Result { let (group_id, user_id) = path.into_inner(); delete_user_from_group(state, group_id, user_id).await?; Ok(HttpResponse::NoContent().finish()) From af105f02133f79e9ac97d7d5c294a6ecff11e9fd Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Mon, 18 May 2026 15:57:16 +0800 Subject: [PATCH 13/16] feat: add better security --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- docker-compose.yml | 6 +++--- src/endpoints/v1/groups/id/delete/endpoint.rs | 11 ++--------- src/endpoints/v1/groups/id/get/endpoint.rs | 15 +++++++++++++++ src/endpoints/v1/groups/id/mod.rs | 19 +++++++++++++++++-- .../v1/groups/id/users/get/endpoint.rs | 14 ++++++++++---- .../v1/groups/id/users/id/delete/endpoint.rs | 19 ++++++++----------- src/endpoints/v1/groups/id/users/id/mod.rs | 16 ++++++++++++++-- src/endpoints/v1/groups/id/users/mod.rs | 15 +++++++++++++-- .../v1/groups/id/users/post/endpoint.rs | 16 +++++++++++----- 11 files changed, 96 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc18d3e..e504178 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1942,9 +1942,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "mairie360_api_lib" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eae04cdd94f636106dd46cdbe6e426b9766c66f26bc0f090e2c10e18795ad4e" +checksum = "bbcc94519080ece64c2abe6e37c7b994327a606401b955a9abb5f8fb910a2283" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index cc5aa7e..9c8381d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ async-trait = "0.1.89" base64 = "0.22.1" chrono = { version = "0.4", features = ["serde"] } futures-util = "0.3" -mairie360_api_lib = "0.6.0" +mairie360_api_lib = "0.6.1" rand = "0.9" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" diff --git a/docker-compose.yml b/docker-compose.yml index 401bbd4..e04c293 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,7 +42,7 @@ x-service: &service-template services: postgres: - image: ghcr.io/mairie360/database:dev-b816587 + image: ghcr.io/mairie360/database:dev-c3cce15 restart: always container_name: mairie360-db-core-api environment: @@ -62,7 +62,7 @@ services: retries: 5 liquibase: - image: ghcr.io/mairie360/liquibase-migrations:dev-b816587 + image: ghcr.io/mairie360/liquibase-migrations:dev-c3cce15 container_name: mairie360-liquibase-core-api depends_on: postgres: @@ -148,4 +148,4 @@ services: - backend depends_on: core: - condition: service_healthy \ No newline at end of file + condition: service_healthy diff --git a/src/endpoints/v1/groups/id/delete/endpoint.rs b/src/endpoints/v1/groups/id/delete/endpoint.rs index 1931246..f9ea8d0 100644 --- a/src/endpoints/v1/groups/id/delete/endpoint.rs +++ b/src/endpoints/v1/groups/id/delete/endpoint.rs @@ -4,7 +4,6 @@ use mairie360_api_lib::pool::AppState; use mairie360_api_lib::security::AuthenticatedUser; use crate::database::groups::delete_group::{delete_group_query, DeleteGroupQueryView}; -use crate::database::groups::does_group_exist::{does_group_exist_query, DoesGroupExistQuerView}; #[derive(Debug, Clone, PartialEq)] enum DeleteGroupError { @@ -44,14 +43,6 @@ async fn delete_group(state: web::Data, id: u64) -> Result<(), DeleteG None => return Err(DeleteGroupError::DatabaseError), }; - let group_check_view = DoesGroupExistQuerView::new(id as u64); - let result = does_group_exist_query(group_check_view, pool.clone()) - .await - .map_err(|_| DeleteGroupError::BadRequest)?; - if !result { - return Err(DeleteGroupError::BadRequest); - } - let db_view = DeleteGroupQueryView::new(id); delete_group_query(db_view, pool) .await @@ -67,6 +58,8 @@ async fn delete_group(state: web::Data, id: u64) -> Result<(), DeleteG (status = 204, description = "Group deleted successfully"), (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden"), + (status = 404, description = "Not found"), (status = 500, description = "Internal server error") ), tag = "Groups", diff --git a/src/endpoints/v1/groups/id/get/endpoint.rs b/src/endpoints/v1/groups/id/get/endpoint.rs index 5b5b0e7..bf32735 100644 --- a/src/endpoints/v1/groups/id/get/endpoint.rs +++ b/src/endpoints/v1/groups/id/get/endpoint.rs @@ -1,3 +1,4 @@ +use crate::database::groups::does_group_exist::{does_group_exist_query, DoesGroupExistQuerView}; use crate::database::groups::get_group::{get_group_query, GetGroupQuerView}; use crate::endpoints::v1::groups::id::get::view::GetGroupResultView; use actix_web::http::StatusCode; @@ -9,6 +10,7 @@ use mairie360_api_lib::security::AuthenticatedUser; enum GetGroupError { BadRequest, DatabaseError, + UnknowGroup, } impl std::fmt::Display for GetGroupError { @@ -20,6 +22,9 @@ impl std::fmt::Display for GetGroupError { GetGroupError::BadRequest => { write!(f, "Bad request.") } + GetGroupError::UnknowGroup => { + write!(f, "Unknow group.") + } } } } @@ -29,6 +34,7 @@ impl ResponseError for GetGroupError { match self { GetGroupError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, GetGroupError::BadRequest => StatusCode::BAD_REQUEST, + GetGroupError::UnknowGroup => StatusCode::NOT_FOUND, } } @@ -46,6 +52,14 @@ async fn get_group( None => return Err(GetGroupError::DatabaseError), }; + let group_check_view = DoesGroupExistQuerView::new(id); + let result = does_group_exist_query(group_check_view, pool.clone()) + .await + .map_err(|_| GetGroupError::UnknowGroup)?; + if !result { + return Err(GetGroupError::UnknowGroup); + } + let db_view = GetGroupQuerView::new(id); let result = get_group_query(db_view, pool) .await @@ -61,6 +75,7 @@ async fn get_group( (status = 200, body = GetGroupResultView), (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized"), + (status = 404, description = "Not found"), (status = 500, description = "Internal server error") ), tag = "Groups", diff --git a/src/endpoints/v1/groups/id/mod.rs b/src/endpoints/v1/groups/id/mod.rs index b6f3f5a..34da7cb 100644 --- a/src/endpoints/v1/groups/id/mod.rs +++ b/src/endpoints/v1/groups/id/mod.rs @@ -3,13 +3,28 @@ pub mod doc; mod get; mod users; +use actix_web::middleware::from_fn; use actix_web::web; +use mairie360_api_lib::security::access_guard_middleware; +use mairie360_api_lib::security::AccessCheckConfig; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/{group_id}") - .service(delete::endpoint::delete) + // 1. Les routes standards .service(get::endpoint::get) - .configure(users::config), + .configure(users::config) + // 2. On applique le middleware et la config UNIQUEMENT au delete + // en l'enveloppant dans un scope vide "" + .service( + web::scope("") + .app_data(AccessCheckConfig { + resource_name: "groups", + action: "update", + id_param_pattern: Some("group_id"), + }) + .wrap(from_fn(access_guard_middleware)) + .service(delete::endpoint::delete), // On réutilise le service existant avec sa macro + ), ); } diff --git a/src/endpoints/v1/groups/id/users/get/endpoint.rs b/src/endpoints/v1/groups/id/users/get/endpoint.rs index 48e83d5..540cd9d 100644 --- a/src/endpoints/v1/groups/id/users/get/endpoint.rs +++ b/src/endpoints/v1/groups/id/users/get/endpoint.rs @@ -10,16 +10,20 @@ use mairie360_api_lib::security::AuthenticatedUser; enum GetUsersGroupError { BadRequest, DatabaseError, + UnknowGroup, } impl std::fmt::Display for GetUsersGroupError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { GetUsersGroupError::DatabaseError => { - write!(f, "An error occurred while accessing the database.") + write!(f, "An error occurred while accessing the database") } GetUsersGroupError::BadRequest => { - write!(f, "Bad request.") + write!(f, "Bad request") + } + GetUsersGroupError::UnknowGroup => { + write!(f, "Unknow group") } } } @@ -30,6 +34,7 @@ impl ResponseError for GetUsersGroupError { match self { GetUsersGroupError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, GetUsersGroupError::BadRequest => StatusCode::BAD_REQUEST, + GetUsersGroupError::UnknowGroup => StatusCode::NOT_FOUND, } } @@ -50,9 +55,9 @@ async fn get_group_users( let check_view = DoesGroupExistQuerView::new(group_id as u64); let result = does_group_exist_query(check_view, pool.clone()) .await - .map_err(|_| GetUsersGroupError::BadRequest)?; + .map_err(|_| GetUsersGroupError::UnknowGroup)?; if !result { - return Err(GetUsersGroupError::BadRequest); + return Err(GetUsersGroupError::UnknowGroup); } let view = GetGroupUsersQueryView::new(group_id as u64); @@ -70,6 +75,7 @@ async fn get_group_users( (status = 200, body = GetGroupUsersResultView), (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized"), + (status = 404, description = "Unknow group"), (status = 500, description = "Internal server error") ), tag = "Groups", diff --git a/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs b/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs index d891d73..2352ba7 100644 --- a/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs +++ b/src/endpoints/v1/groups/id/users/id/delete/endpoint.rs @@ -6,13 +6,13 @@ use mairie360_api_lib::security::AuthenticatedUser; use crate::database::groups::delete_user_from_group::{ delete_user_from_group_query, DeleteUserFromGroupQueryView, }; -use crate::database::groups::does_group_exist::{does_group_exist_query, DoesGroupExistQuerView}; use crate::database::groups::is_user_member::{is_user_member_query, IsUserMemberQueryView}; #[derive(Debug, Clone, PartialEq)] enum DeleteUserFromGroupError { BadRequest, DatabaseError, + UnknowUser, } impl std::fmt::Display for DeleteUserFromGroupError { @@ -24,6 +24,9 @@ impl std::fmt::Display for DeleteUserFromGroupError { DeleteUserFromGroupError::BadRequest => { write!(f, "Bad request.") } + DeleteUserFromGroupError::UnknowUser => { + write!(f, "Unknow user.") + } } } } @@ -33,6 +36,7 @@ impl ResponseError for DeleteUserFromGroupError { match self { DeleteUserFromGroupError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, DeleteUserFromGroupError::BadRequest => StatusCode::BAD_REQUEST, + DeleteUserFromGroupError::UnknowUser => StatusCode::NOT_FOUND, } } @@ -51,20 +55,12 @@ async fn delete_user_from_group( None => return Err(DeleteUserFromGroupError::DatabaseError), }; - let check_view = DoesGroupExistQuerView::new(group_id as u64); - let result = does_group_exist_query(check_view, pool.clone()) - .await - .map_err(|_| DeleteUserFromGroupError::BadRequest)?; - if !result { - return Err(DeleteUserFromGroupError::BadRequest); - } - let user_check_view = IsUserMemberQueryView::new(group_id, user_id); let result = is_user_member_query(user_check_view, pool.clone()) .await - .map_err(|_| DeleteUserFromGroupError::BadRequest)?; + .map_err(|_| DeleteUserFromGroupError::UnknowUser)?; if !result { - return Err(DeleteUserFromGroupError::BadRequest); + return Err(DeleteUserFromGroupError::UnknowUser); } let db_view = DeleteUserFromGroupQueryView::new(group_id, user_id); @@ -82,6 +78,7 @@ async fn delete_user_from_group( (status = 204, description = "User deleted from group successfully"), (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized"), + (status = 404, description = "Not found"), (status = 500, description = "Internal server error") ), tag = "Groups", diff --git a/src/endpoints/v1/groups/id/users/id/mod.rs b/src/endpoints/v1/groups/id/users/id/mod.rs index 6aa0094..3123d04 100644 --- a/src/endpoints/v1/groups/id/users/id/mod.rs +++ b/src/endpoints/v1/groups/id/users/id/mod.rs @@ -1,8 +1,20 @@ mod delete; pub mod doc; -use actix_web::web; +use actix_web::{middleware::from_fn, web}; +use mairie360_api_lib::security::{access_guard_middleware, AccessCheckConfig}; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service(web::scope("/{user_id}").service(delete::endpoint::delete)); + cfg.service( + web::scope("/{user_id}").service( + web::scope("") + .app_data(AccessCheckConfig { + resource_name: "groups", + action: "update", + id_param_pattern: Some("group_id"), + }) + .wrap(from_fn(access_guard_middleware)) + .service(delete::endpoint::delete), // On réutilise le service existant avec sa macro + ), + ); } diff --git a/src/endpoints/v1/groups/id/users/mod.rs b/src/endpoints/v1/groups/id/users/mod.rs index 9afd7b5..af4e2b9 100644 --- a/src/endpoints/v1/groups/id/users/mod.rs +++ b/src/endpoints/v1/groups/id/users/mod.rs @@ -3,13 +3,24 @@ mod get; mod id; mod post; +use actix_web::middleware::from_fn; use actix_web::web; +use mairie360_api_lib::security::{access_guard_middleware, AccessCheckConfig}; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/users") .configure(id::config) - .service(post::endpoint::post) - .service(get::endpoint::get), + .service(get::endpoint::get) + .service( + web::scope("") + .app_data(AccessCheckConfig { + resource_name: "groups", + action: "update", + id_param_pattern: Some("group_id"), + }) + .wrap(from_fn(access_guard_middleware)) + .service(post::endpoint::post), // On réutilise le service existant avec sa macro + ), ); } diff --git a/src/endpoints/v1/groups/id/users/post/endpoint.rs b/src/endpoints/v1/groups/id/users/post/endpoint.rs index 0643cd4..afb1c5f 100644 --- a/src/endpoints/v1/groups/id/users/post/endpoint.rs +++ b/src/endpoints/v1/groups/id/users/post/endpoint.rs @@ -9,8 +9,9 @@ use mairie360_api_lib::security::AuthenticatedUser; #[derive(Debug, Clone, PartialEq)] enum PostUserGroupError { - BadRequest, + // BadRequest, DatabaseError, + UnknowUser, } impl std::fmt::Display for PostUserGroupError { @@ -19,8 +20,11 @@ impl std::fmt::Display for PostUserGroupError { PostUserGroupError::DatabaseError => { write!(f, "An error occurred while accessing the database.") } - PostUserGroupError::BadRequest => { - write!(f, "Bad request.") + // PostUserGroupError::BadRequest => { + // write!(f, "Bad request.") + // } + PostUserGroupError::UnknowUser => { + write!(f, "Unknow user.") } } } @@ -29,8 +33,9 @@ impl std::fmt::Display for PostUserGroupError { impl ResponseError for PostUserGroupError { fn status_code(&self) -> StatusCode { match self { + // PostUserGroupError::BadRequest => StatusCode::BAD_REQUEST, PostUserGroupError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, - PostUserGroupError::BadRequest => StatusCode::BAD_REQUEST, + PostUserGroupError::UnknowUser => StatusCode::NOT_FOUND, } } @@ -51,7 +56,7 @@ async fn add_user_to_group( let db_view = AddUserToGroupQueryView::new(view.user_id(), view.group_id()); add_user_to_group_query(db_view, pool) .await - .map_err(|_| PostUserGroupError::BadRequest)?; + .map_err(|_| PostUserGroupError::UnknowUser)?; Ok(()) } @@ -64,6 +69,7 @@ async fn add_user_to_group( (status = 200, description = "User added to group successfully"), (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized"), + (status = 404, description = "Unknow user."), (status = 500, description = "Internal server error") ), tag = "Groups", From bc01b3403976c5b944583f819f51582aa88419c9 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Mon, 18 May 2026 17:27:50 +0800 Subject: [PATCH 14/16] fix: fix cicd errors --- src/database/groups/add_user_to_group/view.rs | 3 +-- src/database/groups/delete_user_from_group/view.rs | 5 +---- src/endpoints/v1/auth/login/endpoint.rs | 11 +++++------ tests/queries/groups/add_user_to_group.rs | 6 +++++- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/database/groups/add_user_to_group/view.rs b/src/database/groups/add_user_to_group/view.rs index cc42861..6750e95 100644 --- a/src/database/groups/add_user_to_group/view.rs +++ b/src/database/groups/add_user_to_group/view.rs @@ -23,8 +23,7 @@ impl AddUserToGroupQueryView { impl DatabaseQueryView for AddUserToGroupQueryView { fn get_request(&self) -> String { - "INSERT INTO group_users (group_id, user_id) VALUES ($1, $2)" - .to_string() + "INSERT INTO group_users (group_id, user_id) VALUES ($1, $2)".to_string() } } diff --git a/src/database/groups/delete_user_from_group/view.rs b/src/database/groups/delete_user_from_group/view.rs index 22c4183..6aa7081 100644 --- a/src/database/groups/delete_user_from_group/view.rs +++ b/src/database/groups/delete_user_from_group/view.rs @@ -9,10 +9,7 @@ pub struct DeleteUserFromGroupQueryView { impl DeleteUserFromGroupQueryView { pub fn new(group_id: u64, user_id: u64) -> Self { - Self { - group_id, - user_id, - } + Self { group_id, user_id } } pub fn group_id(&self) -> u64 { diff --git a/src/endpoints/v1/auth/login/endpoint.rs b/src/endpoints/v1/auth/login/endpoint.rs index 6f85402..2f6e08b 100644 --- a/src/endpoints/v1/auth/login/endpoint.rs +++ b/src/endpoints/v1/auth/login/endpoint.rs @@ -1,3 +1,5 @@ +use super::login_response_view::LoginResponseView; +use super::login_view::LoginView; use crate::database::auth::login::{login_query, LoginUserQueryView}; use crate::database::sessions::create_session::CreateSessionQueryView; use crate::endpoints::v1::auth::create_new_session; @@ -5,12 +7,9 @@ use actix_web::{ dev::ConnectionInfo, http::StatusCode, post, web, HttpResponse, Responder, ResponseError, }; use base64::{engine::general_purpose, Engine as _}; -use mairie360_api_lib::pool::AppState; -use rand::{rng, RngCore}; - -use super::login_response_view::LoginResponseView; -use super::login_view::LoginView; use mairie360_api_lib::jwt_manager::generate_jwt; +use mairie360_api_lib::pool::AppState; +use rand::fill; #[derive(Debug, Clone, PartialEq)] enum LoginError { @@ -50,7 +49,7 @@ fn generate_refresh_token() -> String { let mut buffer = [0u8; 32]; // Remplissage avec des données aléatoires sécurisées - rng().fill_bytes(&mut buffer); + fill(&mut buffer); // Encodage en Base64 pour avoir une String lisible general_purpose::URL_SAFE_NO_PAD.encode(buffer) diff --git a/tests/queries/groups/add_user_to_group.rs b/tests/queries/groups/add_user_to_group.rs index b1710b6..c98a234 100644 --- a/tests/queries/groups/add_user_to_group.rs +++ b/tests/queries/groups/add_user_to_group.rs @@ -21,7 +21,11 @@ async fn add_user_to_group_success() { let view = AddUserToGroupQueryView::new(result as u64, 2); let result = add_user_to_group_query(view, pool).await; - assert!(result.is_ok(), "add_user_to_group_success failed: {:?}", result); + assert!( + result.is_ok(), + "add_user_to_group_success failed: {:?}", + result + ); } #[tokio::test] From 39deca90fb706e72574e5bd75db5ffce2bfea7ff Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Thu, 21 May 2026 17:08:14 +0800 Subject: [PATCH 15/16] fix: fix endpoints tag --- src/endpoints/v1/auth/force_change_password/endpoint.rs | 2 +- src/endpoints/v1/auth/forgot_password/endpoint.rs | 2 +- src/endpoints/v1/auth/reset_password/endpoint.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/endpoints/v1/auth/force_change_password/endpoint.rs b/src/endpoints/v1/auth/force_change_password/endpoint.rs index 0e2f536..7c2ab0b 100644 --- a/src/endpoints/v1/auth/force_change_password/endpoint.rs +++ b/src/endpoints/v1/auth/force_change_password/endpoint.rs @@ -111,7 +111,7 @@ async fn force_change_password_trigger( (status = 403, description = "Unknown user token"), (status = 500, description = "Internal server error") ), - tag = "Auth" + tag = "Authentication" )] #[post("/force_change_password")] pub async fn force_change_password( diff --git a/src/endpoints/v1/auth/forgot_password/endpoint.rs b/src/endpoints/v1/auth/forgot_password/endpoint.rs index bfd8367..d46f653 100644 --- a/src/endpoints/v1/auth/forgot_password/endpoint.rs +++ b/src/endpoints/v1/auth/forgot_password/endpoint.rs @@ -190,7 +190,7 @@ async fn forgot_password_trigger( (status = 404, description = "User not found"), (status = 500, description = "Internal server error") ), - tag = "Auth", + tag = "Authentication", security( ("jwt" = []) ) diff --git a/src/endpoints/v1/auth/reset_password/endpoint.rs b/src/endpoints/v1/auth/reset_password/endpoint.rs index 0efee47..1d764d8 100644 --- a/src/endpoints/v1/auth/reset_password/endpoint.rs +++ b/src/endpoints/v1/auth/reset_password/endpoint.rs @@ -142,7 +142,7 @@ async fn reset_password_trigger( (status = 401, description = "Unauthorized, invalid token"), (status = 500, description = "Internal server error") ), - tag = "Auth", + tag = "Authentication", )] #[post("/reset_password")] pub async fn reset_password( From 7fd59ff222d44eb280631ebf88fad5466753923b Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Thu, 21 May 2026 18:29:47 +0800 Subject: [PATCH 16/16] feat: refacto users endpoints --- src/database/auth/change_password/view.rs | 32 ---- src/database/users/about/mod.rs | 8 - src/database/users/about/view.rs | 29 ---- src/database/users/get_user_by_id/mod.rs | 6 + .../users/{about => get_user_by_id}/query.rs | 12 +- .../result_view.rs => get_user_by_id/view.rs} | 41 ++++- src/database/users/mod.rs | 3 +- src/database/users/patch_user/mod.rs | 5 + src/database/users/patch_user/query.rs | 56 +++++++ src/database/users/patch_user/view.rs | 75 +++++++++ .../v1/user/about/about_request_view.rs | 41 ----- src/endpoints/v1/user/about/doc.rs | 9 -- src/endpoints/v1/user/about/endpoint.rs | 146 ------------------ src/endpoints/v1/user/about/mod.rs | 4 - src/endpoints/v1/user/doc.rs | 5 +- src/endpoints/v1/user/id/doc.rs | 6 + src/endpoints/v1/user/id/get/endpoint.rs | 75 +++++++++ src/endpoints/v1/user/id/get/mod.rs | 2 + src/endpoints/v1/user/id/get/view.rs | 86 +++++++++++ src/endpoints/v1/user/id/mod.rs | 7 + src/endpoints/v1/user/me/doc.rs | 10 ++ src/endpoints/v1/user/me/get/endpoint.rs | 69 +++++++++ src/endpoints/v1/user/me/get/mod.rs | 2 + .../about_response_view.rs => me/get/view.rs} | 19 ++- src/endpoints/v1/user/me/mod.rs | 13 ++ src/endpoints/v1/user/me/patch/endpoint.rs | 80 ++++++++++ src/endpoints/v1/user/me/patch/mod.rs | 2 + src/endpoints/v1/user/me/patch/view.rs | 53 +++++++ src/endpoints/v1/user/mod.rs | 9 +- 29 files changed, 612 insertions(+), 293 deletions(-) delete mode 100644 src/database/users/about/mod.rs delete mode 100644 src/database/users/about/view.rs create mode 100644 src/database/users/get_user_by_id/mod.rs rename src/database/users/{about => get_user_by_id}/query.rs (54%) rename src/database/users/{about/result_view.rs => get_user_by_id/view.rs} (53%) create mode 100644 src/database/users/patch_user/mod.rs create mode 100644 src/database/users/patch_user/query.rs create mode 100644 src/database/users/patch_user/view.rs delete mode 100644 src/endpoints/v1/user/about/about_request_view.rs delete mode 100644 src/endpoints/v1/user/about/doc.rs delete mode 100644 src/endpoints/v1/user/about/endpoint.rs delete mode 100644 src/endpoints/v1/user/about/mod.rs create mode 100644 src/endpoints/v1/user/id/doc.rs create mode 100644 src/endpoints/v1/user/id/get/endpoint.rs create mode 100644 src/endpoints/v1/user/id/get/mod.rs create mode 100644 src/endpoints/v1/user/id/get/view.rs create mode 100644 src/endpoints/v1/user/id/mod.rs create mode 100644 src/endpoints/v1/user/me/doc.rs create mode 100644 src/endpoints/v1/user/me/get/endpoint.rs create mode 100644 src/endpoints/v1/user/me/get/mod.rs rename src/endpoints/v1/user/{about/about_response_view.rs => me/get/view.rs} (77%) create mode 100644 src/endpoints/v1/user/me/mod.rs create mode 100644 src/endpoints/v1/user/me/patch/endpoint.rs create mode 100644 src/endpoints/v1/user/me/patch/mod.rs create mode 100644 src/endpoints/v1/user/me/patch/view.rs diff --git a/src/database/auth/change_password/view.rs b/src/database/auth/change_password/view.rs index cb5d3e9..502432e 100644 --- a/src/database/auth/change_password/view.rs +++ b/src/database/auth/change_password/view.rs @@ -34,35 +34,3 @@ impl Display for ChangePasswordQueryView { write!(f, "ChangePasswordQueryView: password = [PROTECTED]") } } - -#[derive(Debug, sqlx::FromRow, PartialEq, Eq)] -pub struct LoginUserQueryResultView { - #[sqlx(rename = "id")] - user_id: i32, - #[sqlx(rename = "password")] - password: String, - #[sqlx(rename = "first_connect")] - first_connect: bool, -} - -impl LoginUserQueryResultView { - pub fn new(user_id: i32, password: String, first_connect: bool) -> Self { - Self { - user_id, - password, - first_connect, - } - } - - pub fn password(&self) -> &str { - &self.password - } - - pub fn user_id(&self) -> i32 { - self.user_id - } - - pub fn first_connect(&self) -> bool { - self.first_connect - } -} diff --git a/src/database/users/about/mod.rs b/src/database/users/about/mod.rs deleted file mode 100644 index c354044..0000000 --- a/src/database/users/about/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod query; -pub use query::about_user_query; - -mod view; -pub use view::AboutUserQueryView; - -mod result_view; -pub use result_view::AboutUserQueryResultView; diff --git a/src/database/users/about/view.rs b/src/database/users/about/view.rs deleted file mode 100644 index 4b66b11..0000000 --- a/src/database/users/about/view.rs +++ /dev/null @@ -1,29 +0,0 @@ -use mairie360_api_lib::database::db_interface::DatabaseQueryView; -use std::fmt::Display; - -pub struct AboutUserQueryView { - id: u64, -} - -impl AboutUserQueryView { - pub fn new(id: u64) -> Self { - Self { id } - } - - pub fn get_id(&self) -> u64 { - self.id - } -} - -impl DatabaseQueryView for AboutUserQueryView { - fn get_request(&self) -> String { - "SELECT first_name, last_name, email, phone_number, status FROM users WHERE id = $1" - .to_string() - } -} - -impl Display for AboutUserQueryView { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "AboutUserQueryView: id = {}", self.id) - } -} diff --git a/src/database/users/get_user_by_id/mod.rs b/src/database/users/get_user_by_id/mod.rs new file mode 100644 index 0000000..3525ad3 --- /dev/null +++ b/src/database/users/get_user_by_id/mod.rs @@ -0,0 +1,6 @@ +mod query; +pub use query::get_user_by_id_query; + +mod view; +pub use view::GetUserByIdQueryResultView; +pub use view::GetUserByIdQueryView; diff --git a/src/database/users/about/query.rs b/src/database/users/get_user_by_id/query.rs similarity index 54% rename from src/database/users/about/query.rs rename to src/database/users/get_user_by_id/query.rs index 11e98e5..18766b2 100644 --- a/src/database/users/about/query.rs +++ b/src/database/users/get_user_by_id/query.rs @@ -1,15 +1,15 @@ -use crate::database::users::about::AboutUserQueryResultView; -use crate::database::users::about::AboutUserQueryView; +use crate::database::users::get_user_by_id::GetUserByIdQueryResultView; +use crate::database::users::get_user_by_id::GetUserByIdQueryView; use mairie360_api_lib::database::db_interface::DatabaseQueryView; use mairie360_api_lib::database::errors::DatabaseError; use mairie360_api_lib::database::queries::QueryError; use sqlx::PgPool; -pub async fn about_user_query( - view: AboutUserQueryView, +pub async fn get_user_by_id_query( + view: GetUserByIdQueryView, pool: PgPool, -) -> Result { - let result = sqlx::query_as::<_, AboutUserQueryResultView>(&view.get_request()) +) -> Result { + let result = sqlx::query_as::<_, GetUserByIdQueryResultView>(&view.get_request()) .bind(view.get_id() as i32) .fetch_optional(&pool) .await?; diff --git a/src/database/users/about/result_view.rs b/src/database/users/get_user_by_id/view.rs similarity index 53% rename from src/database/users/about/result_view.rs rename to src/database/users/get_user_by_id/view.rs index 1bf308f..cc23103 100644 --- a/src/database/users/about/result_view.rs +++ b/src/database/users/get_user_by_id/view.rs @@ -1,22 +1,52 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; use serde::{Deserialize, Serialize}; -use serde_json; +use std::fmt::Display; + +pub struct GetUserByIdQueryView { + id: u64, +} + +impl GetUserByIdQueryView { + pub fn new(id: u64) -> Self { + Self { id } + } + + pub fn get_id(&self) -> u64 { + self.id + } +} + +impl DatabaseQueryView for GetUserByIdQueryView { + fn get_request(&self) -> String { + "SELECT first_name, last_name, email, phone_number, status, is_archived FROM users WHERE id = $1" + .to_string() + } +} + +impl Display for GetUserByIdQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "GetUserByIdQueryView: id = {}", self.id) + } +} #[derive(Serialize, Deserialize, Debug, sqlx::FromRow)] -pub struct AboutUserQueryResultView { +pub struct GetUserByIdQueryResultView { first_name: String, last_name: String, email: String, phone_number: String, status: String, + is_archived: bool, } -impl AboutUserQueryResultView { +impl GetUserByIdQueryResultView { pub fn new( first_name: &str, last_name: &str, email: &str, phone_number: &str, status: &str, + is_archived: bool, ) -> Self { Self { first_name: first_name.to_string(), @@ -24,6 +54,7 @@ impl AboutUserQueryResultView { email: email.to_string(), phone_number: phone_number.to_string(), status: status.to_string(), + is_archived, } } @@ -50,4 +81,8 @@ impl AboutUserQueryResultView { pub fn status(&self) -> &str { &self.status } + + pub fn is_archived(&self) -> bool { + self.is_archived + } } diff --git a/src/database/users/mod.rs b/src/database/users/mod.rs index ced7521..7c2f082 100644 --- a/src/database/users/mod.rs +++ b/src/database/users/mod.rs @@ -1 +1,2 @@ -pub mod about; +pub mod get_user_by_id; +pub mod patch_user; diff --git a/src/database/users/patch_user/mod.rs b/src/database/users/patch_user/mod.rs new file mode 100644 index 0000000..6c2b4d8 --- /dev/null +++ b/src/database/users/patch_user/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::patch_user_query; + +mod view; +pub use view::PatchUserQueryView; diff --git a/src/database/users/patch_user/query.rs b/src/database/users/patch_user/query.rs new file mode 100644 index 0000000..1209a60 --- /dev/null +++ b/src/database/users/patch_user/query.rs @@ -0,0 +1,56 @@ +use crate::database::users::patch_user::PatchUserQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn patch_user_query( + view: PatchUserQueryView, + pool: &PgPool, +) -> Result<(), DatabaseError> { + let mut query_builder = sqlx::QueryBuilder::new("UPDATE users SET "); + + // On utilise un booléen pour savoir si on a déjà ajouté un champ + let mut first = true; + + // Macro pour ajouter proprement chaque champ + macro_rules! add_field { + ($field_name:expr, $value:expr) => { + if !first { + query_builder.push(", "); + } + query_builder.push($field_name); + query_builder.push(" = "); + query_builder.push_bind($value); + first = false; + }; + } + + if let Some(first_name) = view.first_name() { + add_field!("first_name", first_name); + } + if let Some(last_name) = view.last_name() { + add_field!("last_name", last_name); + } + if let Some(email) = view.email() { + add_field!("email", email); + } + if let Some(phone_number) = view.phone_number() { + add_field!("phone_number", phone_number); + } + + // Si aucun champ n'a été ajouté, on arrête tout + if first { + return Ok(()); + } + + // Ajout de la clause WHERE + query_builder.push(" WHERE id = "); + query_builder.push_bind(view.id() as i32); + + query_builder + .build() + .execute(pool) + .await + .map_err(DatabaseError::from)?; + + Ok(()) +} diff --git a/src/database/users/patch_user/view.rs b/src/database/users/patch_user/view.rs new file mode 100644 index 0000000..f554875 --- /dev/null +++ b/src/database/users/patch_user/view.rs @@ -0,0 +1,75 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use std::fmt::Display; + +pub struct PatchUserQueryView { + id: u64, + first_name: Option, + last_name: Option, + email: Option, + phone_number: Option, +} + +impl PatchUserQueryView { + pub fn new( + id: u64, + first_name: Option<&str>, + last_name: Option<&str>, + email: Option<&str>, + phone_number: Option<&str>, + ) -> Self { + Self { + id, + first_name: first_name.map(|s| s.to_string()), + last_name: last_name.map(|s| s.to_string()), + email: email.map(|s| s.to_string()), + phone_number: phone_number.map(|s| s.to_string()), + } + } + + pub fn id(&self) -> u64 { + self.id + } + + pub fn first_name(&self) -> Option<&str> { + self.first_name.as_deref() + } + pub fn last_name(&self) -> Option<&str> { + self.last_name.as_deref() + } + pub fn email(&self) -> Option<&str> { + self.email.as_deref() + } + pub fn phone_number(&self) -> Option<&str> { + self.phone_number.as_deref() + } +} + +impl DatabaseQueryView for PatchUserQueryView { + fn get_request(&self) -> String { + let mut request = "UPDATE users SET ".to_string(); + if let Some(first_name) = &self.first_name { + request.push_str(&format!("first_name = '{}', ", first_name)); + } + if let Some(last_name) = &self.last_name { + request.push_str(&format!("last_name = '{}', ", last_name)); + } + if let Some(email) = &self.email { + request.push_str(&format!("email = '{}', ", email)); + } + if let Some(phone_number) = &self.phone_number { + request.push_str(&format!("phone_number = '{}', ", phone_number)); + } + request.push_str(&format!("WHERE id = {}", self.id)); + request.push_str(" RETURNING true"); + request + } +} + +impl Display for PatchUserQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.phone_number { + Some(_) => write!(f, "PatchUserQueryView: id = {:?}, first_name = {:?}, last_name = {:?}, email = {:?}, phone_number = {:?}", self.id(), self.first_name(), self.last_name(), self.email(), self.phone_number()), + None => write!(f, "PatchUserQueryView: id = {:?}, first_name = {:?}, last_name = {:?}, email = {:?}", self.id(), self.first_name(), self.last_name(), self.email()), + } + } +} diff --git a/src/endpoints/v1/user/about/about_request_view.rs b/src/endpoints/v1/user/about/about_request_view.rs deleted file mode 100644 index bfdc220..0000000 --- a/src/endpoints/v1/user/about/about_request_view.rs +++ /dev/null @@ -1,41 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::fmt::Display; -use utoipa::ToSchema; - -#[derive(Serialize, Deserialize, ToSchema)] -pub struct AboutRequestView { - user_id: u64, -} - -impl AboutRequestView { - pub fn new(user_id: u64) -> Self { - AboutRequestView { user_id } - } - - pub fn user_id(&self) -> u64 { - self.user_id - } -} - -impl Display for AboutRequestView { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "AboutRequestView {{ user_id: {}}}", self.user_id) - } -} - -#[derive(Serialize, Deserialize, ToSchema)] -pub struct AboutPathParamRequestView { - pub user_id: u64, -} - -impl AboutPathParamRequestView { - pub fn user_id(&self) -> u64 { - self.user_id - } -} - -impl Display for AboutPathParamRequestView { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "AboutRequestView {{ user_id: {} }}", self.user_id) - } -} diff --git a/src/endpoints/v1/user/about/doc.rs b/src/endpoints/v1/user/about/doc.rs deleted file mode 100644 index e198fb3..0000000 --- a/src/endpoints/v1/user/about/doc.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::endpoints::v1::user::about::endpoint; -use utoipa::OpenApi; - -#[derive(OpenApi)] -#[openapi( - paths(endpoint::about), - components(schemas(super::about_response_view::AboutResponseView),) -)] -pub struct AboutDoc; diff --git a/src/endpoints/v1/user/about/endpoint.rs b/src/endpoints/v1/user/about/endpoint.rs deleted file mode 100644 index 0c4719a..0000000 --- a/src/endpoints/v1/user/about/endpoint.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::database::users::about::{about_user_query, AboutUserQueryView}; -use crate::endpoints::v1::user::about::about_request_view::{ - AboutPathParamRequestView, AboutRequestView, -}; -use crate::endpoints::v1::user::about::about_response_view::AboutResponseView; - -use actix_web::http::StatusCode; -use actix_web::{get, web, HttpResponse, Responder, ResponseError}; - -use mairie360_api_lib::database::queries::does_user_exist_by_id_query; - -use mairie360_api_lib::database::query_views::DoesUserExistByIdQueryView; - -use mairie360_api_lib::pool::redis::simple_key::secured::{handle_secure_get, handle_secure_post}; -use mairie360_api_lib::pool::AppState; -use serde_json; - -#[derive(Debug, Clone, PartialEq)] -enum AboutError { - UserNotFound, - DatabaseError, -} - -impl std::fmt::Display for AboutError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AboutError::UserNotFound => write!(f, "User not found."), - AboutError::DatabaseError => { - write!(f, "An error occurred while accessing the database.") - } - } - } -} - -impl ResponseError for AboutError { - fn status_code(&self) -> StatusCode { - match self { - AboutError::UserNotFound => StatusCode::UNAUTHORIZED, // On garde 401 selon tes specs - AboutError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code()).body(self.to_string()) - } -} - -// --- Cache Logic --- - -async fn get_cache_value(user_id: u64, state: &web::Data) -> Option { - match state.get_redis_conn().await { - Some(redis_manager) => { - let key = format!("user:{}:about", user_id); - - if let Ok(json_str) = handle_secure_get(redis_manager, &key).await { - // La désérialisation vers la struct valide automatiquement le format - serde_json::from_str::(&json_str).ok() - } else { - None - } - } - None => None, - } -} - -async fn set_cache_value(user_id: u64, data: &AboutResponseView, state: &web::Data) { - match state.get_redis_conn().await { - Some(redis_manager) => { - if let Ok(json_str) = serde_json::to_string(data) { - let key = format!("user:{}:about", user_id); - let _ = handle_secure_post(redis_manager, &key, &json_str).await; - } - } - None => {} - } -} - -async fn about_request( - about_view: &AboutRequestView, - state: web::Data, -) -> Result { - let user_id = about_view.user_id(); - - let exists = does_user_exist_by_id_query( - DoesUserExistByIdQueryView::new(user_id), - state.db_pool.clone().unwrap(), - ) - .await - .map_err(|e| { - eprintln!("Error checking existence for user {}: {}", user_id, e); - AboutError::DatabaseError - })?; - - if !exists { - return Err(AboutError::UserNotFound); - } - - // 1. Tentative via Cache - if let Some(cached) = get_cache_value(user_id, &state).await { - return Ok(cached); - } - - // 2. Query Database - let query_result = about_user_query( - AboutUserQueryView::new(user_id), - state.db_pool.clone().unwrap(), - ) - .await - .map_err(|_| AboutError::DatabaseError)?; - - // On transforme le résultat brut en AboutResponseView - // Si about_user_query renvoie déjà une structure compatible, on l'utilise - let response = AboutResponseView::from(query_result); - - // 3. Mise en cache et retour - set_cache_value(user_id, &response, &state).await; - Ok(response) -} - -#[utoipa::path( - get, - path = "{user_id}/about", - responses( - (status = 200, description = "User info retrieved successfully", body = AboutResponseView), - (status = 401, description = "Invalid user ID"), - (status = 500, description = "Internal server error") - ), - params( - ("user_id" = u64, Path, description = "The ID of the user"), - ), - tag = "Users", - security( - ("jwt" = []) - ) -)] -#[get("/{user_id}/about")] -pub async fn about( - path: web::Path, - state: web::Data, -) -> Result { - let about_view = path.into_inner(); - - let response_data = about_request(&AboutRequestView::new(about_view.user_id()), state).await?; - - Ok(HttpResponse::Ok().json(response_data)) -} diff --git a/src/endpoints/v1/user/about/mod.rs b/src/endpoints/v1/user/about/mod.rs deleted file mode 100644 index c02b646..0000000 --- a/src/endpoints/v1/user/about/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod about_request_view; -pub mod about_response_view; -pub mod doc; -pub mod endpoint; diff --git a/src/endpoints/v1/user/doc.rs b/src/endpoints/v1/user/doc.rs index 0f353d5..9005ac5 100644 --- a/src/endpoints/v1/user/doc.rs +++ b/src/endpoints/v1/user/doc.rs @@ -1,8 +1,9 @@ -use crate::endpoints::v1::user::about::doc::AboutDoc; +use crate::endpoints::v1::user::{id::doc::IdDoc, me::doc::MeDoc}; use utoipa::OpenApi; #[derive(OpenApi)] #[openapi(nest( - (path = "/", api = AboutDoc, tags = ["Users"]), + (path = "/me", api = MeDoc, tags = ["Users"]), + (path = "/{id}", api = IdDoc, tags = ["Users"]), ))] pub struct UserDoc; diff --git a/src/endpoints/v1/user/id/doc.rs b/src/endpoints/v1/user/id/doc.rs new file mode 100644 index 0000000..2548158 --- /dev/null +++ b/src/endpoints/v1/user/id/doc.rs @@ -0,0 +1,6 @@ +use crate::endpoints::v1::user::id::get::endpoint::__path_get; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi(paths(get), components())] +pub struct IdDoc; diff --git a/src/endpoints/v1/user/id/get/endpoint.rs b/src/endpoints/v1/user/id/get/endpoint.rs new file mode 100644 index 0000000..b47c3b6 --- /dev/null +++ b/src/endpoints/v1/user/id/get/endpoint.rs @@ -0,0 +1,75 @@ +use crate::database::users::get_user_by_id::{get_user_by_id_query, GetUserByIdQueryView}; +use crate::endpoints::v1::user::id::get::view::GetUserResponseView; +use actix_web::http::StatusCode; +use actix_web::{get, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; + +#[derive(Debug, Clone, PartialEq)] +enum GetUserError { + DatabaseError, + UnknownUser, +} + +impl std::fmt::Display for GetUserError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GetUserError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + GetUserError::UnknownUser => { + write!(f, "User not found.") + } + } + } +} + +impl ResponseError for GetUserError { + fn status_code(&self) -> StatusCode { + match self { + GetUserError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + GetUserError::UnknownUser => StatusCode::NOT_FOUND, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn get_user( + state: web::Data, + id: u64, +) -> Result { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(GetUserError::DatabaseError), + }; + + let view = GetUserByIdQueryView::new(id); + let result = get_user_by_id_query(view, pool) + .await + .map_err(|_| GetUserError::UnknownUser)?; + + Ok(result.into()) +} + +#[utoipa::path( + get, + path = "/", + responses( + (status = 200, description = "User retrieved successfully", body = GetUserResponseView), + (status = 500, description = "Internal server error") + ), + tag = "Users", + security( + ("jwt" = []) + ) +)] +#[get("/")] +pub async fn get( + state: web::Data, + id: web::Path, +) -> Result { + let user = get_user(state, id.parse::().unwrap_or(0)).await?; + Ok(HttpResponse::Ok().json(user)) +} diff --git a/src/endpoints/v1/user/id/get/mod.rs b/src/endpoints/v1/user/id/get/mod.rs new file mode 100644 index 0000000..cbeb677 --- /dev/null +++ b/src/endpoints/v1/user/id/get/mod.rs @@ -0,0 +1,2 @@ +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/user/id/get/view.rs b/src/endpoints/v1/user/id/get/view.rs new file mode 100644 index 0000000..d8ac93a --- /dev/null +++ b/src/endpoints/v1/user/id/get/view.rs @@ -0,0 +1,86 @@ +use crate::database::users::get_user_by_id::GetUserByIdQueryResultView; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GetUserResponseView { + first_name: String, + last_name: String, + email: String, + phone: String, + status: String, + is_archived: bool, +} + +impl GetUserResponseView { + pub fn new( + first_name: String, + last_name: String, + email: String, + phone: String, + status: String, + is_archived: bool, + ) -> Self { + GetUserResponseView { + first_name, + last_name, + email, + phone, + status, + is_archived, + } + } + + pub fn first_name(&self) -> &str { + &self.first_name + } + + pub fn last_name(&self) -> &str { + &self.last_name + } + + pub fn email(&self) -> &str { + &self.email + } + + pub fn phone(&self) -> &str { + &self.phone + } + + pub fn status(&self) -> &str { + &self.status + } + + pub fn is_archived(&self) -> bool { + self.is_archived + } +} + +impl Display for GetUserResponseView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "GetUserResponseView {{ first_name: {}, last_name: {}, email: {}, phone: {}, status: {}, is_archived: {} }}", + self.first_name, + self.last_name, + self.email, + self.phone, + self.status, + self.is_archived, + ) + } +} + +impl From for GetUserResponseView { + fn from(query_result: GetUserByIdQueryResultView) -> Self { + GetUserResponseView { + first_name: query_result.first_name().to_string(), + last_name: query_result.last_name().to_string(), + email: query_result.email().to_string(), + phone: query_result.phone_number().to_string(), + status: query_result.status().to_string(), + is_archived: query_result.is_archived(), + } + } +} diff --git a/src/endpoints/v1/user/id/mod.rs b/src/endpoints/v1/user/id/mod.rs new file mode 100644 index 0000000..c890d2b --- /dev/null +++ b/src/endpoints/v1/user/id/mod.rs @@ -0,0 +1,7 @@ +use actix_web::web; +pub mod doc; +pub mod get; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(web::scope("/{id}").service(get::endpoint::get)); +} diff --git a/src/endpoints/v1/user/me/doc.rs b/src/endpoints/v1/user/me/doc.rs new file mode 100644 index 0000000..04e03de --- /dev/null +++ b/src/endpoints/v1/user/me/doc.rs @@ -0,0 +1,10 @@ +use crate::endpoints::v1::user::me::get::endpoint::__path_get; +use crate::endpoints::v1::user::me::patch::endpoint::__path_patch; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi( + paths(get, patch), + components(schemas(super::get::view::GetMeResponseView, super::patch::view::PatchMeView)) +)] +pub struct MeDoc; diff --git a/src/endpoints/v1/user/me/get/endpoint.rs b/src/endpoints/v1/user/me/get/endpoint.rs new file mode 100644 index 0000000..12aac87 --- /dev/null +++ b/src/endpoints/v1/user/me/get/endpoint.rs @@ -0,0 +1,69 @@ +use crate::database::users::get_user_by_id::{get_user_by_id_query, GetUserByIdQueryView}; +use crate::endpoints::v1::user::me::get::view::GetMeResponseView; +use actix_web::http::StatusCode; +use actix_web::{get, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; +use mairie360_api_lib::security::AuthenticatedUser; + +#[derive(Debug, Clone, PartialEq)] +enum GetMeError { + DatabaseError, +} + +impl std::fmt::Display for GetMeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GetMeError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + } + } +} + +impl ResponseError for GetMeError { + fn status_code(&self) -> StatusCode { + match self { + GetMeError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn get_me(state: web::Data, user_id: u64) -> Result { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(GetMeError::DatabaseError), + }; + let view = GetUserByIdQueryView::new(user_id); + let result = get_user_by_id_query(view, pool).await.map_err(|e| { + eprintln!("Login DB Error: {}", e); + GetMeError::DatabaseError + })?; + + Ok(GetMeResponseView::from(result)) +} + +#[utoipa::path( + get, + path = "/", + responses( + (status = 200, description = "Me retrieved successfully", body = GetMeResponseView), + (status = 400, description = "Bad request"), + (status = 500, description = "Internal server error") + ), + tag = "Users", + security( + ("jwt" = []) + ) +)] +#[get("/")] +pub async fn get( + state: web::Data, + auth_user: AuthenticatedUser, +) -> Result { + let me = get_me(state, auth_user.id).await?; + Ok(HttpResponse::Ok().json(me)) +} diff --git a/src/endpoints/v1/user/me/get/mod.rs b/src/endpoints/v1/user/me/get/mod.rs new file mode 100644 index 0000000..cbeb677 --- /dev/null +++ b/src/endpoints/v1/user/me/get/mod.rs @@ -0,0 +1,2 @@ +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/user/about/about_response_view.rs b/src/endpoints/v1/user/me/get/view.rs similarity index 77% rename from src/endpoints/v1/user/about/about_response_view.rs rename to src/endpoints/v1/user/me/get/view.rs index 97660be..859f606 100644 --- a/src/endpoints/v1/user/about/about_response_view.rs +++ b/src/endpoints/v1/user/me/get/view.rs @@ -1,11 +1,10 @@ +use crate::database::users::get_user_by_id::GetUserByIdQueryResultView; use serde::{Deserialize, Serialize}; use std::fmt::Display; use utoipa::ToSchema; -use crate::database::users::about::AboutUserQueryResultView; - #[derive(Serialize, Deserialize, ToSchema)] -pub struct AboutResponseView { +pub struct GetMeResponseView { first_name: String, last_name: String, email: String, @@ -13,7 +12,7 @@ pub struct AboutResponseView { status: String, } -impl AboutResponseView { +impl GetMeResponseView { pub fn new( first_name: String, last_name: String, @@ -21,7 +20,7 @@ impl AboutResponseView { phone: String, status: String, ) -> Self { - AboutResponseView { + GetMeResponseView { first_name, last_name, email, @@ -51,11 +50,11 @@ impl AboutResponseView { } } -impl Display for AboutResponseView { +impl Display for GetMeResponseView { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "AboutResponseView {{ first_name: {}, last_name: {}, email: {}, phone: {}, status: {} }}", + "GetMeResponseView {{ first_name: {}, last_name: {}, email: {}, phone: {}, status: {} }}", self.first_name, self.last_name, self.email, @@ -65,9 +64,9 @@ impl Display for AboutResponseView { } } -impl From for AboutResponseView { - fn from(query_result: AboutUserQueryResultView) -> Self { - AboutResponseView { +impl From for GetMeResponseView { + fn from(query_result: GetUserByIdQueryResultView) -> Self { + GetMeResponseView { first_name: query_result.first_name().to_string(), last_name: query_result.last_name().to_string(), email: query_result.email().to_string(), diff --git a/src/endpoints/v1/user/me/mod.rs b/src/endpoints/v1/user/me/mod.rs new file mode 100644 index 0000000..89e8217 --- /dev/null +++ b/src/endpoints/v1/user/me/mod.rs @@ -0,0 +1,13 @@ +use actix_web::web; + +pub mod doc; +pub mod get; +pub mod patch; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/me") + .service(get::endpoint::get) + .service(patch::endpoint::patch), + ); +} diff --git a/src/endpoints/v1/user/me/patch/endpoint.rs b/src/endpoints/v1/user/me/patch/endpoint.rs new file mode 100644 index 0000000..1cc14dc --- /dev/null +++ b/src/endpoints/v1/user/me/patch/endpoint.rs @@ -0,0 +1,80 @@ +use actix_web::http::StatusCode; +use actix_web::{patch, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; +use mairie360_api_lib::security::AuthenticatedUser; + +use crate::database::users::patch_user::{patch_user_query, PatchUserQueryView}; +use crate::endpoints::v1::user::me::patch::view::PatchMeView; + +#[derive(Debug, Clone, PartialEq)] +enum PatchMeError { + DatabaseError, +} + +impl std::fmt::Display for PatchMeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PatchMeError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + } + } +} + +impl ResponseError for PatchMeError { + fn status_code(&self) -> StatusCode { + match self { + PatchMeError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn patch_me( + state: web::Data, + view: PatchMeView, + user_id: u64, +) -> Result<(), PatchMeError> { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(PatchMeError::DatabaseError), + }; + + let db_view = PatchUserQueryView::new( + user_id, + view.first_name(), + view.last_name(), + view.email(), + view.phone(), + ); + patch_user_query(db_view, &pool).await.map_err(|e| { + eprintln!("Error: {:?}", e); + PatchMeError::DatabaseError + })?; + Ok(()) +} + +#[utoipa::path( + patch, + path = "/", + responses( + (status = 200, description = "User updated successfully"), + (status = 500, description = "Internal server error") + ), + tag = "Users", + security( + ("jwt" = []) + ) +)] +#[patch("/")] +pub async fn patch( + state: web::Data, + view: web::Json, + auth_user: AuthenticatedUser, +) -> Result { + patch_me(state, view.into_inner(), auth_user.id).await?; + Ok(HttpResponse::Ok()) +} diff --git a/src/endpoints/v1/user/me/patch/mod.rs b/src/endpoints/v1/user/me/patch/mod.rs new file mode 100644 index 0000000..cbeb677 --- /dev/null +++ b/src/endpoints/v1/user/me/patch/mod.rs @@ -0,0 +1,2 @@ +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/user/me/patch/view.rs b/src/endpoints/v1/user/me/patch/view.rs new file mode 100644 index 0000000..a7d4412 --- /dev/null +++ b/src/endpoints/v1/user/me/patch/view.rs @@ -0,0 +1,53 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use utoipa::ToSchema; + +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct PatchMeView { + first_name: Option, + last_name: Option, + email: Option, + phone: Option, +} + +impl PatchMeView { + pub fn new( + first_name: Option, + last_name: Option, + email: Option, + phone: Option, + ) -> Self { + Self { + first_name, + last_name, + email, + phone, + } + } + + pub fn first_name(&self) -> Option<&str> { + self.first_name.as_deref() + } + + pub fn last_name(&self) -> Option<&str> { + self.last_name.as_deref() + } + + pub fn email(&self) -> Option<&str> { + self.email.as_deref() + } + + pub fn phone(&self) -> Option<&str> { + self.phone.as_deref() + } +} + +impl Display for PatchMeView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "PatchMeView {{ first_name: {:?}, last_name: {:?}, email: {:?}, phone: {:?} }}", + self.first_name, self.last_name, self.email, self.phone + ) + } +} diff --git a/src/endpoints/v1/user/mod.rs b/src/endpoints/v1/user/mod.rs index 7a0a59e..d9b3b28 100644 --- a/src/endpoints/v1/user/mod.rs +++ b/src/endpoints/v1/user/mod.rs @@ -1,8 +1,13 @@ -pub mod about; pub mod doc; +pub mod id; +pub mod me; use actix_web::web; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service(web::scope("/user").service(about::endpoint::about)); + cfg.service( + web::scope("/user") + .configure(me::config) + .configure(id::config), + ); }