feat: unify /me endpoint to aggregate all module profiles

GET /v1/iam/users/me now returns hackathon, QR, and mentor profiles
alongside the core IAM user data. Module-specific profiles are
included as optional fields when the user exists in those modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-04-10 09:47:27 +07:00
co-authored by Claude Opus 4.6
parent 729335014f
commit a4bbc73c7e
5 changed files with 157 additions and 4 deletions
Generated
+1
View File
@@ -1970,6 +1970,7 @@ dependencies = [
"sea-orm",
"serde",
"serde_json",
"sqlx",
"strum 0.27.2",
"strum_macros",
"tokio",
+4
View File
@@ -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,
+1
View File
@@ -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
@@ -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<HackathonProfileDto>,
#[serde(skip_serializing_if = "Option::is_none")]
pub qr: Option<QrProfileDto>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mentor: Option<MentorProfileDto>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct HackathonProfileDto {
pub is_admin: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub phone_number: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skills: Option<serde_json::Value>,
}
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_company: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub years_of_experience: Option<i32>,
}
impl From<UserListItem> for UsersListItemDto {
fn from(item: UserListItem) -> Self {
Self {
@@ -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<UsersDetailItemDto>)
(status = 200, description = "[USER] Get current user profile (unified across all modules)", body = ResponseSuccessDto<UsersMeResponseDto>)
),
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<Uuid>,
) -> Option<HackathonProfileDto> {
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<Uuid>,
) -> Option<QrProfileDto> {
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<Uuid>,
) -> Option<MentorProfileDto> {
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<String>,
location: Option<String>,
bio: Option<String>,
skills: Option<serde_json::Value>,
}
#[derive(sqlx::FromRow)]
struct QrRow {
role: String,
provider: String,
}
#[derive(sqlx::FromRow)]
struct MentorRow {
id: Uuid,
status: Option<String>,
current_company: Option<String>,
current_role: Option<String>,
years_of_experience: Option<i32>,
}