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:
co-authored by
Claude Opus 4.6
parent
729335014f
commit
a4bbc73c7e
Generated
+1
@@ -1970,6 +1970,7 @@ dependencies = [
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"strum 0.27.2",
|
||||
"strum_macros",
|
||||
"tokio",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user