diff --git a/imphnen-gateway/src/docs/openapi.rs b/imphnen-gateway/src/docs/openapi.rs index e6193d0..f7f0167 100644 --- a/imphnen-gateway/src/docs/openapi.rs +++ b/imphnen-gateway/src/docs/openapi.rs @@ -202,6 +202,7 @@ use utoipa::OpenApi; imphnen_iam::users::infrastructure::http::dto::HackathonProfileDto, imphnen_iam::users::infrastructure::http::dto::QrProfileDto, imphnen_iam::users::infrastructure::http::dto::MentorProfileDto, + imphnen_iam::users::infrastructure::http::dto::SessionProfileDto, GachaClaimDetailDto, GachaClaimCreateRequestDto, GachaCreditDto, GachaCreditAddRequestDto, GachaItemDto, GachaItemCreateRequestDto, diff --git a/imphnen-iam/src/users/infrastructure/http/dto.rs b/imphnen-iam/src/users/infrastructure/http/dto.rs index 2718aeb..4df023a 100644 --- a/imphnen-iam/src/users/infrastructure/http/dto.rs +++ b/imphnen-iam/src/users/infrastructure/http/dto.rs @@ -141,6 +141,21 @@ pub struct UsersMeResponseDto { pub qr: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mentor: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sessions: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct SessionProfileDto { + pub id: String, + pub topic: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub scheduled_at: String, + pub duration_minutes: i32, + pub session_type: String, + pub status: String, + pub role: String, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] 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 dc5ba49..9bdb635 100644 --- a/imphnen-iam/src/users/infrastructure/http/handlers/get_handlers.rs +++ b/imphnen-iam/src/users/infrastructure/http/handlers/get_handlers.rs @@ -1,10 +1,15 @@ use super::super::dto::{ - HackathonProfileDto, MentorProfileDto, QrProfileDto, UsersDetailItemDto, - UsersListItemDto, UsersMeResponseDto, + HackathonProfileDto, MentorProfileDto, QrProfileDto, SessionProfileDto, + UsersDetailItemDto, UsersListItemDto, UsersMeResponseDto, }; use crate::require_permissions; use crate::users::domain::UserService; -use axum::{Extension, extract::Path, http::HeaderMap, response::IntoResponse}; +use axum::{ + Extension, + extract::{Path, Query}, + http::HeaderMap, + response::IntoResponse, +}; use imphnen_entities::{ PermissionsEnum, ResponseListSuccessDto, ResponseSuccessDto, }; @@ -13,9 +18,17 @@ use imphnen_utils::{ApiPaginated, ApiSuccess, AppError}; use paginator_axum::PaginationQuery; use paginator_utils::PaginatorResponse; use sea_orm::DatabaseConnection; +use serde::Deserialize; +use std::collections::HashSet; use std::sync::Arc; use uuid::Uuid; +#[derive(Debug, Deserialize)] +pub struct MeQueryParams { + /// Comma-separated list of modules to include: hackathon,qr,mentor,sessions + pub include: Option, +} + #[utoipa::path( get, path = "/v1/iam/users", @@ -86,6 +99,9 @@ pub async fn get_user_by_id( get, path = "/v1/iam/users/me", security(("Bearer" = [])), + params( + ("include" = Option, Query, description = "Comma-separated modules to include: hackathon,qr,mentor,sessions. Omit to include all."), + ), responses( (status = 200, description = "[USER] Get current user profile (unified across all modules)", body = ResponseSuccessDto) ), @@ -95,6 +111,7 @@ pub async fn get_user_me( headers: HeaderMap, Extension(state): Extension, Extension(service): Extension>, + Query(params): Query, ) -> Result { let (claims, _) = crate::permissions_guard( headers, @@ -111,15 +128,43 @@ pub async fn get_user_me( 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; + let includes: Option> = params.include.map(|s| { + s.split(',').map(|v| v.trim().to_lowercase()).collect() + }); + let should_include = |module: &str| -> bool { + match &includes { + None => true, + Some(set) => set.contains(module), + } + }; + + let hackathon = if should_include("hackathon") { + fetch_hackathon_profile(db, user_uuid).await + } else { + None + }; + let qr = if should_include("qr") { + fetch_qr_profile(db, user_uuid).await + } else { + None + }; + let mentor = if should_include("mentor") { + fetch_mentor_profile(db, user_uuid).await + } else { + None + }; + let sessions = if should_include("sessions") { + fetch_sessions(db, user_uuid).await + } else { + None + }; Ok(ApiSuccess(UsersMeResponseDto { user: user_dto, hackathon, qr, mentor, + sessions, })) } @@ -184,6 +229,56 @@ async fn fetch_mentor_profile( }) } +async fn fetch_sessions( + db: &DatabaseConnection, + user_id: Option, +) -> Option> { + let uid = user_id?; + let pool = db.get_postgres_connection_pool(); + let rows = sqlx::query_as::<_, SessionRow>( + r#"SELECT id, topic, description, scheduled_at, duration_minutes, + session_type, status, + CASE WHEN mentor_id = $1 THEN 'mentor' ELSE 'mentee' END as role + FROM sessions + WHERE mentor_id = $1 OR mentee_id = $1 + ORDER BY scheduled_at DESC + LIMIT 20"#, + ) + .bind(uid) + .fetch_all(pool) + .await + .ok()?; + if rows.is_empty() { + return None; + } + Some( + rows.into_iter() + .map(|s| SessionProfileDto { + id: s.id.to_string(), + topic: s.topic, + description: s.description, + scheduled_at: s.scheduled_at.to_rfc3339(), + duration_minutes: s.duration_minutes, + session_type: s.session_type, + status: s.status, + role: s.role, + }) + .collect(), + ) +} + +#[derive(sqlx::FromRow)] +struct SessionRow { + id: Uuid, + topic: String, + description: Option, + scheduled_at: chrono::DateTime, + duration_minutes: i32, + session_type: String, + status: String, + role: String, +} + #[derive(sqlx::FromRow)] struct HackathonRow { is_admin: bool,