From f6232832af4c04d5b5628169815fa01dd4e351ad Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Wed, 13 Aug 2025 20:40:02 +0700 Subject: [PATCH] feat(users): Enhance user schema and DTOs with additional fields for website, social links, location, skills, experience, and education --- imphnen-backend/src/bin/seed_users.rs | 6 + .../src/v1/mentors/mentors_dto.rs | 1 + imphnen-dimentorin/src/v1/mentors/mod.rs | 2 +- .../v1/gacha_items/gacha_items_controller.rs | 6 +- .../src/v1/gacha_items/gacha_items_dto.rs | 10 ++ .../src/v1/gacha_items/gacha_items_schema.rs | 4 + .../src/v1/gacha_items/gacha_items_service.rs | 30 ++-- imphnen-iam/src/v1/auth/auth_repository.rs | 6 + .../v1/permissions/permissions_controller.rs | 6 +- .../src/v1/permissions/permissions_dto.rs | 7 + .../src/v1/permissions/permissions_schema.rs | 12 +- .../src/v1/permissions/permissions_service.rs | 31 ++-- imphnen-iam/src/v1/users/users_dto.rs | 99 ++++++++++++- imphnen-iam/src/v1/users/users_schema.rs | 136 +++++++++++++++--- imphnen-iam/src/v1/users/users_service.rs | 14 +- tests/src/lib.rs | 6 + tests/src/mock_test.rs | 6 + 17 files changed, 327 insertions(+), 55 deletions(-) diff --git a/imphnen-backend/src/bin/seed_users.rs b/imphnen-backend/src/bin/seed_users.rs index 3ae5f9f..3052e59 100644 --- a/imphnen-backend/src/bin/seed_users.rs +++ b/imphnen-backend/src/bin/seed_users.rs @@ -65,6 +65,12 @@ async fn main() -> Result<(), Box> { github_url: Some("https://github.com/user".into()), cv_url: Some("https://example.com/cv.pdf".into()), portfolio_url: Some("https://example.com/portfolio".into()), + website_url: Some("https://example.com/website".into()), + twitter_url: Some("https://twitter.com/user".into()), + location: Some("Jakarta, Indonesia".into()), + skills: Some(vec!["JavaScript".into(), "React".into(), "Node.js".into()]), + experience: None, + education: None, role: Thing::from(("app_roles", role_id)), created_at: get_iso_date(), updated_at: get_iso_date(), diff --git a/imphnen-dimentorin/src/v1/mentors/mentors_dto.rs b/imphnen-dimentorin/src/v1/mentors/mentors_dto.rs index 2e5b98c..549edc2 100644 --- a/imphnen-dimentorin/src/v1/mentors/mentors_dto.rs +++ b/imphnen-dimentorin/src/v1/mentors/mentors_dto.rs @@ -114,6 +114,7 @@ pub struct MentorUpdateRequestDto { #[validate(url(message = "Invalid CV URL"))] #[serde(skip_serializing_if = "Option::is_none")] pub cv_url: Option, + #[validate(url(message = "Invalid portfolio URL"))] #[serde(skip_serializing_if = "Option::is_none")] pub portfolio_url: Option, #[validate(length(min = 1, message = "At least 1 industry required"))] diff --git a/imphnen-dimentorin/src/v1/mentors/mod.rs b/imphnen-dimentorin/src/v1/mentors/mod.rs index d530b75..63f2c94 100644 --- a/imphnen-dimentorin/src/v1/mentors/mod.rs +++ b/imphnen-dimentorin/src/v1/mentors/mod.rs @@ -26,5 +26,5 @@ pub fn mentors_router() -> Router { .route("/update/{id}", put(put_update_mentor)) .route("/update", put(put_update_mentor_no_id)) .route("/delete/{id}", delete(delete_mentor)) - .route("/verify/{id}", put(put(put_verify_mentor))) + .route("/verify/{id}", put(put_verify_mentor)) } diff --git a/imphnen-gacha/src/v1/gacha_items/gacha_items_controller.rs b/imphnen-gacha/src/v1/gacha_items/gacha_items_controller.rs index 310d601..c8bbb64 100644 --- a/imphnen-gacha/src/v1/gacha_items/gacha_items_controller.rs +++ b/imphnen-gacha/src/v1/gacha_items/gacha_items_controller.rs @@ -1,5 +1,5 @@ use crate::{ - AppState, GachaItemDto, GachaItemRequestDto, GachaItemService, MessageResponseDto, + AppState, GachaItemDto, GachaItemRequestDto, GachaItemUpdateRequestDto, GachaItemService, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto, }; use axum::{ @@ -111,7 +111,7 @@ pub async fn post_create_gacha_item( security( ("Bearer" = []) ), - request_body = GachaItemRequestDto, + request_body = GachaItemUpdateRequestDto, responses( (status = 200, description = "Update gacha item", body = MessageResponseDto) ), @@ -121,7 +121,7 @@ pub async fn put_update_gacha_item( headers: HeaderMap, Extension(state): Extension, Path(id): Path, - Json(payload): Json, + Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( &headers, diff --git a/imphnen-gacha/src/v1/gacha_items/gacha_items_dto.rs b/imphnen-gacha/src/v1/gacha_items/gacha_items_dto.rs index 589a29f..9c7d823 100644 --- a/imphnen-gacha/src/v1/gacha_items/gacha_items_dto.rs +++ b/imphnen-gacha/src/v1/gacha_items/gacha_items_dto.rs @@ -11,6 +11,16 @@ pub struct GachaItemRequestDto { pub image_url: String, } +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct GachaItemUpdateRequestDto { + #[validate(length(min = 1, message = "Item name must not be empty"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[validate(length(min = 1, message = "Image URL must not be empty"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub image_url: Option, +} + #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct GachaItemDto { pub id: String, diff --git a/imphnen-gacha/src/v1/gacha_items/gacha_items_schema.rs b/imphnen-gacha/src/v1/gacha_items/gacha_items_schema.rs index 86aedec..bdfd216 100644 --- a/imphnen-gacha/src/v1/gacha_items/gacha_items_schema.rs +++ b/imphnen-gacha/src/v1/gacha_items/gacha_items_schema.rs @@ -43,4 +43,8 @@ impl GachaItemSchema { ..Default::default() } } + + pub fn from_existing(existing: GachaItemSchema) -> Self { + existing + } } diff --git a/imphnen-gacha/src/v1/gacha_items/gacha_items_service.rs b/imphnen-gacha/src/v1/gacha_items/gacha_items_service.rs index 0afa2a5..05aabfe 100644 --- a/imphnen-gacha/src/v1/gacha_items/gacha_items_service.rs +++ b/imphnen-gacha/src/v1/gacha_items/gacha_items_service.rs @@ -1,11 +1,12 @@ use crate::{ - AppState, GachaItemDto, GachaItemRepository, GachaItemRequestDto, GachaItemSchema, + AppState, GachaItemDto, GachaItemRepository, GachaItemRequestDto, GachaItemUpdateRequestDto, GachaItemSchema, MetaRequestDto, ResourceEnum, ResponseListSuccessDto, ResponseSuccessDto, common_response, make_thing, success_list_response, success_response, validate_request, }; use axum::http::StatusCode; use axum::response::Response; +use imphnen_utils::get_iso_date; pub struct GachaItemService; @@ -59,20 +60,33 @@ impl GachaItemService { pub async fn update_gacha_item( state: &AppState, - payload: GachaItemRequestDto, + payload: GachaItemUpdateRequestDto, id: String, ) -> Response { if let Err((status, message)) = validate_request(&payload) { return common_response(status, &message); } let repo = GachaItemRepository::new(state); - let schema = GachaItemSchema { - id: make_thing(&ResourceEnum::GachaItems.to_string(), &id), - name: payload.name, - image_url: payload.image_url, - ..Default::default() + + // Get current gacha item data first + let _thing_id = make_thing(&ResourceEnum::GachaItems.to_string(), &id); + let current_item = match repo.query_gacha_item_by_id(id.clone()).await { + Ok(item) => item, + Err(_) => return common_response(StatusCode::NOT_FOUND, "Gacha Item not found"), }; - match repo.query_update_gacha_item(schema).await { + + let mut updated_item = current_item; + updated_item.updated_at = Some(get_iso_date()); + + // Only update fields that are provided + if let Some(name) = payload.name { + updated_item.name = name; + } + if let Some(image_url) = payload.image_url { + updated_item.image_url = image_url; + } + + match repo.query_update_gacha_item(updated_item).await { Ok(msg) => common_response(StatusCode::OK, &msg), Err(e) => { if e.to_string().contains("not found") { diff --git a/imphnen-iam/src/v1/auth/auth_repository.rs b/imphnen-iam/src/v1/auth/auth_repository.rs index 5b877ee..0ac14d8 100644 --- a/imphnen-iam/src/v1/auth/auth_repository.rs +++ b/imphnen-iam/src/v1/auth/auth_repository.rs @@ -110,6 +110,12 @@ impl<'a> AuthRepository<'a> { github_url: None, cv_url: None, portfolio_url: None, + website_url: None, + twitter_url: None, + location: None, + skills: None, + experience: None, + education: None, password: String::new(), role: role_detail_query_dto, created_at: String::new(), diff --git a/imphnen-iam/src/v1/permissions/permissions_controller.rs b/imphnen-iam/src/v1/permissions/permissions_controller.rs index 60708ec..b626421 100644 --- a/imphnen-iam/src/v1/permissions/permissions_controller.rs +++ b/imphnen-iam/src/v1/permissions/permissions_controller.rs @@ -8,7 +8,7 @@ use crate::{ AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto, v1::{ - permissions_dto::{PermissionsItemDto, PermissionsRequestDto}, + permissions_dto::{PermissionsItemDto, PermissionsRequestDto, PermissionsUpdateRequestDto}, permissions_service::PermissionsService, }, }; @@ -116,7 +116,7 @@ pub async fn post_create_permission( ("Bearer" = []) ), path = "/v1/permissions/update/{id}", - request_body = PermissionsRequestDto, + request_body = PermissionsUpdateRequestDto, responses( (status = 200, description = "Update permission", body = MessageResponseDto) ), @@ -126,7 +126,7 @@ pub async fn put_update_permission( headers: axum::http::HeaderMap, Extension(state): Extension, Path(id): Path, - Json(payload): Json, + Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( &headers, diff --git a/imphnen-iam/src/v1/permissions/permissions_dto.rs b/imphnen-iam/src/v1/permissions/permissions_dto.rs index 19aaf3b..84cae19 100644 --- a/imphnen-iam/src/v1/permissions/permissions_dto.rs +++ b/imphnen-iam/src/v1/permissions/permissions_dto.rs @@ -9,6 +9,13 @@ pub struct PermissionsRequestDto { pub name: String, } +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct PermissionsUpdateRequestDto { + #[validate(length(min = 1, message = "Permission name must not be empty"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct PermissionsItemDto { pub id: String, diff --git a/imphnen-iam/src/v1/permissions/permissions_schema.rs b/imphnen-iam/src/v1/permissions/permissions_schema.rs index ef86d95..53a76b9 100644 --- a/imphnen-iam/src/v1/permissions/permissions_schema.rs +++ b/imphnen-iam/src/v1/permissions/permissions_schema.rs @@ -2,7 +2,7 @@ use crate::{ResourceEnum, make_thing}; use serde::{Deserialize, Serialize}; use surrealdb::{Uuid, sql::Thing}; -use super::PermissionsItemDto; +use super::{PermissionsItemDto, PermissionsQueryDto}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PermissionsSchema { @@ -37,4 +37,14 @@ impl PermissionsSchema { updated_at: self.updated_at.clone(), } } + + pub fn from(dto: PermissionsQueryDto) -> Self { + Self { + id: dto.id, + name: dto.name, + is_deleted: false, + created_at: dto.created_at, + updated_at: dto.updated_at, + } + } } diff --git a/imphnen-iam/src/v1/permissions/permissions_service.rs b/imphnen-iam/src/v1/permissions/permissions_service.rs index 7d8f730..094cd40 100644 --- a/imphnen-iam/src/v1/permissions/permissions_service.rs +++ b/imphnen-iam/src/v1/permissions/permissions_service.rs @@ -5,8 +5,9 @@ use crate::{ }; use axum::http::StatusCode; use axum::response::Response; +use imphnen_utils::get_iso_date; -use super::PermissionsRequestDto; +use super::{PermissionsRequestDto, PermissionsUpdateRequestDto}; pub struct PermissionsService; @@ -70,21 +71,31 @@ impl PermissionsService { pub async fn update_permission( state: &AppState, - payload: PermissionsRequestDto, + payload: PermissionsUpdateRequestDto, id: String, ) -> Response { if let Err((status, message)) = validate_request(&payload) { return common_response(status, &message); } let repo = PermissionsRepository::new(state); - match repo - .query_update_permission(PermissionsSchema { - id: make_thing(&ResourceEnum::Permissions.to_string(), &id), - name: payload.name, - ..Default::default() - }) - .await - { + + // Get current permission data first + let thing_id = make_thing(&ResourceEnum::Permissions.to_string(), &id); + let current_permission = match repo.query_permission_by_id(id.clone()).await { + Ok(permission) => permission, + Err(_) => return common_response(StatusCode::NOT_FOUND, "Permission not found"), + }; + + let mut updated_permission = current_permission; + updated_permission.id = thing_id; + updated_permission.updated_at = Some(get_iso_date()); + + // Only update fields that are provided + if let Some(name) = payload.name { + updated_permission.name = name; + } + + match repo.query_update_permission(updated_permission).await { Ok(msg) => common_response(StatusCode::OK, &msg), Err(e) => { if e.to_string().contains("not found") { diff --git a/imphnen-iam/src/v1/users/users_dto.rs b/imphnen-iam/src/v1/users/users_dto.rs index cfb4dcc..33b9df5 100644 --- a/imphnen-iam/src/v1/users/users_dto.rs +++ b/imphnen-iam/src/v1/users/users_dto.rs @@ -11,6 +11,24 @@ lazy_static! { regex::Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap(); } +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct ExperienceDto { + pub id: String, + pub company: String, + pub position: String, + pub duration: String, + pub period: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct EducationDto { + pub id: String, + pub institution: String, + pub degree: String, + pub field: String, + pub period: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct UsersActiveInactiveRequestDto { pub is_active: bool, @@ -59,43 +77,74 @@ pub struct UsersUpdateRequestDto { length(min = 1, message = "Email cannot be empty"), email(message = "Email not valid") )] - pub email: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, #[validate(length( min = 8, message = "Password must have at least 8 characters" ))] - pub password: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub password: Option, #[validate(length(min = 2, message = "Fullname at least have 2 character"))] - pub fullname: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub fullname: Option, #[validate(length(min = 2, message = "Legal name at least have 2 character"))] + #[serde(skip_serializing_if = "Option::is_none")] pub legal_name: Option, #[validate(length( min = 10, message = "Phone number at least have 10 character" ))] - pub phone_number: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub phone_number: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub phone_for_verification: Option, - pub is_active: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_active: Option, #[validate(length(min = 1, message = "Gender is required"))] + #[serde(skip_serializing_if = "Option::is_none")] pub gender: Option, #[validate(length(min = 1, message = "Birthdate is required"))] + #[serde(skip_serializing_if = "Option::is_none")] pub birthdate: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub domicile: Option, - #[validate(url(message = "Invalid identity document URL"))] #[validate(length(min = 50, message = "Bio must be at least 50 characters"))] + #[serde(skip_serializing_if = "Option::is_none")] pub bio: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub last_education: Option, #[validate(url(message = "Invalid LinkedIn URL"))] + #[serde(skip_serializing_if = "Option::is_none")] pub linkedin_url: Option, #[validate(url(message = "Invalid GitHub URL"))] + #[serde(skip_serializing_if = "Option::is_none")] pub github_url: Option, #[validate(url(message = "Invalid CV URL"))] + #[serde(skip_serializing_if = "Option::is_none")] pub cv_url: Option, #[validate(url(message = "Invalid portfolio URL"))] + #[serde(skip_serializing_if = "Option::is_none")] pub portfolio_url: Option, + #[validate(url(message = "Invalid website URL"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub website_url: Option, + #[validate(url(message = "Invalid Twitter URL"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub twitter_url: Option, #[validate(length(min = 1, message = "Avatar is required"))] + #[serde(skip_serializing_if = "Option::is_none")] pub avatar: Option, - pub role_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub role_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub skills: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub experience: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub education: Option>, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] @@ -118,6 +167,12 @@ pub struct UsersDetailItemDto { pub github_url: Option, pub cv_url: Option, pub portfolio_url: Option, + pub website_url: Option, + pub twitter_url: Option, + pub location: Option, + pub skills: Option>, + pub experience: Option>, + pub education: Option>, pub created_at: String, pub updated_at: String, } @@ -143,6 +198,12 @@ impl UsersDetailItemDto { github_url: dto.github_url.clone(), cv_url: dto.cv_url.clone(), portfolio_url: dto.portfolio_url.clone(), + website_url: dto.website_url.clone(), + twitter_url: dto.twitter_url.clone(), + location: dto.location.clone(), + skills: dto.skills.clone(), + experience: dto.experience.clone(), + education: dto.education.clone(), created_at: dto.created_at.clone(), updated_at: dto.updated_at.clone(), } @@ -168,6 +229,12 @@ impl UsersDetailItemDto { github_url: schema.github_url.clone(), cv_url: schema.cv_url.clone(), portfolio_url: schema.portfolio_url.clone(), + website_url: schema.website_url.clone(), + twitter_url: schema.twitter_url.clone(), + location: schema.location.clone(), + skills: schema.skills.clone(), + experience: schema.experience.clone(), + education: schema.education.clone(), created_at: schema.created_at.clone(), updated_at: schema.updated_at.clone(), } @@ -236,6 +303,12 @@ pub struct UsersDetailQueryDto { pub github_url: Option, pub cv_url: Option, pub portfolio_url: Option, + pub website_url: Option, + pub twitter_url: Option, + pub location: Option, + pub skills: Option>, + pub experience: Option>, + pub education: Option>, pub password: String, pub role: RolesDetailQueryDto, pub created_at: String, @@ -264,6 +337,12 @@ impl UsersDetailQueryDto { github_url: self.github_url.clone(), cv_url: self.cv_url.clone(), portfolio_url: self.portfolio_url.clone(), + website_url: self.website_url.clone(), + twitter_url: self.twitter_url.clone(), + location: self.location.clone(), + skills: self.skills.clone(), + experience: self.experience.clone(), + education: self.education.clone(), is_deleted: self.is_deleted, password: self.password.clone(), birthdate: self.birthdate.clone(), @@ -294,6 +373,12 @@ impl From<&UsersDetailItemDto> for UsersDetailQueryDto { github_url: dto.github_url.clone(), cv_url: dto.cv_url.clone(), portfolio_url: dto.portfolio_url.clone(), + website_url: dto.website_url.clone(), + twitter_url: dto.twitter_url.clone(), + location: dto.location.clone(), + skills: dto.skills.clone(), + experience: dto.experience.clone(), + education: dto.education.clone(), password: String::new(), role: RolesDetailQueryDto::default(), created_at: dto.created_at.clone(), diff --git a/imphnen-iam/src/v1/users/users_schema.rs b/imphnen-iam/src/v1/users/users_schema.rs index 75ccf2e..cbada14 100644 --- a/imphnen-iam/src/v1/users/users_schema.rs +++ b/imphnen-iam/src/v1/users/users_schema.rs @@ -1,4 +1,4 @@ -use super::{UsersCreateRequestDto, UsersDetailQueryDto, UsersUpdateRequestDto}; +use super::{UsersCreateRequestDto, UsersDetailQueryDto, UsersUpdateRequestDto, ExperienceDto, EducationDto}; use imphnen_libs::{ResourceEnum, hash_password}; use imphnen_utils::extract_id; use imphnen_utils::{get_iso_date, make_thing}; @@ -40,6 +40,18 @@ pub struct UsersSchema { pub cv_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub portfolio_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub website_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub twitter_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub skills: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub experience: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub education: Option>, pub role: Thing, pub created_at: String, pub updated_at: String, @@ -71,6 +83,12 @@ impl Default for UsersSchema { github_url: None, cv_url: None, portfolio_url: None, + website_url: None, + twitter_url: None, + location: None, + skills: None, + experience: None, + education: None, role: make_thing( &ResourceEnum::Roles.to_string(), "5713cb37-dc02-4e87-8048-d7a41d352059", @@ -103,6 +121,12 @@ impl UsersSchema { github_url: dto.github_url, cv_url: dto.cv_url, portfolio_url: dto.portfolio_url, + website_url: dto.website_url, + twitter_url: dto.twitter_url, + location: dto.location, + skills: dto.skills, + experience: dto.experience, + education: dto.education, password: dto.password, created_at: dto.created_at, updated_at: dto.updated_at, @@ -110,32 +134,98 @@ impl UsersSchema { } } - pub fn update(user: UsersUpdateRequestDto, id: String) -> Self { + pub fn update(_user: UsersUpdateRequestDto, id: String) -> Self { Self { id: make_thing(&ResourceEnum::Users.to_string(), &id), - fullname: user.fullname, - legal_name: user.legal_name, - email: user.email, - phone_number: user.phone_number, - phone_for_verification: user.phone_for_verification, - is_active: user.is_active, - gender: user.gender, - birthdate: user.birthdate, - domicile: user.domicile, - bio: user.bio, - last_education: user.last_education, - linkedin_url: user.linkedin_url, - github_url: user.github_url, - cv_url: user.cv_url, - portfolio_url: user.portfolio_url, - avatar: user.avatar, - is_deleted: false, - role: make_thing(&ResourceEnum::Roles.to_string(), &user.role_id), updated_at: get_iso_date(), + // Set defaults for required fields - these should be overridden by actual data from DB ..Default::default() } } + pub fn partial_update(current_user: UsersDetailQueryDto, user: UsersUpdateRequestDto) -> Self { + let mut schema = Self::from(current_user); + schema.updated_at = get_iso_date(); + + // Only update fields that are provided (Some) + if let Some(fullname) = user.fullname { + schema.fullname = fullname; + } + if let Some(email) = user.email { + schema.email = email; + } + if let Some(password) = user.password { + schema.password = hash_password(&password).unwrap_or_else(|_| password); + } + if let Some(phone_number) = user.phone_number { + schema.phone_number = phone_number; + } + if let Some(is_active) = user.is_active { + schema.is_active = is_active; + } + if let Some(role_id) = user.role_id { + schema.role = make_thing(&ResourceEnum::Roles.to_string(), &role_id); + } + + // Optional fields - only update if provided + if let Some(legal_name) = user.legal_name { + schema.legal_name = Some(legal_name); + } + if let Some(phone_for_verification) = user.phone_for_verification { + schema.phone_for_verification = Some(phone_for_verification); + } + if let Some(gender) = user.gender { + schema.gender = Some(gender); + } + if let Some(birthdate) = user.birthdate { + schema.birthdate = Some(birthdate); + } + if let Some(domicile) = user.domicile { + schema.domicile = Some(domicile); + } + if let Some(bio) = user.bio { + schema.bio = Some(bio); + } + if let Some(last_education) = user.last_education { + schema.last_education = Some(last_education); + } + if let Some(linkedin_url) = user.linkedin_url { + schema.linkedin_url = Some(linkedin_url); + } + if let Some(github_url) = user.github_url { + schema.github_url = Some(github_url); + } + if let Some(cv_url) = user.cv_url { + schema.cv_url = Some(cv_url); + } + if let Some(portfolio_url) = user.portfolio_url { + schema.portfolio_url = Some(portfolio_url); + } + if let Some(website_url) = user.website_url { + schema.website_url = Some(website_url); + } + if let Some(twitter_url) = user.twitter_url { + schema.twitter_url = Some(twitter_url); + } + if let Some(location) = user.location { + schema.location = Some(location); + } + if let Some(skills) = user.skills { + schema.skills = Some(skills); + } + if let Some(experience) = user.experience { + schema.experience = Some(experience); + } + if let Some(education) = user.education { + schema.education = Some(education); + } + if let Some(avatar) = user.avatar { + schema.avatar = Some(avatar); + } + + schema + } + pub fn create(user: UsersCreateRequestDto) -> Self { let password = hash_password(&user.password).unwrap(); Self { @@ -160,6 +250,12 @@ impl UsersSchema { github_url: None, cv_url: None, portfolio_url: None, + website_url: None, + twitter_url: None, + location: None, + skills: None, + experience: None, + education: None, avatar: user.avatar, is_deleted: false, role: make_thing(&ResourceEnum::Roles.to_string(), &user.role_id), diff --git a/imphnen-iam/src/v1/users/users_service.rs b/imphnen-iam/src/v1/users/users_service.rs index 4f11aa7..148a0d9 100644 --- a/imphnen-iam/src/v1/users/users_service.rs +++ b/imphnen-iam/src/v1/users/users_service.rs @@ -130,7 +130,15 @@ impl UsersServiceTrait for UsersService { if let Err((status, message)) = validate_request(&user) { return common_response(status, &message); } - let updated_user = UsersSchema::update(user, id); + + // Get current user data first + let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id); + let current_user = match repo.query_user_by_id(&thing_id).await { + Ok(user) => user, + Err(_) => return common_response(StatusCode::NOT_FOUND, "User not found"), + }; + + let updated_user = UsersSchema::partial_update(current_user, user); match repo.query_update_user(updated_user).await { Ok(msg) => common_response(StatusCode::OK, &msg), Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), @@ -160,10 +168,12 @@ impl UsersServiceTrait for UsersService { Ok(user) => user, Err(_) => return common_response(StatusCode::NOT_FOUND, "User not found"), }; + if let Err((status, message)) = validate_request(&user) { return common_response(status, &message); } - let updated_user = UsersSchema::update(user, user_data.id.id.to_raw()); + + let updated_user = UsersSchema::partial_update(user_data, user); match repo.query_update_user(updated_user).await { Ok(msg) => common_response(StatusCode::OK, &msg), Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 5507a43..a80d85a 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -37,6 +37,12 @@ pub fn create_test_user( github_url: None, cv_url: None, portfolio_url: None, + website_url: None, + twitter_url: None, + location: None, + skills: None, + experience: None, + education: None, role: make_thing("app_roles", role_id), created_at: get_iso_date(), updated_at: get_iso_date(), diff --git a/tests/src/mock_test.rs b/tests/src/mock_test.rs index 719f6d2..d383740 100644 --- a/tests/src/mock_test.rs +++ b/tests/src/mock_test.rs @@ -167,6 +167,12 @@ pub async fn seed_users_for_test( github_url: None, cv_url: None, portfolio_url: None, + website_url: None, + twitter_url: None, + location: None, + skills: None, + experience: None, + education: None, role: Thing::from(("app_roles", role_id)), created_at: get_iso_date(), updated_at: get_iso_date(),