diff --git a/Cargo.lock b/Cargo.lock index b18be08..ecbe1df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1970,6 +1970,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", + "sqlx", "strum 0.27.2", "strum_macros", "tokio", diff --git a/imphnen-gateway/src/docs/openapi.rs b/imphnen-gateway/src/docs/openapi.rs index fa65a49..e6193d0 100644 --- a/imphnen-gateway/src/docs/openapi.rs +++ b/imphnen-gateway/src/docs/openapi.rs @@ -198,6 +198,10 @@ use utoipa::OpenApi; RolesListItemDto, RolesDetailItemDto, RolesCreateRequestDto, RolesUpdateRequestDto, PermissionsCreateRequestDto, PermissionsItemDto, UsersDetailItemDto, UsersListItemDto, UsersUpdateRequestDto, UsersCreateRequestDto, FileUploadSchema, + imphnen_iam::users::infrastructure::http::dto::UsersMeResponseDto, + imphnen_iam::users::infrastructure::http::dto::HackathonProfileDto, + imphnen_iam::users::infrastructure::http::dto::QrProfileDto, + imphnen_iam::users::infrastructure::http::dto::MentorProfileDto, GachaClaimDetailDto, GachaClaimCreateRequestDto, GachaCreditDto, GachaCreditAddRequestDto, GachaItemDto, GachaItemCreateRequestDto, diff --git a/imphnen-iam/Cargo.toml b/imphnen-iam/Cargo.toml index 93d1bcb..076bcb7 100644 --- a/imphnen-iam/Cargo.toml +++ b/imphnen-iam/Cargo.toml @@ -23,6 +23,7 @@ zod-rs.workspace = true zod-rs-util.workspace = true axum-test.workspace = true sea-orm.workspace = true +sqlx.workspace = true rand.workspace = true tokio.workspace = true chrono.workspace = true diff --git a/imphnen-iam/src/users/infrastructure/http/dto.rs b/imphnen-iam/src/users/infrastructure/http/dto.rs index 077ff63..2718aeb 100644 --- a/imphnen-iam/src/users/infrastructure/http/dto.rs +++ b/imphnen-iam/src/users/infrastructure/http/dto.rs @@ -131,6 +131,50 @@ impl From<&UsersDetailQueryDto> for UsersDetailItemDto { } } +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct UsersMeResponseDto { + #[serde(flatten)] + pub user: UsersDetailItemDto, + #[serde(skip_serializing_if = "Option::is_none")] + pub hackathon: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub qr: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mentor: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct HackathonProfileDto { + pub is_admin: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub phone_number: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bio: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub skills: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct QrProfileDto { + pub role: String, + pub provider: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct MentorProfileDto { + pub mentor_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub current_company: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub current_role: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub years_of_experience: Option, +} + impl From for UsersListItemDto { fn from(item: UserListItem) -> Self { Self { diff --git a/imphnen-iam/src/users/infrastructure/http/handlers/get_handlers.rs b/imphnen-iam/src/users/infrastructure/http/handlers/get_handlers.rs index 06e7692..dc5ba49 100644 --- a/imphnen-iam/src/users/infrastructure/http/handlers/get_handlers.rs +++ b/imphnen-iam/src/users/infrastructure/http/handlers/get_handlers.rs @@ -1,4 +1,7 @@ -use super::super::dto::{UsersDetailItemDto, UsersListItemDto}; +use super::super::dto::{ + HackathonProfileDto, MentorProfileDto, QrProfileDto, UsersDetailItemDto, + UsersListItemDto, UsersMeResponseDto, +}; use crate::require_permissions; use crate::users::domain::UserService; use axum::{Extension, extract::Path, http::HeaderMap, response::IntoResponse}; @@ -9,6 +12,7 @@ use imphnen_libs::AppState; use imphnen_utils::{ApiPaginated, ApiSuccess, AppError}; use paginator_axum::PaginationQuery; use paginator_utils::PaginatorResponse; +use sea_orm::DatabaseConnection; use std::sync::Arc; use uuid::Uuid; @@ -83,7 +87,7 @@ pub async fn get_user_by_id( path = "/v1/iam/users/me", security(("Bearer" = [])), responses( - (status = 200, description = "[USER] Get current user", body = ResponseSuccessDto) + (status = 200, description = "[USER] Get current user profile (unified across all modules)", body = ResponseSuccessDto) ), tag = "Users" )] @@ -98,9 +102,108 @@ pub async fn get_user_me( vec![], ) .await?; - let user = service.get_me(claims.user_id).await?; + let user = service.get_me(claims.user_id.clone()).await?; if user.is_deleted { return Err(AppError::NotFoundError("User not found".to_string())); } - Ok(ApiSuccess(UsersDetailItemDto::from(user))) + + let user_dto = UsersDetailItemDto::from(user); + let user_uuid = Uuid::parse_str(&claims.user_id).ok(); + let db = &state.postgres_connection.conn; + + let hackathon = fetch_hackathon_profile(db, user_uuid).await; + let qr = fetch_qr_profile(db, user_uuid).await; + let mentor = fetch_mentor_profile(db, user_uuid).await; + + Ok(ApiSuccess(UsersMeResponseDto { + user: user_dto, + hackathon, + qr, + mentor, + })) +} + +async fn fetch_hackathon_profile( + db: &DatabaseConnection, + user_id: Option, +) -> Option { + let uid = user_id?; + let pool = db.get_postgres_connection_pool(); + sqlx::query_as::<_, HackathonRow>( + "SELECT COALESCE(is_admin, false) as is_admin, phone_number, location, bio, skills FROM hackathon_users WHERE id = $1", + ) + .bind(uid) + .fetch_optional(pool) + .await + .ok()? + .map(|h| HackathonProfileDto { + is_admin: h.is_admin, + phone_number: h.phone_number, + location: h.location, + bio: h.bio, + skills: h.skills, + }) +} + +async fn fetch_qr_profile( + db: &DatabaseConnection, + user_id: Option, +) -> Option { + let uid = user_id?; + let pool = db.get_postgres_connection_pool(); + sqlx::query_as::<_, QrRow>("SELECT role, provider FROM qr_users WHERE id = $1") + .bind(uid) + .fetch_optional(pool) + .await + .ok()? + .map(|q| QrProfileDto { + role: q.role, + provider: q.provider, + }) +} + +async fn fetch_mentor_profile( + db: &DatabaseConnection, + user_id: Option, +) -> Option { + let uid = user_id?; + let pool = db.get_postgres_connection_pool(); + sqlx::query_as::<_, MentorRow>( + r#"SELECT id, status, current_company, "current_role", years_of_experience FROM app_mentors WHERE user_id = $1 AND is_deleted = false"#, + ) + .bind(uid) + .fetch_optional(pool) + .await + .ok()? + .map(|m| MentorProfileDto { + mentor_id: m.id.to_string(), + status: m.status, + current_company: m.current_company, + current_role: m.current_role, + years_of_experience: m.years_of_experience, + }) +} + +#[derive(sqlx::FromRow)] +struct HackathonRow { + is_admin: bool, + phone_number: Option, + location: Option, + bio: Option, + skills: Option, +} + +#[derive(sqlx::FromRow)] +struct QrRow { + role: String, + provider: String, +} + +#[derive(sqlx::FromRow)] +struct MentorRow { + id: Uuid, + status: Option, + current_company: Option, + current_role: Option, + years_of_experience: Option, }