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/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( 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), + ); }