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",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sqlx",
|
||||||
"strum 0.27.2",
|
"strum 0.27.2",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
@@ -198,6 +198,10 @@ use utoipa::OpenApi;
|
|||||||
RolesListItemDto, RolesDetailItemDto, RolesCreateRequestDto, RolesUpdateRequestDto,
|
RolesListItemDto, RolesDetailItemDto, RolesCreateRequestDto, RolesUpdateRequestDto,
|
||||||
PermissionsCreateRequestDto, PermissionsItemDto,
|
PermissionsCreateRequestDto, PermissionsItemDto,
|
||||||
UsersDetailItemDto, UsersListItemDto, UsersUpdateRequestDto, UsersCreateRequestDto, FileUploadSchema,
|
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,
|
GachaClaimDetailDto, GachaClaimCreateRequestDto,
|
||||||
GachaCreditDto, GachaCreditAddRequestDto,
|
GachaCreditDto, GachaCreditAddRequestDto,
|
||||||
GachaItemDto, GachaItemCreateRequestDto,
|
GachaItemDto, GachaItemCreateRequestDto,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ zod-rs.workspace = true
|
|||||||
zod-rs-util.workspace = true
|
zod-rs-util.workspace = true
|
||||||
axum-test.workspace = true
|
axum-test.workspace = true
|
||||||
sea-orm.workspace = true
|
sea-orm.workspace = true
|
||||||
|
sqlx.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
chrono.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 {
|
impl From<UserListItem> for UsersListItemDto {
|
||||||
fn from(item: UserListItem) -> Self {
|
fn from(item: UserListItem) -> Self {
|
||||||
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::require_permissions;
|
||||||
use crate::users::domain::UserService;
|
use crate::users::domain::UserService;
|
||||||
use axum::{Extension, extract::Path, http::HeaderMap, response::IntoResponse};
|
use axum::{Extension, extract::Path, http::HeaderMap, response::IntoResponse};
|
||||||
@@ -9,6 +12,7 @@ use imphnen_libs::AppState;
|
|||||||
use imphnen_utils::{ApiPaginated, ApiSuccess, AppError};
|
use imphnen_utils::{ApiPaginated, ApiSuccess, AppError};
|
||||||
use paginator_axum::PaginationQuery;
|
use paginator_axum::PaginationQuery;
|
||||||
use paginator_utils::PaginatorResponse;
|
use paginator_utils::PaginatorResponse;
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -83,7 +87,7 @@ pub async fn get_user_by_id(
|
|||||||
path = "/v1/iam/users/me",
|
path = "/v1/iam/users/me",
|
||||||
security(("Bearer" = [])),
|
security(("Bearer" = [])),
|
||||||
responses(
|
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"
|
tag = "Users"
|
||||||
)]
|
)]
|
||||||
@@ -98,9 +102,108 @@ pub async fn get_user_me(
|
|||||||
vec![],
|
vec![],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let user = service.get_me(claims.user_id).await?;
|
let user = service.get_me(claims.user_id.clone()).await?;
|
||||||
if user.is_deleted {
|
if user.is_deleted {
|
||||||
return Err(AppError::NotFoundError("User not found".to_string()));
|
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