diff --git a/imphnen-backend/Cargo.toml b/imphnen-backend/Cargo.toml index aa97468..283f7dd 100644 --- a/imphnen-backend/Cargo.toml +++ b/imphnen-backend/Cargo.toml @@ -3,6 +3,46 @@ name = "imphnen-backend" version = "0.1.0" edition = "2021" +[[bin]] +name = "api" +path = "src/main.rs" + +[[bin]] +name = "clear_db_test" +path = "src/bin/clear_db_test.rs" + +[[bin]] +name = "seeder" +path = "src/bin/seeder.rs" + +[[bin]] +name = "seed_events" +path = "src/bin/seed_events.rs" + +[[bin]] +name = "seed_gacha_rolls" +path = "src/bin/seed_gacha_rolls.rs" + +[[bin]] +name = "seed_mentor_user" +path = "src/bin/seed_mentor_user.rs" + +[[bin]] +name = "seed_permissions" +path = "src/bin/seed_permissions.rs" + +[[bin]] +name = "seed_roles" +path = "src/bin/seed_roles.rs" + +[[bin]] +name = "seed_roles_permissions" +path = "src/bin/seed_roles_permissions.rs" + +[[bin]] +name = "seed_users" +path = "src/bin/seed_users.rs" + [dependencies] imphnen-libs.workspace = true imphnen-utils.workspace = true diff --git a/imphnen-backend/src/bin/seed_permissions.rs b/imphnen-backend/src/bin/seed_permissions.rs index c96cfe9..24958f2 100644 --- a/imphnen-backend/src/bin/seed_permissions.rs +++ b/imphnen-backend/src/bin/seed_permissions.rs @@ -51,6 +51,7 @@ async fn main() -> Result<(), Box> { PermissionsEnum::ReadOwnMentorProfile, PermissionsEnum::UpdateOwnMentorProfile, PermissionsEnum::ReadOwnMentorStatus, + PermissionsEnum::UpdateMentors, PermissionsEnum::VerifyMentors, PermissionsEnum::DeleteMentors, ] { diff --git a/imphnen-backend/src/bin/seed_users.rs b/imphnen-backend/src/bin/seed_users.rs index 06570b5..a6ef540 100644 --- a/imphnen-backend/src/bin/seed_users.rs +++ b/imphnen-backend/src/bin/seed_users.rs @@ -46,15 +46,25 @@ async fn main() -> Result<(), Box> { let user = UsersSchema { id: Thing::from(("app_users", id)), fullname: fullname.into(), + legal_name: None, email: email.into(), password: hash_password("password").unwrap(), avatar: None, phone_number: "081234567890".into(), + phone_for_verification: None, is_active: true, is_deleted: false, mentor_id: None, gender: None, birthdate: None, + domicile: None, + identity_document_url: None, + bio: None, + last_education: None, + linkedin_url: None, + github_url: None, + cv_url: None, + portfolio_url: None, role: Thing::from(("app_roles", role_id)), created_at: get_iso_date(), updated_at: get_iso_date(), diff --git a/imphnen-iam/src/v1/auth/auth_repository.rs b/imphnen-iam/src/v1/auth/auth_repository.rs index 34c6a08..a4e92a3 100644 --- a/imphnen-iam/src/v1/auth/auth_repository.rs +++ b/imphnen-iam/src/v1/auth/auth_repository.rs @@ -94,13 +94,23 @@ impl<'a> AuthRepository<'a> { Ok(UsersDetailQueryDto { id: Thing::from(("app_users".to_string(), email.clone())), fullname: "Cached User".to_string(), + legal_name: None, email: cache.email, avatar: None, phone_number: String::new(), + phone_for_verification: None, is_active: true, is_deleted: false, gender: None, birthdate: None, + domicile: None, + identity_document_url: None, + bio: None, + last_education: None, + linkedin_url: None, + github_url: None, + cv_url: None, + portfolio_url: None, password: String::new(), role: role_detail_query_dto, created_at: String::new(), diff --git a/imphnen-iam/src/v1/auth/google/google_oauth_service.rs b/imphnen-iam/src/v1/auth/google/google_oauth_service.rs index 2f53fcc..0d16c03 100644 --- a/imphnen-iam/src/v1/auth/google/google_oauth_service.rs +++ b/imphnen-iam/src/v1/auth/google/google_oauth_service.rs @@ -215,9 +215,15 @@ where // Update avatar if user doesn't have one and Google provides one if user.avatar.is_none() && google_user.picture.is_some() { info!("Updating avatar for existing user: {}", google_user.email); - // Note: We would need to implement an update_user_avatar method in the user service - // For now, we'll just log this - info!("Avatar would be updated to: {:?}", google_user.picture); + match self.users_service.update_user_avatar(&google_user.email, google_user.picture.clone()).await { + Ok(_) => { + info!("Successfully updated avatar for user: {}", google_user.email); + user.avatar = google_user.picture.clone(); + }, + Err(e) => { + error!("Failed to update avatar for user {}: {:?}", google_user.email, e); + } + } } user diff --git a/imphnen-iam/src/v1/users/users_dto.rs b/imphnen-iam/src/v1/users/users_dto.rs index 877751d..e296b4e 100644 --- a/imphnen-iam/src/v1/users/users_dto.rs +++ b/imphnen-iam/src/v1/users/users_dto.rs @@ -67,16 +67,33 @@ pub struct UsersUpdateRequestDto { pub password: String, #[validate(length(min = 2, message = "Fullname at least have 2 character"))] pub fullname: String, + #[validate(length(min = 2, message = "Legal name at least have 2 character"))] + pub legal_name: Option, #[validate(length( min = 10, message = "Phone number at least have 10 character" ))] pub phone_number: String, + pub phone_for_verification: Option, pub is_active: bool, #[validate(length(min = 1, message = "Gender is required"))] pub gender: Option, #[validate(length(min = 1, message = "Birthdate is required"))] pub birthdate: Option, + pub domicile: Option, + #[validate(url(message = "Invalid identity document URL"))] + pub identity_document_url: Option, + #[validate(length(min = 50, message = "Bio must be at least 50 characters"))] + pub bio: Option, + pub last_education: Option, + #[validate(url(message = "Invalid LinkedIn URL"))] + pub linkedin_url: Option, + #[validate(url(message = "Invalid GitHub URL"))] + pub github_url: Option, + #[validate(url(message = "Invalid CV URL"))] + pub cv_url: Option, + #[validate(url(message = "Invalid portfolio URL"))] + pub portfolio_url: Option, #[validate(length(min = 1, message = "Avatar is required"))] pub avatar: Option, pub role_id: String, @@ -87,12 +104,22 @@ pub struct UsersDetailItemDto { pub id: String, pub role: RolesDetailItemDto, pub fullname: String, + pub legal_name: Option, pub email: String, pub avatar: Option, pub phone_number: String, + pub phone_for_verification: Option, pub is_active: bool, pub gender: Option, pub birthdate: Option, + pub domicile: Option, + pub identity_document_url: Option, + pub bio: Option, + pub last_education: Option, + pub linkedin_url: Option, + pub github_url: Option, + pub cv_url: Option, + pub portfolio_url: Option, pub created_at: String, pub updated_at: String, } @@ -103,12 +130,22 @@ impl UsersDetailItemDto { id: dto.id.id.to_raw().clone(), role: RolesDetailItemDto::from(&dto.role), fullname: dto.fullname.clone(), + legal_name: dto.legal_name.clone(), email: dto.email.clone(), avatar: dto.avatar.clone(), phone_number: dto.phone_number.clone(), // Corrected from dto.phone.clone() + phone_for_verification: dto.phone_for_verification.clone(), is_active: dto.is_active, gender: dto.gender.clone(), birthdate: dto.birthdate.clone(), + domicile: dto.domicile.clone(), + identity_document_url: dto.identity_document_url.clone(), + bio: dto.bio.clone(), + last_education: dto.last_education.clone(), + linkedin_url: dto.linkedin_url.clone(), + github_url: dto.github_url.clone(), + cv_url: dto.cv_url.clone(), + portfolio_url: dto.portfolio_url.clone(), created_at: dto.created_at.clone(), updated_at: dto.updated_at.clone(), } @@ -119,12 +156,22 @@ impl UsersDetailItemDto { id: schema.id.id.to_raw(), role: RolesDetailItemDto::default(), // Placeholder, role needs to be fetched fullname: schema.fullname.clone(), + legal_name: schema.legal_name.clone(), email: schema.email.clone(), avatar: schema.avatar.clone(), phone_number: schema.phone_number.clone(), + phone_for_verification: schema.phone_for_verification.clone(), is_active: schema.is_active, gender: schema.gender.clone(), birthdate: schema.birthdate.clone(), + domicile: schema.domicile.clone(), + identity_document_url: schema.identity_document_url.clone(), + bio: schema.bio.clone(), + last_education: schema.last_education.clone(), + linkedin_url: schema.linkedin_url.clone(), + github_url: schema.github_url.clone(), + cv_url: schema.cv_url.clone(), + portfolio_url: schema.portfolio_url.clone(), created_at: schema.created_at.clone(), updated_at: schema.updated_at.clone(), } @@ -177,13 +224,23 @@ impl UsersListQueryDto { pub struct UsersDetailQueryDto { pub id: Thing, pub fullname: String, + pub legal_name: Option, pub email: String, pub avatar: Option, pub phone_number: String, + pub phone_for_verification: Option, pub is_active: bool, pub is_deleted: bool, pub gender: Option, pub birthdate: Option, + pub domicile: Option, + pub identity_document_url: Option, + pub bio: Option, + pub last_education: Option, + pub linkedin_url: Option, + pub github_url: Option, + pub cv_url: Option, + pub portfolio_url: Option, pub password: String, pub role: RolesDetailQueryDto, pub created_at: String, @@ -197,12 +254,22 @@ impl UsersDetailQueryDto { id: self.id.clone(), role: self.role.clone(), fullname: self.fullname.clone(), + legal_name: self.legal_name.clone(), email: self.email.clone(), avatar: self.avatar.clone(), phone_number: self.phone_number.clone(), + phone_for_verification: self.phone_for_verification.clone(), is_active: self.is_active, mentor_id: self.mentor_id.clone(), gender: self.gender.clone(), + domicile: self.domicile.clone(), + identity_document_url: self.identity_document_url.clone(), + bio: self.bio.clone(), + last_education: self.last_education.clone(), + linkedin_url: self.linkedin_url.clone(), + github_url: self.github_url.clone(), + cv_url: self.cv_url.clone(), + portfolio_url: self.portfolio_url.clone(), is_deleted: self.is_deleted, password: self.password.clone(), birthdate: self.birthdate.clone(), @@ -217,13 +284,23 @@ impl From<&UsersDetailItemDto> for UsersDetailQueryDto { Self { id: crate::make_thing(&imphnen_libs::ResourceEnum::Users.to_string(), &dto.id), fullname: dto.fullname.clone(), + legal_name: dto.legal_name.clone(), email: dto.email.clone(), avatar: dto.avatar.clone(), phone_number: dto.phone_number.clone(), + phone_for_verification: dto.phone_for_verification.clone(), is_active: dto.is_active, is_deleted: false, gender: dto.gender.clone(), birthdate: dto.birthdate.clone(), + domicile: dto.domicile.clone(), + identity_document_url: dto.identity_document_url.clone(), + bio: dto.bio.clone(), + last_education: dto.last_education.clone(), + linkedin_url: dto.linkedin_url.clone(), + github_url: dto.github_url.clone(), + cv_url: dto.cv_url.clone(), + portfolio_url: dto.portfolio_url.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 e42bd91..27fa146 100644 --- a/imphnen-iam/src/v1/users/users_schema.rs +++ b/imphnen-iam/src/v1/users/users_schema.rs @@ -9,11 +9,15 @@ use surrealdb::{Uuid, sql::Thing}; pub struct UsersSchema { pub id: Thing, pub fullname: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub legal_name: Option, pub email: String, pub password: String, #[serde(skip_serializing_if = "Option::is_none")] pub avatar: Option, pub phone_number: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub phone_for_verification: Option, pub is_active: bool, pub is_deleted: bool, #[serde(skip_serializing_if = "Option::is_none")] @@ -22,6 +26,22 @@ pub struct UsersSchema { pub gender: Option, #[serde(skip_serializing_if = "Option::is_none")] pub birthdate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub domicile: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub identity_document_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bio: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_education: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub linkedin_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub github_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cv_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub portfolio_url: Option, pub role: Thing, pub created_at: String, pub updated_at: String, @@ -35,10 +55,12 @@ impl Default for UsersSchema { &Uuid::new_v4().to_string(), ), fullname: String::new(), + legal_name: None, email: String::new(), password: hash_password("").unwrap(), avatar: None, phone_number: String::new(), + phone_for_verification: None, is_active: false, is_deleted: false, mentor_id: Some(make_thing( @@ -47,6 +69,14 @@ impl Default for UsersSchema { )), gender: None, birthdate: None, + domicile: None, + identity_document_url: None, + bio: None, + last_education: None, + linkedin_url: None, + github_url: None, + cv_url: None, + portfolio_url: None, role: make_thing( &ResourceEnum::Roles.to_string(), "5713cb37-dc02-4e87-8048-d7a41d352059", @@ -62,9 +92,11 @@ impl UsersSchema { Self { id: dto.id, fullname: dto.fullname, + legal_name: dto.legal_name, email: dto.email, avatar: dto.avatar, phone_number: dto.phone_number, + phone_for_verification: dto.phone_for_verification, is_active: dto.is_active, is_deleted: dto.is_deleted, mentor_id: Some(dto.mentor_id.unwrap_or_else(|| { @@ -75,6 +107,14 @@ impl UsersSchema { })), gender: dto.gender, birthdate: dto.birthdate, + domicile: dto.domicile, + identity_document_url: dto.identity_document_url, + bio: dto.bio, + last_education: dto.last_education, + linkedin_url: dto.linkedin_url, + github_url: dto.github_url, + cv_url: dto.cv_url, + portfolio_url: dto.portfolio_url, password: dto.password, created_at: dto.created_at, updated_at: dto.updated_at, @@ -86,11 +126,21 @@ impl UsersSchema { 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, + identity_document_url: user.identity_document_url, + 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), @@ -107,9 +157,11 @@ impl UsersSchema { &Uuid::new_v4().to_string(), ), fullname: user.fullname, + legal_name: None, email: user.email, password, phone_number: user.phone_number, + phone_for_verification: None, is_active: false, mentor_id: Some(make_thing( &ResourceEnum::Users.to_string(), @@ -117,6 +169,14 @@ impl UsersSchema { )), gender: None, birthdate: None, + domicile: None, + identity_document_url: None, + bio: None, + last_education: None, + linkedin_url: None, + github_url: None, + cv_url: None, + portfolio_url: 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 d3bc6a0..d2be0de 100644 --- a/imphnen-iam/src/v1/users/users_service.rs +++ b/imphnen-iam/src/v1/users/users_service.rs @@ -16,6 +16,7 @@ use imphnen_utils::make_thing; use uuid::Uuid; use anyhow::Result; use async_trait::async_trait; +use tracing::info; use crate::v1::users::users_dto::{UsersDetailItemDto as UserDto, UsersCreateRequestDto as CreateUserDto}; #[async_trait] @@ -33,6 +34,7 @@ pub trait UsersServiceTrait: Send + Sync + 'static { async fn get_user_by_email(&self, email: &str) -> Result>; async fn create_user_by_dto(&self, new_user: CreateUserDto) -> Result; + async fn update_user_avatar(&self, email: &str, avatar_url: Option) -> Result<()>; } #[derive(Clone)] @@ -301,4 +303,34 @@ impl UsersServiceTrait for UsersService { Err(e) => Err(anyhow::anyhow!(e.to_string())), } } + + async fn update_user_avatar(&self, email: &str, avatar_url: Option) -> Result<()> { + let surrealdb_ws = surrealdb_init_ws().await + .map_err(|e| anyhow::anyhow!("Failed to initialize websocket database: {}", e))?; + let surrealdb_mem = surrealdb_init_mem().await + .map_err(|e| anyhow::anyhow!("Failed to initialize memory database: {}", e))?; + + let state = AppState { + surrealdb_ws, + surrealdb_mem, + }; + let repo = UsersRepository::new(&state); + + // Get the existing user + let mut user = repo.query_user_by_email(email.to_string()).await + .map_err(|e| anyhow::anyhow!("Failed to get user: {}", e))?; + + // Update the avatar + user.avatar = avatar_url; + + // Convert to schema and update + let user_schema = UsersSchema::from(user); + match repo.query_update_user(user_schema).await { + Ok(_) => { + info!("Successfully updated avatar for user: {}", email); + Ok(()) + }, + Err(e) => Err(anyhow::anyhow!("Failed to update user avatar: {}", e)), + } + } } diff --git a/imphnen-utils/src/csrf_token.rs b/imphnen-utils/src/csrf_token.rs index 4a0741d..1a868ad 100644 --- a/imphnen-utils/src/csrf_token.rs +++ b/imphnen-utils/src/csrf_token.rs @@ -191,8 +191,14 @@ mod tests { let secret = "test_secret"; let token = generate_csrf_token(secret).unwrap(); - // Should fail with 0 max age - assert!(validate_csrf_token(&token, secret, 0).is_err()); + // Add a 2 second delay to ensure the token expires when max_age is 1 second + std::thread::sleep(std::time::Duration::from_secs(2)); + + // Should fail with 1 second max age (token is now 2 seconds old) + assert!(validate_csrf_token(&token, secret, 1).is_err()); + + // Should still work with a large max age + assert!(validate_csrf_token(&token, secret, 300).is_ok()); } #[test] diff --git a/tests/src/iam/auth/auth_login_tests.rs b/tests/src/iam/auth/auth_login_tests.rs index 7151763..49b3a09 100644 --- a/tests/src/iam/auth/auth_login_tests.rs +++ b/tests/src/iam/auth/auth_login_tests.rs @@ -47,13 +47,23 @@ mod auth_login_tests { id: crate::make_thing("app_users", &uuid::Uuid::new_v4().to_string()), email: email.to_string(), fullname: "Test User".to_string(), + legal_name: None, password: hash_password(password).unwrap(), is_deleted: false, avatar: None, phone_number: "081234567890".to_string(), + phone_for_verification: None, is_active, gender: None, birthdate: None, + domicile: None, + identity_document_url: None, + bio: None, + last_education: None, + linkedin_url: None, + github_url: None, + cv_url: None, + portfolio_url: None, role: crate::make_thing("app_roles", &role.id), mentor_id: None, created_at: imphnen_utils::get_iso_date(), diff --git a/tests/src/iam/auth/auth_repository_test.rs b/tests/src/iam/auth/auth_repository_test.rs index f2cccbe..06d2ed3 100644 --- a/tests/src/iam/auth/auth_repository_test.rs +++ b/tests/src/iam/auth/auth_repository_test.rs @@ -21,13 +21,23 @@ mod auth_repository_test { id: make_thing("app_users", &Uuid::new_v4().to_string()), email: email.to_string(), fullname: "Test User".to_string(), + legal_name: None, password: "password".to_string(), is_deleted: false, avatar: None, phone_number: "081234567890".to_string(), + phone_for_verification: None, is_active: true, gender: None, birthdate: None, + domicile: None, + identity_document_url: None, + bio: None, + last_education: None, + linkedin_url: None, + github_url: None, + cv_url: None, + portfolio_url: None, role: make_thing("app_roles", &get_role_id(state).await), mentor_id: None, created_at: get_iso_date(), @@ -71,12 +81,22 @@ mod auth_repository_test { let mock_user = UsersDetailQueryDto { id: make_thing(&ResourceEnum::UsersCache.to_string(), &email), fullname: "Test User".into(), + legal_name: None, email: email.clone(), avatar: None, phone_number: "08123456789".into(), + phone_for_verification: None, is_active: true, gender: None, birthdate: None, + domicile: None, + identity_document_url: None, + bio: None, + last_education: None, + linkedin_url: None, + github_url: None, + cv_url: None, + portfolio_url: None, role: RolesDetailQueryDto { id: make_thing("app_roles", &Uuid::new_v4().to_string()), name: "Dummy Role".into(), diff --git a/tests/src/lib.rs b/tests/src/lib.rs index ce7a91c..9501351 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -21,13 +21,23 @@ pub fn create_test_user( id: make_thing("app_users", &Uuid::new_v4().to_string()), email: email.to_string(), fullname: format!("Randomize {} {}", fullname, rand::random::()), + legal_name: None, password: hash_password("secret").unwrap(), is_deleted: false, avatar: None, phone_number: "081234567890".to_string(), + phone_for_verification: None, is_active, gender: None, birthdate: None, + domicile: None, + identity_document_url: None, + bio: None, + last_education: None, + linkedin_url: None, + github_url: None, + cv_url: None, + portfolio_url: 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 6ade061..a30680d 100644 --- a/tests/src/mock_test.rs +++ b/tests/src/mock_test.rs @@ -149,15 +149,25 @@ pub async fn seed_users_for_test( let user = UsersSchema { id: Thing::from(("app_users", id)), fullname: fullname.into(), + legal_name: None, email: email.into(), password: hash_password("password").unwrap(), avatar: None, phone_number: "081234567890".into(), + phone_for_verification: None, is_active: true, is_deleted: false, mentor_id: None, gender: None, birthdate: None, + domicile: None, + identity_document_url: None, + bio: None, + last_education: None, + linkedin_url: None, + github_url: None, + cv_url: None, + portfolio_url: None, role: Thing::from(("app_roles", role_id)), created_at: get_iso_date(), updated_at: get_iso_date(),