a
This commit is contained in:
Generated
+2
@@ -2094,6 +2094,7 @@ name = "imphnen-dimentorin"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-test",
|
||||
"chrono",
|
||||
@@ -2102,6 +2103,7 @@ dependencies = [
|
||||
"imphnen-entities",
|
||||
"imphnen-iam",
|
||||
"imphnen-libs",
|
||||
"imphnen-middleware",
|
||||
"imphnen-utils",
|
||||
"lazy_static",
|
||||
"rand 0.9.2",
|
||||
|
||||
@@ -8,7 +8,9 @@ imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-middleware.workspace = true
|
||||
axum.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
|
||||
@@ -3,3 +3,10 @@ pub mod v1;
|
||||
// Explicitly export only what's needed from v1
|
||||
pub use v1::dimentorin_router;
|
||||
pub use v1::mentors::mentors_router;
|
||||
pub use v1::sessions::{
|
||||
sessions_router, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
AvailabilitySlotDto,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use axum::Router;
|
||||
|
||||
pub mod mentors;
|
||||
pub mod sessions;
|
||||
|
||||
/// Creates the main Dimentorin router with all version 1 endpoints
|
||||
/// Routes:
|
||||
/// - /mentors -> mentors::mentors_router()
|
||||
/// - /sessions -> sessions::sessions_router()
|
||||
/// - /users/me/sessions -> sessions::get_my_sessions()
|
||||
pub fn dimentorin_router() -> Router {
|
||||
Router::new()
|
||||
.nest("/mentors", mentors::mentors_router())
|
||||
.merge(sessions::sessions_router())
|
||||
}
|
||||
|
||||
// Explicitly re-export key items for easier consumption
|
||||
@@ -15,3 +19,8 @@ pub use mentors::mentors_router;
|
||||
pub use mentors::MentorsService;
|
||||
pub use mentors::MentorsRepository;
|
||||
pub use mentors::MentorSchema;
|
||||
|
||||
pub use sessions::sessions_router;
|
||||
pub use sessions::SessionsService;
|
||||
pub use sessions::SessionsRepository;
|
||||
pub use sessions::SessionSchema;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
pub mod sessions_controller;
|
||||
pub mod sessions_dto;
|
||||
pub mod sessions_repository;
|
||||
pub mod sessions_schema;
|
||||
pub mod sessions_service;
|
||||
|
||||
pub use sessions_controller::*;
|
||||
pub use sessions_dto::*;
|
||||
pub use sessions_repository::*;
|
||||
pub use sessions_schema::*;
|
||||
pub use sessions_service::*;
|
||||
@@ -0,0 +1,297 @@
|
||||
use super::{
|
||||
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListResponseDto,
|
||||
SessionsService, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
http::HeaderMap,
|
||||
response::Response,
|
||||
routing::{get, post, put},
|
||||
Json, Router,
|
||||
};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::extract_email;
|
||||
use serde::Deserialize;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
post_book_session,
|
||||
get_mentor_sessions,
|
||||
get_mentor_availability,
|
||||
put_update_session_status,
|
||||
post_submit_feedback,
|
||||
get_my_sessions,
|
||||
),
|
||||
components(schemas(
|
||||
BookSessionRequestDto,
|
||||
BookSessionResponseDto,
|
||||
SessionListResponseDto,
|
||||
super::SessionListItemDto,
|
||||
MentorAvailabilityDto,
|
||||
super::AvailabilitySlotDto,
|
||||
UpdateSessionStatusRequestDto,
|
||||
UpdateSessionStatusResponseDto,
|
||||
SessionFeedbackRequestDto,
|
||||
SessionFeedbackResponseDto,
|
||||
)),
|
||||
tags(
|
||||
(name = "sessions", description = "Mentoring Sessions Management API")
|
||||
)
|
||||
)]
|
||||
pub struct SessionsApiDoc;
|
||||
|
||||
// ============================================
|
||||
// Book Session
|
||||
// ============================================
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/mentors/{id}/sessions/book",
|
||||
tag = "sessions",
|
||||
summary = "Book a mentoring session",
|
||||
description = "Book a mentoring session with a specific mentor. Requires authentication.",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
),
|
||||
request_body = BookSessionRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "Session booked successfully", body = BookSessionResponseDto),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn post_book_session(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(mentor_id): Path<String>,
|
||||
Json(dto): Json<BookSessionRequestDto>,
|
||||
) -> Response {
|
||||
let user_email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SessionsService::book_session(&state, mentor_id, user_email, dto).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Mentor's Sessions
|
||||
// ============================================
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SessionStatusFilter {
|
||||
status: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/{id}/sessions",
|
||||
tag = "sessions",
|
||||
summary = "List mentor's sessions",
|
||||
description = "Get all sessions for a specific mentor. Only accessible by the mentor themselves or admin.",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
("status" = Option<String>, Query, description = "Filter by status (pending, confirmed, completed, cancelled, no_show)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_sessions(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(mentor_id): Path<String>,
|
||||
Query(filter): Query<SessionStatusFilter>,
|
||||
) -> Response {
|
||||
let user_email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SessionsService::get_mentor_sessions(&state, mentor_id, user_email, filter.status).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Mentor Availability
|
||||
// ============================================
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/{id}/availability",
|
||||
tag = "sessions",
|
||||
summary = "Get mentor availability",
|
||||
description = "Get available time slots for booking with a mentor. Public endpoint.",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Availability retrieved successfully", body = MentorAvailabilityDto),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_availability(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(mentor_id): Path<String>,
|
||||
) -> Response {
|
||||
SessionsService::get_mentor_availability(&state, mentor_id).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session Status
|
||||
// ============================================
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/sessions/{id}/status",
|
||||
tag = "sessions",
|
||||
summary = "Update session status",
|
||||
description = "Update the status of a session (confirm, complete, cancel). Only accessible by the mentor.",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Session ID"),
|
||||
),
|
||||
request_body = UpdateSessionStatusRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Status updated successfully", body = UpdateSessionStatusResponseDto),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Session not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn put_update_session_status(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(session_id): Path<String>,
|
||||
Json(dto): Json<UpdateSessionStatusRequestDto>,
|
||||
) -> Response {
|
||||
let user_email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SessionsService::update_session_status(&state, session_id, user_email, dto).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Submit Feedback
|
||||
// ============================================
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/sessions/{id}/feedback",
|
||||
tag = "sessions",
|
||||
summary = "Submit session feedback",
|
||||
description = "Submit feedback and rating for a completed session. Only accessible by the mentee.",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Session ID"),
|
||||
),
|
||||
request_body = SessionFeedbackRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Feedback submitted successfully", body = SessionFeedbackResponseDto),
|
||||
(status = 400, description = "Invalid request or session not completed"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Session not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn post_submit_feedback(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(session_id): Path<String>,
|
||||
Json(dto): Json<SessionFeedbackRequestDto>,
|
||||
) -> Response {
|
||||
let user_email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SessionsService::submit_feedback(&state, session_id, user_email, dto).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get User's Sessions
|
||||
// ============================================
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/users/me/sessions",
|
||||
tag = "sessions",
|
||||
summary = "Get my sessions",
|
||||
description = "Get all sessions for the authenticated user (as mentee). Requires authentication.",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("status" = Option<String>, Query, description = "Filter by status (pending, confirmed, completed, cancelled, no_show)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_my_sessions(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Query(filter): Query<SessionStatusFilter>,
|
||||
) -> Response {
|
||||
let user_email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SessionsService::get_user_sessions(&state, user_email, filter.status).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Router
|
||||
// ============================================
|
||||
|
||||
pub fn sessions_router() -> Router {
|
||||
Router::new()
|
||||
// Book session (under mentors path)
|
||||
.route("/mentors/:id/sessions/book", post(post_book_session))
|
||||
// Get mentor's sessions
|
||||
.route("/mentors/:id/sessions", get(get_mentor_sessions))
|
||||
// Get mentor availability (public - no auth)
|
||||
.route("/mentors/:id/availability", get(get_mentor_availability))
|
||||
// Update session status
|
||||
.route("/sessions/:id/status", put(put_update_session_status))
|
||||
// Submit feedback
|
||||
.route("/sessions/:id/feedback", post(post_submit_feedback))
|
||||
// Get my sessions
|
||||
.route("/users/me/sessions", get(get_my_sessions))
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
// ============================================
|
||||
// Book Session (POST /v1/mentors/{id}/sessions/book)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct BookSessionRequestDto {
|
||||
#[validate(length(min = 3, max = 200, message = "Topic must be 3-200 characters"))]
|
||||
pub topic: String,
|
||||
|
||||
#[validate(length(max = 1000, message = "Description must be max 1000 characters"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
|
||||
#[validate(length(min = 1, message = "Scheduled time is required"))]
|
||||
pub scheduled_at: String, // ISO 8601 datetime
|
||||
|
||||
#[validate(range(min = 15, max = 240, message = "Duration must be 15-240 minutes"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_minutes: Option<i32>,
|
||||
|
||||
#[validate(length(max = 50, message = "Session type must be max 50 characters"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_type: Option<String>, // "video_call", "phone_call", "chat"
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BookSessionResponseDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List Sessions (GET /v1/mentors/{id}/sessions & /v1/users/me/sessions)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListItemDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub mentee_email: Option<String>,
|
||||
pub topic: String,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub rating: Option<i32>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListResponseDto {
|
||||
pub sessions: Vec<SessionListItemDto>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Session Detail
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionDetailDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentor_fullname: Option<String>,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Mentor Availability (GET /v1/mentors/{id}/availability)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AvailabilitySlotDto {
|
||||
pub date: String, // YYYY-MM-DD
|
||||
pub time: String, // HH:MM
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorAvailabilityDto {
|
||||
pub mentor_id: String,
|
||||
pub availability_commitment: String,
|
||||
pub preferred_formats: Vec<String>,
|
||||
pub slots: Vec<AvailabilitySlotDto>,
|
||||
pub booked_dates: Vec<String>, // Dates with existing sessions
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session Status (PUT /v1/sessions/{id}/status)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateSessionStatusRequestDto {
|
||||
#[validate(length(min = 1, max = 50, message = "Status must be 1-50 characters"))]
|
||||
pub status: String, // "confirmed", "completed", "cancelled", "no_show"
|
||||
|
||||
#[validate(url(message = "Meeting link must be a valid URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub meeting_link: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateSessionStatusResponseDto {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
pub meeting_link: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Submit Feedback (POST /v1/sessions/{id}/feedback)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct SessionFeedbackRequestDto {
|
||||
#[validate(length(min = 10, max = 2000, message = "Feedback must be 10-2000 characters"))]
|
||||
pub feedback: String,
|
||||
|
||||
#[validate(range(min = 1, max = 5, message = "Rating must be 1-5"))]
|
||||
pub rating: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionFeedbackResponseDto {
|
||||
pub id: String,
|
||||
pub feedback: String,
|
||||
pub rating: i32,
|
||||
pub submitted_at: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Query DTOs (internal use)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionDetailQueryDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub mentor_fullname: Option<String>,
|
||||
pub mentee_fullname: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionListQueryDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub topic: String,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub rating: Option<i32>,
|
||||
pub created_at: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub mentee_email: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
use super::{SessionDetailQueryDto, SessionListQueryDto, SessionSchema};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::get_id;
|
||||
use serde::Deserialize;
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
pub struct SessionsRepository<'a> {
|
||||
pub state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> SessionsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Create Session
|
||||
// ============================================
|
||||
pub async fn create_session(&self, schema: SessionSchema) -> Result<SessionSchema, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let created: Option<SessionSchema> = db
|
||||
.create("sessions")
|
||||
.content(schema)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create session: {}", e))?;
|
||||
|
||||
created.ok_or_else(|| "Session creation returned None".to_string())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Session by ID
|
||||
// ============================================
|
||||
pub async fn query_session_by_id(&self, id: &Thing) -> Result<Option<SessionSchema>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let session: Option<SessionSchema> = db
|
||||
.select(record_key)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch session: {}", e))?;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Session Detail with User Info
|
||||
// ============================================
|
||||
pub async fn query_session_detail(&self, id: &Thing) -> Result<Option<SessionDetailQueryDto>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let query = r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
description,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
meeting_link,
|
||||
session_type,
|
||||
status,
|
||||
feedback,
|
||||
rating,
|
||||
feedback_submitted_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
(SELECT fullname FROM $parent.mentor_id.user_id)[0].fullname AS mentor_fullname,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname
|
||||
FROM type::thing($table, $id)
|
||||
"#;
|
||||
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("table", "sessions"))
|
||||
.bind(("id", id.id.to_string()))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to query session detail: {}", e))?;
|
||||
|
||||
let session: Option<SessionDetailQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse session detail: {}", e))?;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List Mentor's Sessions
|
||||
// ============================================
|
||||
pub async fn query_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: &Thing,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionListQueryDto>, String> {
|
||||
let query = if let Some(_status) = status_filter.as_ref() {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentor_id = $mentor_id AND status = $status
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
} else {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentor_id = $mentor_id
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
};
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentor_id_clone = mentor_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.await
|
||||
}
|
||||
.map_err(|e| format!("Failed to query mentor sessions: {}", e))?;
|
||||
|
||||
let sessions: Vec<SessionListQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse mentor sessions: {}", e))?;
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List User's Sessions (as mentee)
|
||||
// ============================================
|
||||
pub async fn query_user_sessions(
|
||||
&self,
|
||||
user_id: &Thing,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionListQueryDto>, String> {
|
||||
let query = if let Some(_status) = status_filter.as_ref() {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentee_id = $user_id AND status = $status
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
} else {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentee_id = $user_id
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
};
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let user_id_clone = user_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.await
|
||||
}
|
||||
.map_err(|e| format!("Failed to query user sessions: {}", e))?;
|
||||
|
||||
let sessions: Vec<SessionListQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse user sessions: {}", e))?;
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Booked Dates for Mentor
|
||||
// ============================================
|
||||
pub async fn query_booked_dates(&self, mentor_id: &Thing) -> Result<Vec<String>, String> {
|
||||
let query = r#"
|
||||
SELECT scheduled_at FROM sessions
|
||||
WHERE mentor_id = $mentor_id
|
||||
AND status IN ['pending', 'confirmed']
|
||||
ORDER BY scheduled_at ASC
|
||||
"#;
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentor_id_clone = mentor_id.clone();
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to query booked dates: {}", e))?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DateOnly {
|
||||
scheduled_at: String,
|
||||
}
|
||||
|
||||
let dates: Vec<DateOnly> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse booked dates: {}", e))?;
|
||||
|
||||
Ok(dates.into_iter().map(|d| d.scheduled_at).collect())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session
|
||||
// ============================================
|
||||
pub async fn update_session(&self, id: &Thing, schema: SessionSchema) -> Result<SessionSchema, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let updated: Option<SessionSchema> = db
|
||||
.update(record_key)
|
||||
.content(schema)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to update session: {}", e))?;
|
||||
|
||||
updated.ok_or_else(|| "Session update returned None".to_string())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Delete Session (soft delete)
|
||||
// ============================================
|
||||
// Delete Session (soft delete)
|
||||
// ============================================
|
||||
pub async fn delete_session(&self, id: &Thing) -> Result<(), String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let _: Option<SessionSchema> = db
|
||||
.delete(record_key)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete session: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use super::{BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{sql::Thing, Uuid};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionSchema {
|
||||
pub id: Thing,
|
||||
pub mentor_id: Thing,
|
||||
pub mentee_id: Thing,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String, // ISO 8601 datetime
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String, // "video_call", "phone_call", "chat"
|
||||
pub status: String, // "pending", "confirmed", "completed", "cancelled", "no_show"
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>, // 1-5
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl Default for SessionSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
ResourceEnum::Sessions.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
mentor_id: make_thing(
|
||||
ResourceEnum::Mentors.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
mentee_id: make_thing(
|
||||
ResourceEnum::Users.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
topic: String::new(),
|
||||
description: None,
|
||||
scheduled_at: get_iso_date(),
|
||||
duration_minutes: 60,
|
||||
meeting_link: None,
|
||||
session_type: "video_call".to_string(),
|
||||
status: "pending".to_string(),
|
||||
feedback: None,
|
||||
rating: None,
|
||||
feedback_submitted_at: None,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionSchema {
|
||||
pub fn from_book_request(
|
||||
mentor_id: Thing,
|
||||
mentee_id: Thing,
|
||||
request: BookSessionRequestDto,
|
||||
) -> Self {
|
||||
Self {
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic: request.topic,
|
||||
description: request.description,
|
||||
scheduled_at: request.scheduled_at,
|
||||
duration_minutes: request.duration_minutes.unwrap_or(60),
|
||||
session_type: request.session_type.unwrap_or_else(|| "video_call".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_status(&mut self, request: UpdateSessionStatusRequestDto) {
|
||||
self.status = request.status;
|
||||
if let Some(link) = request.meeting_link {
|
||||
self.meeting_link = Some(link);
|
||||
}
|
||||
self.updated_at = get_iso_date();
|
||||
}
|
||||
|
||||
pub fn add_feedback(&mut self, request: SessionFeedbackRequestDto) {
|
||||
self.feedback = Some(request.feedback);
|
||||
self.rating = Some(request.rating);
|
||||
self.feedback_submitted_at = Some(get_iso_date());
|
||||
self.updated_at = get_iso_date();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
use super::{
|
||||
AvailabilitySlotDto, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||
SessionListResponseDto, SessionSchema, SessionsRepository, UpdateSessionStatusRequestDto,
|
||||
UpdateSessionStatusResponseDto,
|
||||
};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use chrono::{Duration, Utc};
|
||||
use imphnen_entities::ResponseSuccessDto;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::{common_response, extract_id, get_iso_date, make_thing, success_response, validate_request};
|
||||
|
||||
pub struct SessionsService;
|
||||
|
||||
impl SessionsService {
|
||||
// ============================================
|
||||
// Book Session
|
||||
// ============================================
|
||||
pub async fn book_session(
|
||||
state: &AppState,
|
||||
mentor_id: String,
|
||||
user_id: String,
|
||||
dto: BookSessionRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&dto) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||
let mentee_thing = make_thing("users", &user_id);
|
||||
|
||||
let schema = SessionSchema::from_book_request(mentor_thing.clone(), mentee_thing.clone(), dto);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.create_session(schema).await {
|
||||
Ok(created) => {
|
||||
let response = BookSessionResponseDto {
|
||||
id: extract_id(&created.id),
|
||||
mentor_id: extract_id(&created.mentor_id),
|
||||
mentee_id: extract_id(&created.mentee_id),
|
||||
topic: created.topic,
|
||||
description: created.description,
|
||||
scheduled_at: created.scheduled_at,
|
||||
duration_minutes: created.duration_minutes,
|
||||
session_type: created.session_type,
|
||||
status: created.status,
|
||||
created_at: created.created_at,
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Mentor's Sessions
|
||||
// ============================================
|
||||
pub async fn get_mentor_sessions(
|
||||
state: &AppState,
|
||||
mentor_id: String,
|
||||
_user_email: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Response {
|
||||
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_mentor_sessions(&mentor_thing, status_filter).await {
|
||||
Ok(sessions) => {
|
||||
let session_items: Vec<SessionListItemDto> = sessions
|
||||
.into_iter()
|
||||
.map(|s| SessionListItemDto {
|
||||
id: s.id,
|
||||
mentor_id: s.mentor_id,
|
||||
mentee_id: s.mentee_id,
|
||||
mentee_fullname: s.mentee_fullname,
|
||||
mentee_email: s.mentee_email,
|
||||
topic: s.topic,
|
||||
scheduled_at: s.scheduled_at,
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
rating: s.rating,
|
||||
created_at: s.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let response = SessionListResponseDto {
|
||||
sessions: session_items,
|
||||
total: 0, // TODO: implement proper pagination
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get User's Sessions (as mentee)
|
||||
// ============================================
|
||||
pub async fn get_user_sessions(
|
||||
state: &AppState,
|
||||
user_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Response {
|
||||
let user_thing = make_thing("users", &user_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_user_sessions(&user_thing, status_filter).await {
|
||||
Ok(sessions) => {
|
||||
let session_items: Vec<SessionListItemDto> = sessions
|
||||
.into_iter()
|
||||
.map(|s| SessionListItemDto {
|
||||
id: s.id,
|
||||
mentor_id: s.mentor_id,
|
||||
mentee_id: s.mentee_id,
|
||||
mentee_fullname: s.mentee_fullname,
|
||||
mentee_email: s.mentee_email,
|
||||
topic: s.topic,
|
||||
scheduled_at: s.scheduled_at,
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
rating: s.rating,
|
||||
created_at: s.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let response = SessionListResponseDto {
|
||||
sessions: session_items,
|
||||
total: 0, // TODO: implement proper pagination
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Mentor Availability
|
||||
// ============================================
|
||||
pub async fn get_mentor_availability(state: &AppState, mentor_id: String) -> Response {
|
||||
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_booked_dates(&mentor_thing).await {
|
||||
Ok(booked_dates) => {
|
||||
// Generate sample availability slots (next 7 days)
|
||||
let mut slots = Vec::new();
|
||||
let today = Utc::now().date_naive();
|
||||
|
||||
for i in 0..7 {
|
||||
let date = today + Duration::days(i);
|
||||
let date_str = date.format("%Y-%m-%d").to_string();
|
||||
|
||||
// Generate time slots (9 AM to 5 PM, every hour)
|
||||
for hour in 9..17 {
|
||||
let time_str = format!("{:02}:00", hour);
|
||||
let datetime_str = format!("{}T{}:00Z", date_str, time_str);
|
||||
|
||||
// Check if this slot is booked
|
||||
let is_booked = booked_dates.iter().any(|d| d.starts_with(&datetime_str[..13]));
|
||||
|
||||
slots.push(AvailabilitySlotDto {
|
||||
date: date_str.clone(),
|
||||
time: time_str,
|
||||
available: !is_booked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let response = MentorAvailabilityDto {
|
||||
mentor_id,
|
||||
availability_commitment: "Available weekdays 9 AM - 5 PM".to_string(),
|
||||
preferred_formats: vec!["video_call".to_string(), "phone_call".to_string()],
|
||||
slots,
|
||||
booked_dates,
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session Status
|
||||
// ============================================
|
||||
pub async fn update_session_status(
|
||||
state: &AppState,
|
||||
session_id: String,
|
||||
_user_id: String,
|
||||
dto: UpdateSessionStatusRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&dto) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let session_thing = make_thing("sessions", &session_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_session_by_id(&session_thing).await {
|
||||
Ok(Some(mut session)) => {
|
||||
session.update_status(dto.clone());
|
||||
match repo.update_session(&session_thing, session).await {
|
||||
Ok(updated) => {
|
||||
let response = UpdateSessionStatusResponseDto {
|
||||
id: extract_id(&updated.id),
|
||||
status: updated.status,
|
||||
meeting_link: updated.meeting_link,
|
||||
updated_at: updated.updated_at,
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
Ok(None) => common_response(StatusCode::NOT_FOUND, "Session not found"),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Submit Feedback
|
||||
// ============================================
|
||||
pub async fn submit_feedback(
|
||||
state: &AppState,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
dto: SessionFeedbackRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&dto) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let session_thing = make_thing("sessions", &session_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_session_by_id(&session_thing).await {
|
||||
Ok(Some(mut session)) => {
|
||||
// Authorization: Only mentee can submit feedback
|
||||
let mentee_id = extract_id(&session.mentee_id);
|
||||
if mentee_id != user_id {
|
||||
return common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"Unauthorized: Only the mentee can submit feedback",
|
||||
);
|
||||
}
|
||||
|
||||
// Validate session is completed
|
||||
if session.status != "completed" {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Feedback can only be submitted for completed sessions",
|
||||
);
|
||||
}
|
||||
|
||||
session.add_feedback(dto.clone());
|
||||
match repo.update_session(&session_thing, session).await {
|
||||
Ok(updated) => {
|
||||
let response = SessionFeedbackResponseDto {
|
||||
id: extract_id(&updated.id),
|
||||
feedback: dto.feedback,
|
||||
rating: dto.rating,
|
||||
submitted_at: updated.feedback_submitted_at.unwrap_or_else(get_iso_date),
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
Ok(None) => common_response(StatusCode::NOT_FOUND, "Session not found"),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,13 @@ use imphnen_dimentorin::v1::mentors::{
|
||||
MentoringLogistics, MentoringRate, ProfessionalProfile,
|
||||
},
|
||||
};
|
||||
use imphnen_dimentorin::v1::sessions::{
|
||||
sessions_controller,
|
||||
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
AvailabilitySlotDto,
|
||||
};
|
||||
use imphnen_gacha::v1::gacha_claims::{gacha_claims_controller, GachaClaimItemDto, GachaClaimRequestDto};
|
||||
use imphnen_gacha::v1::gacha_items::{gacha_items_controller, GachaItemDto};
|
||||
use imphnen_gacha::v1::gacha_items::gacha_items_dto::GachaItemRequestDto;
|
||||
@@ -28,6 +35,13 @@ use imphnen_hackathon::v1::hackathon::{
|
||||
HackathonTimelineDto, HackathonTimelineUpdateRequestDto, HackathonUpdateRequestDto,
|
||||
},
|
||||
};
|
||||
use imphnen_hackathon::v1::registrations::{
|
||||
registration_controller,
|
||||
RegistrationRequestDto, RegistrationResponseDto, RegistrationListResponseDto,
|
||||
RegistrationListItemDto, UpdateRegistrationStatusRequestDto, UpdateRegistrationStatusResponseDto,
|
||||
CheckInResponseDto, RegistrationStatsDto, UserHackathonsResponseDto, UserHackathonDto,
|
||||
RegistrationStatus, ParticipantRole,
|
||||
};
|
||||
use imphnen_entities::{PermissionsItemDto, RolesDetailItemDto};
|
||||
use imphnen_entities::{MessageResponseDto, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto, ResponseSuccessDto};
|
||||
use imphnen_iam::v1::auth::auth_dto::{AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, TokenDto};
|
||||
@@ -113,6 +127,12 @@ use utoipa::{
|
||||
mentors_controller::put_update_mentor,
|
||||
mentors_controller::put_verify_mentor,
|
||||
mentors_controller::delete_mentor,
|
||||
sessions_controller::post_book_session,
|
||||
sessions_controller::get_mentor_sessions,
|
||||
sessions_controller::get_mentor_availability,
|
||||
sessions_controller::put_update_session_status,
|
||||
sessions_controller::post_submit_feedback,
|
||||
sessions_controller::get_my_sessions,
|
||||
hackathon_controller::create_hackathon,
|
||||
hackathon_controller::get_hackathon,
|
||||
hackathon_controller::list_hackathons,
|
||||
@@ -131,6 +151,12 @@ use utoipa::{
|
||||
hackathon_controller::update_hackathon_submission,
|
||||
hackathon_controller::submit_hackathon_submission,
|
||||
hackathon_controller::delete_hackathon_submission,
|
||||
registration_controller::post_register_hackathon,
|
||||
registration_controller::get_hackathon_registrations,
|
||||
registration_controller::get_my_hackathons,
|
||||
registration_controller::put_update_registration_status,
|
||||
registration_controller::post_check_in_participant,
|
||||
registration_controller::get_registration_stats,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
@@ -191,6 +217,21 @@ use utoipa::{
|
||||
ResponseListSuccessDto<Vec<MentorListResponseDto>>,
|
||||
ResponseSuccessDto<MentorDetailResponseDto>,
|
||||
ResponseSuccessDto<MentorRegisterResponseDto>,
|
||||
BookSessionRequestDto,
|
||||
BookSessionResponseDto,
|
||||
SessionListResponseDto,
|
||||
SessionListItemDto,
|
||||
MentorAvailabilityDto,
|
||||
AvailabilitySlotDto,
|
||||
UpdateSessionStatusRequestDto,
|
||||
UpdateSessionStatusResponseDto,
|
||||
SessionFeedbackRequestDto,
|
||||
SessionFeedbackResponseDto,
|
||||
ResponseSuccessDto<BookSessionResponseDto>,
|
||||
ResponseSuccessDto<SessionListResponseDto>,
|
||||
ResponseSuccessDto<MentorAvailabilityDto>,
|
||||
ResponseSuccessDto<UpdateSessionStatusResponseDto>,
|
||||
ResponseSuccessDto<SessionFeedbackResponseDto>,
|
||||
TeamsCreateRequestDto,
|
||||
TeamsUpdateRequestDto,
|
||||
TeamInviteRequestDto,
|
||||
@@ -214,6 +255,24 @@ use utoipa::{
|
||||
HackathonTimelineDto,
|
||||
HackathonTimelineUpdateRequestDto,
|
||||
HackathonUpdateRequestDto,
|
||||
RegistrationRequestDto,
|
||||
RegistrationResponseDto,
|
||||
RegistrationListResponseDto,
|
||||
RegistrationListItemDto,
|
||||
UpdateRegistrationStatusRequestDto,
|
||||
UpdateRegistrationStatusResponseDto,
|
||||
CheckInResponseDto,
|
||||
RegistrationStatsDto,
|
||||
UserHackathonsResponseDto,
|
||||
UserHackathonDto,
|
||||
RegistrationStatus,
|
||||
ParticipantRole,
|
||||
ResponseSuccessDto<RegistrationResponseDto>,
|
||||
ResponseSuccessDto<RegistrationListResponseDto>,
|
||||
ResponseSuccessDto<UpdateRegistrationStatusResponseDto>,
|
||||
ResponseSuccessDto<CheckInResponseDto>,
|
||||
ResponseSuccessDto<RegistrationStatsDto>,
|
||||
ResponseSuccessDto<UserHackathonsResponseDto>,
|
||||
ResponseListSuccessDto<Vec<HackathonDto>>,
|
||||
ResponseSuccessDto<HackathonDto>,
|
||||
ResponseListSuccessDto<Vec<HackathonEventDto>>,
|
||||
@@ -247,11 +306,13 @@ use utoipa::{
|
||||
(name = "Testimonials", description = "Testimonial Management Endpoints"),
|
||||
(name = "Mentors", description = "Mentor Management Endpoints"),
|
||||
(name = "Mentors - Admin", description = "Mentor Admin Management Endpoints (Admin Access Required)"),
|
||||
(name = "sessions", description = "Mentoring Sessions Management API"),
|
||||
(name = "Gacha", description = "Gacha System Endpoints"),
|
||||
(name = "Hackathons", description = "Hackathon Management Endpoints"),
|
||||
(name = "Hackathon Events", description = "Hackathon Event Management Endpoints"),
|
||||
(name = "Hackathon Timeline", description = "Hackathon Timeline Management Endpoints"),
|
||||
(name = "Hackathon Submissions", description = "Hackathon Submission Management Endpoints"),
|
||||
(name = "registrations", description = "Hackathon Registration Management API"),
|
||||
)
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use axum::Router;
|
||||
|
||||
pub mod hackathon;
|
||||
pub mod registrations;
|
||||
|
||||
// Export the router function from hackathon module
|
||||
pub use hackathon::hackathon_router;
|
||||
pub use registrations::registrations_router;
|
||||
|
||||
// Main route constructor
|
||||
pub fn hackathon_protected_routes() -> Router {
|
||||
@@ -14,6 +16,7 @@ pub fn hackathon_protected_routes() -> Router {
|
||||
.nest("/hackathons", hackathon_router())
|
||||
.route("/hackathons/submissions/{id}/status", axum::routing::patch(update_submission_status))
|
||||
.route("/hackathons/{hackathon_id}/admin/results", axum::routing::get(get_admin_hackathon_results))
|
||||
.merge(registrations_router())
|
||||
}
|
||||
|
||||
// Public routes for hackathons (only listing and retrieving)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
pub mod registration_controller;
|
||||
pub mod registration_dto;
|
||||
pub mod registration_repository;
|
||||
pub mod registration_schema;
|
||||
pub mod registration_service;
|
||||
|
||||
pub use registration_controller::*;
|
||||
pub use registration_dto::*;
|
||||
pub use registration_repository::*;
|
||||
pub use registration_schema::*;
|
||||
pub use registration_service::*;
|
||||
@@ -0,0 +1,291 @@
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::Response,
|
||||
routing::{get, post, put},
|
||||
Json, Router,
|
||||
};
|
||||
use imphnen_entities::ResponseSuccessDto;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::{common_response, extract_email, make_thing_from_enum};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
|
||||
use super::{
|
||||
CheckInResponseDto, RegistrationListResponseDto, RegistrationRequestDto,
|
||||
RegistrationResponseDto, RegistrationStatsDto, RegistrationsService,
|
||||
UpdateRegistrationStatusRequestDto, UpdateRegistrationStatusResponseDto,
|
||||
UserHackathonsResponseDto,
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// POST /v1/hackathons/{id}/register
|
||||
// ============================================
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/hackathons/{id}/register",
|
||||
tag = "registrations",
|
||||
summary = "Register for a hackathon",
|
||||
description = "Submit a registration for a hackathon. User must be authenticated.",
|
||||
params(
|
||||
("id" = String, Path, description = "Hackathon ID")
|
||||
),
|
||||
request_body = RegistrationRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Registration submitted successfully", body = ResponseSuccessDto<RegistrationResponseDto>),
|
||||
(status = 400, description = "Invalid input or validation error"),
|
||||
(status = 401, description = "Unauthorized - authentication required"),
|
||||
(status = 409, description = "User already registered for this hackathon"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn post_register_hackathon(
|
||||
Extension(state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Json(data): Json<RegistrationRequestDto>,
|
||||
) -> Response {
|
||||
// Authentication
|
||||
let user_email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
};
|
||||
|
||||
// Parse hackathon ID
|
||||
let hackathon_id = make_thing_from_enum(ResourceEnum::Hackathons, &id);
|
||||
|
||||
let service = RegistrationsService::new(&state);
|
||||
service.register_hackathon(&hackathon_id, &user_email, data).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GET /v1/hackathons/{id}/registrations
|
||||
// ============================================
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/hackathons/{id}/registrations",
|
||||
tag = "registrations",
|
||||
summary = "List hackathon registrations",
|
||||
description = "Get all registrations for a hackathon. Requires admin/organizer permissions. Optional status filter.",
|
||||
params(
|
||||
("id" = String, Path, description = "Hackathon ID"),
|
||||
("status" = Option<String>, Query, description = "Filter by status: pending, approved, rejected, waitlisted, cancelled")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Registrations retrieved successfully", body = ResponseSuccessDto<RegistrationListResponseDto>),
|
||||
(status = 400, description = "Invalid input"),
|
||||
(status = 401, description = "Unauthorized - authentication required"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_hackathon_registrations(
|
||||
Extension(state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
|
||||
) -> Response {
|
||||
// Authentication
|
||||
match extract_email(&headers) {
|
||||
Some(_) => {},
|
||||
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
};
|
||||
|
||||
// Parse hackathon ID
|
||||
let hackathon_id = make_thing_from_enum(ResourceEnum::Hackathons, &id);
|
||||
|
||||
let status_filter = params.get("status").cloned();
|
||||
|
||||
let service = RegistrationsService::new(&state);
|
||||
service.get_hackathon_registrations(&hackathon_id, status_filter).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GET /v1/users/me/hackathons
|
||||
// ============================================
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/users/me/hackathons",
|
||||
tag = "registrations",
|
||||
summary = "Get my hackathon registrations",
|
||||
description = "Get all hackathons the current user has registered for.",
|
||||
responses(
|
||||
(status = 200, description = "Hackathons retrieved successfully", body = ResponseSuccessDto<UserHackathonsResponseDto>),
|
||||
(status = 401, description = "Unauthorized - authentication required"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_my_hackathons(
|
||||
Extension(state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
// Authentication
|
||||
let user_email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
};
|
||||
|
||||
let service = RegistrationsService::new(&state);
|
||||
service.get_my_hackathons(&user_email).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PUT /v1/hackathons/{hackathon_id}/registrations/{registration_id}/status
|
||||
// ============================================
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/hackathons/{hackathon_id}/registrations/{registration_id}/status",
|
||||
tag = "registrations",
|
||||
summary = "Update registration status",
|
||||
description = "Approve, reject, or update the status of a registration. Requires admin/organizer permissions.",
|
||||
params(
|
||||
("hackathon_id" = String, Path, description = "Hackathon ID"),
|
||||
("registration_id" = String, Path, description = "Registration ID")
|
||||
),
|
||||
request_body = UpdateRegistrationStatusRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Status updated successfully", body = ResponseSuccessDto<UpdateRegistrationStatusResponseDto>),
|
||||
(status = 400, description = "Invalid input or validation error"),
|
||||
(status = 401, description = "Unauthorized - authentication required"),
|
||||
(status = 404, description = "Registration not found"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn put_update_registration_status(
|
||||
Extension(state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path((_hackathon_id, registration_id)): Path<(String, String)>,
|
||||
Json(data): Json<UpdateRegistrationStatusRequestDto>,
|
||||
) -> Response {
|
||||
// Authentication
|
||||
match extract_email(&headers) {
|
||||
Some(_) => {},
|
||||
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
};
|
||||
|
||||
// Parse registration ID
|
||||
let reg_id = make_thing_from_enum(ResourceEnum::HackathonRegistrations, ®istration_id);
|
||||
|
||||
let service = RegistrationsService::new(&state);
|
||||
service.update_registration_status(®_id, data).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// POST /v1/hackathons/{hackathon_id}/registrations/{registration_id}/check-in
|
||||
// ============================================
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/hackathons/{hackathon_id}/registrations/{registration_id}/check-in",
|
||||
tag = "registrations",
|
||||
summary = "Check-in participant",
|
||||
description = "Mark a participant as checked in for the hackathon. Requires admin/organizer permissions.",
|
||||
params(
|
||||
("hackathon_id" = String, Path, description = "Hackathon ID"),
|
||||
("registration_id" = String, Path, description = "Registration ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Participant checked in successfully", body = ResponseSuccessDto<CheckInResponseDto>),
|
||||
(status = 400, description = "Invalid request - participant not approved or already checked in"),
|
||||
(status = 401, description = "Unauthorized - authentication required"),
|
||||
(status = 404, description = "Registration not found"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn post_check_in_participant(
|
||||
Extension(state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path((_hackathon_id, registration_id)): Path<(String, String)>,
|
||||
) -> Response {
|
||||
// Authentication
|
||||
match extract_email(&headers) {
|
||||
Some(_) => {},
|
||||
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
};
|
||||
|
||||
// Parse registration ID
|
||||
let reg_id = make_thing_from_enum(ResourceEnum::HackathonRegistrations, ®istration_id);
|
||||
|
||||
let service = RegistrationsService::new(&state);
|
||||
service.check_in_participant(®_id).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GET /v1/hackathons/{id}/registrations/stats
|
||||
// ============================================
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/hackathons/{id}/registrations/stats",
|
||||
tag = "registrations",
|
||||
summary = "Get registration statistics",
|
||||
description = "Get comprehensive statistics about hackathon registrations. Requires admin/organizer permissions.",
|
||||
params(
|
||||
("id" = String, Path, description = "Hackathon ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Statistics retrieved successfully", body = ResponseSuccessDto<RegistrationStatsDto>),
|
||||
(status = 400, description = "Invalid input"),
|
||||
(status = 401, description = "Unauthorized - authentication required"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_registration_stats(
|
||||
Extension(state): Extension<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
// Authentication
|
||||
match extract_email(&headers) {
|
||||
Some(_) => {},
|
||||
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
};
|
||||
|
||||
// Parse hackathon ID
|
||||
let hackathon_id = make_thing_from_enum(ResourceEnum::Hackathons, &id);
|
||||
|
||||
let service = RegistrationsService::new(&state);
|
||||
service.get_registration_stats(&hackathon_id).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Router
|
||||
// ============================================
|
||||
pub fn registrations_router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/hackathons/:id/register",
|
||||
post(post_register_hackathon),
|
||||
)
|
||||
.route(
|
||||
"/hackathons/:id/registrations",
|
||||
get(get_hackathon_registrations),
|
||||
)
|
||||
.route(
|
||||
"/hackathons/:id/registrations/stats",
|
||||
get(get_registration_stats),
|
||||
)
|
||||
.route(
|
||||
"/hackathons/:hackathon_id/registrations/:registration_id/status",
|
||||
put(put_update_registration_status),
|
||||
)
|
||||
.route(
|
||||
"/hackathons/:hackathon_id/registrations/:registration_id/check-in",
|
||||
post(post_check_in_participant),
|
||||
)
|
||||
.route("/users/me/hackathons", get(get_my_hackathons))
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
use super::{ParticipantRole, RegistrationStatus};
|
||||
|
||||
// ============================================
|
||||
// Registration Request/Response DTOs
|
||||
// ============================================
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct RegistrationRequestDto {
|
||||
pub team_id: Option<String>,
|
||||
pub role: Option<ParticipantRole>,
|
||||
|
||||
#[validate(length(max = 1000, message = "Motivation must not exceed 1000 characters"))]
|
||||
pub motivation: Option<String>,
|
||||
|
||||
pub skills: Option<Vec<String>>,
|
||||
|
||||
#[validate(custom(function = "validate_experience_level"))]
|
||||
pub experience_level: Option<String>,
|
||||
|
||||
#[validate(length(max = 100))]
|
||||
pub github_username: Option<String>,
|
||||
|
||||
#[validate(url(message = "Invalid portfolio URL"))]
|
||||
pub portfolio_url: Option<String>,
|
||||
|
||||
pub dietary_requirements: Option<String>,
|
||||
|
||||
#[validate(custom(function = "validate_tshirt_size"))]
|
||||
pub tshirt_size: Option<String>,
|
||||
|
||||
#[validate(length(max = 100))]
|
||||
pub emergency_contact_name: Option<String>,
|
||||
|
||||
#[validate(length(max = 20))]
|
||||
pub emergency_contact_phone: Option<String>,
|
||||
}
|
||||
|
||||
fn validate_experience_level(level: &str) -> Result<(), validator::ValidationError> {
|
||||
let valid_levels = ["beginner", "intermediate", "advanced"];
|
||||
if valid_levels.contains(&level) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(validator::ValidationError::new("Invalid experience level"))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_tshirt_size(size: &str) -> Result<(), validator::ValidationError> {
|
||||
let valid_sizes = ["XS", "S", "M", "L", "XL", "XXL"];
|
||||
if valid_sizes.contains(&size) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(validator::ValidationError::new("Invalid t-shirt size"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RegistrationResponseDto {
|
||||
pub id: String,
|
||||
pub hackathon_id: String,
|
||||
pub user_id: String,
|
||||
pub team_id: Option<String>,
|
||||
pub status: RegistrationStatus,
|
||||
pub role: ParticipantRole,
|
||||
pub registration_date: String,
|
||||
pub checked_in: bool,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List Registrations DTOs
|
||||
// ============================================
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RegistrationListItemDto {
|
||||
pub id: String,
|
||||
pub hackathon_id: String,
|
||||
pub hackathon_name: Option<String>,
|
||||
pub user_id: String,
|
||||
pub user_fullname: Option<String>,
|
||||
pub user_email: Option<String>,
|
||||
pub team_id: Option<String>,
|
||||
pub team_name: Option<String>,
|
||||
pub status: RegistrationStatus,
|
||||
pub role: ParticipantRole,
|
||||
pub registration_date: String,
|
||||
pub checked_in: bool,
|
||||
pub check_in_time: Option<String>,
|
||||
pub experience_level: Option<String>,
|
||||
pub skills: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RegistrationListResponseDto {
|
||||
pub registrations: Vec<RegistrationListItemDto>,
|
||||
pub total: usize,
|
||||
pub status_filter: Option<String>,
|
||||
}
|
||||
|
||||
// Internal query DTO (fields already as String from DB)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegistrationListQueryDto {
|
||||
pub id: String,
|
||||
pub hackathon_id: String,
|
||||
pub hackathon_name: Option<String>,
|
||||
pub user_id: String,
|
||||
pub user_fullname: Option<String>,
|
||||
pub user_email: Option<String>,
|
||||
pub team_id: Option<String>,
|
||||
pub team_name: Option<String>,
|
||||
pub status: RegistrationStatus,
|
||||
pub role: ParticipantRole,
|
||||
pub registration_date: String,
|
||||
pub checked_in: bool,
|
||||
pub check_in_time: Option<String>,
|
||||
pub experience_level: Option<String>,
|
||||
pub skills: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Status DTOs
|
||||
// ============================================
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateRegistrationStatusRequestDto {
|
||||
pub status: RegistrationStatus,
|
||||
|
||||
#[validate(length(max = 500, message = "Reason must not exceed 500 characters"))]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateRegistrationStatusResponseDto {
|
||||
pub id: String,
|
||||
pub status: RegistrationStatus,
|
||||
pub updated_at: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Check-in DTOs
|
||||
// ============================================
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CheckInResponseDto {
|
||||
pub id: String,
|
||||
pub user_fullname: Option<String>,
|
||||
pub checked_in: bool,
|
||||
pub check_in_time: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Statistics DTOs
|
||||
// ============================================
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RegistrationStatsDto {
|
||||
pub hackathon_id: String,
|
||||
pub hackathon_name: Option<String>,
|
||||
pub total_registrations: usize,
|
||||
pub pending: usize,
|
||||
pub approved: usize,
|
||||
pub rejected: usize,
|
||||
pub waitlisted: usize,
|
||||
pub cancelled: usize,
|
||||
pub checked_in: usize,
|
||||
pub team_registrations: usize,
|
||||
pub individual_registrations: usize,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// User's Hackathons DTOs
|
||||
// ============================================
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UserHackathonDto {
|
||||
pub registration_id: String,
|
||||
pub hackathon_id: String,
|
||||
pub hackathon_name: String,
|
||||
pub hackathon_description: Option<String>,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub status: RegistrationStatus,
|
||||
pub role: ParticipantRole,
|
||||
pub registration_date: String,
|
||||
pub checked_in: bool,
|
||||
pub team_id: Option<String>,
|
||||
pub team_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UserHackathonsResponseDto {
|
||||
pub hackathons: Vec<UserHackathonDto>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
// Internal query DTO
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserHackathonQueryDto {
|
||||
pub registration_id: String,
|
||||
pub hackathon_id: String,
|
||||
pub hackathon_name: String,
|
||||
pub hackathon_description: Option<String>,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub status: RegistrationStatus,
|
||||
pub role: ParticipantRole,
|
||||
pub registration_date: String,
|
||||
pub checked_in: bool,
|
||||
pub team_id: Option<String>,
|
||||
pub team_name: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
use super::{RegistrationListQueryDto, RegistrationSchema, RegistrationStatus, UserHackathonQueryDto};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::get_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
pub struct RegistrationsRepository<'a> {
|
||||
pub state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> RegistrationsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Create Registration
|
||||
// ============================================
|
||||
pub async fn create_registration(&self, registration: RegistrationSchema) -> Result<RegistrationSchema, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let created: Option<RegistrationSchema> = db
|
||||
.create("hackathon_registrations")
|
||||
.content(registration)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create registration: {}", e))?;
|
||||
|
||||
created.ok_or_else(|| "Registration creation returned None".to_string())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Registration by ID
|
||||
// ============================================
|
||||
pub async fn query_registration_by_id(&self, id: &Thing) -> Result<Option<RegistrationSchema>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let registration: Option<RegistrationSchema> = db
|
||||
.select(record_key)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch registration: {}", e))?;
|
||||
|
||||
Ok(registration)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Check if User Already Registered
|
||||
// ============================================
|
||||
pub async fn check_existing_registration(
|
||||
&self,
|
||||
hackathon_id: &Thing,
|
||||
user_id: &Thing,
|
||||
) -> Result<Option<RegistrationSchema>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let query = r#"
|
||||
SELECT * FROM hackathon_registrations
|
||||
WHERE hackathon_id = $hackathon_id
|
||||
AND user_id = $user_id
|
||||
AND is_deleted = false
|
||||
LIMIT 1
|
||||
"#;
|
||||
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("hackathon_id", hackathon_id.clone()))
|
||||
.bind(("user_id", user_id.clone()))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to check existing registration: {}", e))?;
|
||||
|
||||
let registration: Option<RegistrationSchema> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse registration: {}", e))?;
|
||||
|
||||
Ok(registration)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List Registrations for Hackathon
|
||||
// ============================================
|
||||
pub async fn query_hackathon_registrations(
|
||||
&self,
|
||||
hackathon_id: &Thing,
|
||||
status_filter: Option<RegistrationStatus>,
|
||||
) -> Result<Vec<RegistrationListQueryDto>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let query = if status_filter.is_some() {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
hackathon_id,
|
||||
(SELECT name FROM $parent.hackathon_id)[0].name AS hackathon_name,
|
||||
user_id,
|
||||
(SELECT fullname FROM $parent.user_id)[0].fullname AS user_fullname,
|
||||
(SELECT email FROM $parent.user_id)[0].email AS user_email,
|
||||
team_id,
|
||||
(SELECT name FROM $parent.team_id)[0].name AS team_name,
|
||||
status,
|
||||
role,
|
||||
registration_date,
|
||||
checked_in,
|
||||
check_in_time,
|
||||
experience_level,
|
||||
skills
|
||||
FROM hackathon_registrations
|
||||
WHERE hackathon_id = $hackathon_id
|
||||
AND status = $status
|
||||
AND is_deleted = false
|
||||
ORDER BY registration_date DESC
|
||||
"#
|
||||
} else {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
hackathon_id,
|
||||
(SELECT name FROM $parent.hackathon_id)[0].name AS hackathon_name,
|
||||
user_id,
|
||||
(SELECT fullname FROM $parent.user_id)[0].fullname AS user_fullname,
|
||||
(SELECT email FROM $parent.user_id)[0].email AS user_email,
|
||||
team_id,
|
||||
(SELECT name FROM $parent.team_id)[0].name AS team_name,
|
||||
status,
|
||||
role,
|
||||
registration_date,
|
||||
checked_in,
|
||||
check_in_time,
|
||||
experience_level,
|
||||
skills
|
||||
FROM hackathon_registrations
|
||||
WHERE hackathon_id = $hackathon_id
|
||||
AND is_deleted = false
|
||||
ORDER BY registration_date DESC
|
||||
"#
|
||||
};
|
||||
|
||||
let hackathon_id_clone = hackathon_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("hackathon_id", hackathon_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("hackathon_id", hackathon_id_clone))
|
||||
.await
|
||||
}
|
||||
.map_err(|e| format!("Failed to query hackathon registrations: {}", e))?;
|
||||
|
||||
let registrations: Vec<RegistrationListQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse registrations: {}", e))?;
|
||||
|
||||
Ok(registrations)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get User's Hackathon Registrations
|
||||
// ============================================
|
||||
pub async fn query_user_hackathons(&self, user_id: &Thing) -> Result<Vec<UserHackathonQueryDto>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let query = r#"
|
||||
SELECT
|
||||
id AS registration_id,
|
||||
hackathon_id,
|
||||
(SELECT name FROM $parent.hackathon_id)[0].name AS hackathon_name,
|
||||
(SELECT description FROM $parent.hackathon_id)[0].description AS hackathon_description,
|
||||
(SELECT start_date FROM $parent.hackathon_id)[0].start_date AS start_date,
|
||||
(SELECT end_date FROM $parent.hackathon_id)[0].end_date AS end_date,
|
||||
status,
|
||||
role,
|
||||
registration_date,
|
||||
checked_in,
|
||||
team_id,
|
||||
(SELECT name FROM $parent.team_id)[0].name AS team_name
|
||||
FROM hackathon_registrations
|
||||
WHERE user_id = $user_id
|
||||
AND is_deleted = false
|
||||
ORDER BY registration_date DESC
|
||||
"#;
|
||||
|
||||
let user_id_clone = user_id.clone();
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to query user hackathons: {}", e))?;
|
||||
|
||||
let hackathons: Vec<UserHackathonQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse user hackathons: {}", e))?;
|
||||
|
||||
Ok(hackathons)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Registration Statistics
|
||||
// ============================================
|
||||
pub async fn query_registration_stats(&self, hackathon_id: &Thing) -> Result<RegistrationStatsQueryDto, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let query = r#"
|
||||
LET $hackathon = (SELECT name FROM $hackathon_id)[0].name;
|
||||
LET $regs = (SELECT * FROM hackathon_registrations WHERE hackathon_id = $hackathon_id AND is_deleted = false);
|
||||
RETURN {
|
||||
hackathon_id: $hackathon_id,
|
||||
hackathon_name: $hackathon,
|
||||
total_registrations: count($regs),
|
||||
pending: count($regs[WHERE status = 'pending']),
|
||||
approved: count($regs[WHERE status = 'approved']),
|
||||
rejected: count($regs[WHERE status = 'rejected']),
|
||||
waitlisted: count($regs[WHERE status = 'waitlisted']),
|
||||
cancelled: count($regs[WHERE status = 'cancelled']),
|
||||
checked_in: count($regs[WHERE checked_in = true]),
|
||||
team_registrations: count($regs[WHERE team_id != NONE]),
|
||||
individual_registrations: count($regs[WHERE team_id = NONE])
|
||||
};
|
||||
"#;
|
||||
|
||||
let hackathon_id_clone = hackathon_id.clone();
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("hackathon_id", hackathon_id_clone))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to query registration stats: {}", e))?;
|
||||
|
||||
let stats: Option<RegistrationStatsQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse registration stats: {}", e))?;
|
||||
|
||||
stats.ok_or_else(|| "Stats query returned None".to_string())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Registration
|
||||
// ============================================
|
||||
pub async fn update_registration(&self, id: &Thing, registration: RegistrationSchema) -> Result<RegistrationSchema, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let updated: Option<RegistrationSchema> = db
|
||||
.update(record_key)
|
||||
.content(registration)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to update registration: {}", e))?;
|
||||
|
||||
updated.ok_or_else(|| "Registration update returned None".to_string())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Delete Registration (soft delete)
|
||||
// ============================================
|
||||
pub async fn delete_registration(&self, id: &Thing) -> Result<(), String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let _: Option<RegistrationSchema> = db
|
||||
.delete(record_key)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete registration: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Helper DTO for stats query
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegistrationStatsQueryDto {
|
||||
pub hackathon_id: String,
|
||||
pub hackathon_name: Option<String>,
|
||||
pub total_registrations: usize,
|
||||
pub pending: usize,
|
||||
pub approved: usize,
|
||||
pub rejected: usize,
|
||||
pub waitlisted: usize,
|
||||
pub cancelled: usize,
|
||||
pub checked_in: usize,
|
||||
pub team_registrations: usize,
|
||||
pub individual_registrations: usize,
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing, make_thing_from_enum};
|
||||
|
||||
use super::RegistrationRequestDto;
|
||||
|
||||
/// Registration status enum
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RegistrationStatus {
|
||||
Pending,
|
||||
Approved,
|
||||
Rejected,
|
||||
Waitlisted,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// Participant role in hackathon
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ParticipantRole {
|
||||
Individual,
|
||||
TeamLeader,
|
||||
TeamMember,
|
||||
}
|
||||
|
||||
/// Hackathon registration schema
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RegistrationSchema {
|
||||
pub id: Thing,
|
||||
pub hackathon_id: Thing,
|
||||
pub user_id: Thing,
|
||||
pub team_id: Option<Thing>,
|
||||
pub status: RegistrationStatus,
|
||||
pub role: ParticipantRole,
|
||||
pub registration_date: String,
|
||||
pub approved_at: Option<String>,
|
||||
pub rejected_at: Option<String>,
|
||||
pub rejection_reason: Option<String>,
|
||||
pub checked_in: bool,
|
||||
pub check_in_time: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub skills: Option<Vec<String>>,
|
||||
pub experience_level: Option<String>, // beginner, intermediate, advanced
|
||||
pub github_username: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
pub motivation: Option<String>,
|
||||
pub dietary_requirements: Option<String>,
|
||||
pub tshirt_size: Option<String>, // XS, S, M, L, XL, XXL
|
||||
pub emergency_contact_name: Option<String>,
|
||||
pub emergency_contact_phone: Option<String>,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl RegistrationSchema {
|
||||
/// Create a new registration from request DTO
|
||||
pub fn from_request(
|
||||
hackathon_id: &Thing,
|
||||
user_id: &Thing,
|
||||
data: RegistrationRequestDto,
|
||||
) -> Result<Self, String> {
|
||||
let now = get_iso_date();
|
||||
|
||||
// Convert team_id from String to Thing if provided
|
||||
let team_id_thing = data.team_id
|
||||
.as_ref()
|
||||
.map(|id| make_thing_from_enum(ResourceEnum::Teams, id));
|
||||
|
||||
Ok(Self {
|
||||
id: make_thing(ResourceEnum::HackathonRegistrations.as_str(), &uuid::Uuid::new_v4().to_string()),
|
||||
hackathon_id: hackathon_id.clone(),
|
||||
user_id: user_id.clone(),
|
||||
team_id: team_id_thing,
|
||||
status: RegistrationStatus::Pending,
|
||||
role: data.role.unwrap_or(ParticipantRole::Individual),
|
||||
registration_date: now.clone(),
|
||||
approved_at: None,
|
||||
rejected_at: None,
|
||||
rejection_reason: None,
|
||||
checked_in: false,
|
||||
check_in_time: None,
|
||||
notes: None,
|
||||
skills: data.skills,
|
||||
experience_level: data.experience_level,
|
||||
github_username: data.github_username,
|
||||
portfolio_url: data.portfolio_url,
|
||||
motivation: data.motivation,
|
||||
dietary_requirements: data.dietary_requirements,
|
||||
tshirt_size: data.tshirt_size,
|
||||
emergency_contact_name: data.emergency_contact_name,
|
||||
emergency_contact_phone: data.emergency_contact_phone,
|
||||
is_deleted: false,
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update registration status
|
||||
pub fn update_status(&mut self, status: RegistrationStatus, reason: Option<String>) {
|
||||
let now = get_iso_date();
|
||||
self.status = status.clone();
|
||||
self.updated_at = now.clone();
|
||||
|
||||
match status {
|
||||
RegistrationStatus::Approved => {
|
||||
self.approved_at = Some(now);
|
||||
self.rejected_at = None;
|
||||
self.rejection_reason = None;
|
||||
}
|
||||
RegistrationStatus::Rejected => {
|
||||
self.rejected_at = Some(now);
|
||||
self.rejection_reason = reason;
|
||||
self.approved_at = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check-in participant
|
||||
pub fn check_in(&mut self) -> Result<(), String> {
|
||||
if self.status != RegistrationStatus::Approved {
|
||||
return Err("Only approved registrations can be checked in".to_string());
|
||||
}
|
||||
|
||||
if self.checked_in {
|
||||
return Err("Already checked in".to_string());
|
||||
}
|
||||
|
||||
let now = get_iso_date();
|
||||
self.checked_in = true;
|
||||
self.check_in_time = Some(now.clone());
|
||||
self.updated_at = now;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
use axum::response::Response;
|
||||
use axum::http::StatusCode;
|
||||
use imphnen_entities::ResponseSuccessDto;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::{
|
||||
common_response, extract_id, make_thing_from_enum, success_response, validate_request,
|
||||
};
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
use super::{
|
||||
CheckInResponseDto, RegistrationListItemDto, RegistrationListResponseDto,
|
||||
RegistrationRequestDto, RegistrationResponseDto, RegistrationSchema, RegistrationStatsDto,
|
||||
RegistrationStatus, RegistrationsRepository, UpdateRegistrationStatusRequestDto,
|
||||
UpdateRegistrationStatusResponseDto, UserHackathonDto, UserHackathonsResponseDto,
|
||||
};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
|
||||
pub struct RegistrationsService<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> RegistrationsService<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Register for Hackathon
|
||||
// ============================================
|
||||
pub async fn register_hackathon(
|
||||
&self,
|
||||
hackathon_id: &Thing,
|
||||
user_email: &str,
|
||||
data: RegistrationRequestDto,
|
||||
) -> Response {
|
||||
// Validate request
|
||||
if let Err((status, message)) = validate_request(&data) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let repository = RegistrationsRepository::new(self.state);
|
||||
|
||||
// Get user ID from email
|
||||
let user_id = make_thing_from_enum(ResourceEnum::Users, user_email);
|
||||
|
||||
// Check if hackathon exists
|
||||
// TODO: Add hackathon existence check via hackathon repository
|
||||
|
||||
// Check if user already registered
|
||||
match repository
|
||||
.check_existing_registration(hackathon_id, &user_id)
|
||||
.await
|
||||
{
|
||||
Ok(Some(_)) => {
|
||||
return common_response(StatusCode::CONFLICT, "You have already registered for this hackathon")
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
|
||||
}
|
||||
|
||||
// Create registration
|
||||
let registration = match RegistrationSchema::from_request(hackathon_id, &user_id, data) {
|
||||
Ok(reg) => reg,
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &e),
|
||||
};
|
||||
|
||||
match repository.create_registration(registration).await {
|
||||
Ok(created) => {
|
||||
let response = RegistrationResponseDto {
|
||||
id: extract_id(&created.id),
|
||||
hackathon_id: extract_id(&created.hackathon_id),
|
||||
user_id: extract_id(&created.user_id),
|
||||
team_id: created.team_id.as_ref().map(|t| extract_id(t)),
|
||||
status: created.status,
|
||||
role: created.role,
|
||||
registration_date: created.registration_date,
|
||||
checked_in: created.checked_in,
|
||||
message: "Registration submitted successfully. You will be notified once approved."
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List Registrations for Hackathon
|
||||
// ============================================
|
||||
pub async fn get_hackathon_registrations(
|
||||
&self,
|
||||
hackathon_id: &Thing,
|
||||
status_filter: Option<String>,
|
||||
) -> Response {
|
||||
let repository = RegistrationsRepository::new(self.state);
|
||||
|
||||
// Parse status filter if provided
|
||||
let status_enum = if let Some(status_str) = &status_filter {
|
||||
match status_str.to_lowercase().as_str() {
|
||||
"pending" => Some(RegistrationStatus::Pending),
|
||||
"approved" => Some(RegistrationStatus::Approved),
|
||||
"rejected" => Some(RegistrationStatus::Rejected),
|
||||
"waitlisted" => Some(RegistrationStatus::Waitlisted),
|
||||
"cancelled" => Some(RegistrationStatus::Cancelled),
|
||||
_ => return common_response(StatusCode::BAD_REQUEST, "Invalid status filter"),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match repository
|
||||
.query_hackathon_registrations(hackathon_id, status_enum)
|
||||
.await
|
||||
{
|
||||
Ok(results) => {
|
||||
let registrations = results
|
||||
.into_iter()
|
||||
.map(|r| RegistrationListItemDto {
|
||||
id: r.id,
|
||||
hackathon_id: r.hackathon_id,
|
||||
hackathon_name: r.hackathon_name,
|
||||
user_id: r.user_id,
|
||||
user_fullname: r.user_fullname,
|
||||
user_email: r.user_email,
|
||||
team_id: r.team_id,
|
||||
team_name: r.team_name,
|
||||
status: r.status,
|
||||
role: r.role,
|
||||
registration_date: r.registration_date,
|
||||
checked_in: r.checked_in,
|
||||
check_in_time: r.check_in_time,
|
||||
experience_level: r.experience_level,
|
||||
skills: r.skills,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let total = registrations.len();
|
||||
let response = RegistrationListResponseDto {
|
||||
registrations,
|
||||
total,
|
||||
status_filter,
|
||||
};
|
||||
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Current User's Hackathon Registrations
|
||||
// ============================================
|
||||
pub async fn get_my_hackathons(&self, user_email: &str) -> Response {
|
||||
let repository = RegistrationsRepository::new(self.state);
|
||||
|
||||
// Get user ID from email
|
||||
let user_id = make_thing_from_enum(ResourceEnum::Users, user_email);
|
||||
|
||||
match repository.query_user_hackathons(&user_id).await {
|
||||
Ok(results) => {
|
||||
let hackathons = results
|
||||
.into_iter()
|
||||
.map(|h| UserHackathonDto {
|
||||
registration_id: h.registration_id,
|
||||
hackathon_id: h.hackathon_id,
|
||||
hackathon_name: h.hackathon_name,
|
||||
hackathon_description: h.hackathon_description,
|
||||
start_date: h.start_date,
|
||||
end_date: h.end_date,
|
||||
status: h.status,
|
||||
role: h.role,
|
||||
registration_date: h.registration_date,
|
||||
checked_in: h.checked_in,
|
||||
team_id: h.team_id,
|
||||
team_name: h.team_name,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let total = hackathons.len();
|
||||
let response = UserHackathonsResponseDto { hackathons, total };
|
||||
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Registration Status
|
||||
// ============================================
|
||||
pub async fn update_registration_status(
|
||||
&self,
|
||||
registration_id: &Thing,
|
||||
data: UpdateRegistrationStatusRequestDto,
|
||||
) -> Response {
|
||||
// Validate request
|
||||
if let Err((status, message)) = validate_request(&data) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let repository = RegistrationsRepository::new(self.state);
|
||||
|
||||
// Get existing registration
|
||||
let mut registration = match repository.query_registration_by_id(registration_id).await {
|
||||
Ok(Some(reg)) => reg,
|
||||
Ok(None) => return common_response(StatusCode::NOT_FOUND, "Registration not found"),
|
||||
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
|
||||
};
|
||||
|
||||
// Update status
|
||||
registration.update_status(data.status.clone(), data.reason);
|
||||
|
||||
// Save updated registration
|
||||
match repository.update_registration(registration_id, registration.clone()).await {
|
||||
Ok(updated) => {
|
||||
let status_clone = updated.status.clone();
|
||||
let response = UpdateRegistrationStatusResponseDto {
|
||||
id: extract_id(&updated.id),
|
||||
status: updated.status,
|
||||
updated_at: updated.updated_at,
|
||||
message: format!("Registration status updated to {:?}", status_clone),
|
||||
};
|
||||
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Check-in Participant
|
||||
// ============================================
|
||||
pub async fn check_in_participant(&self, registration_id: &Thing) -> Response {
|
||||
let repository = RegistrationsRepository::new(self.state);
|
||||
|
||||
// Get existing registration
|
||||
let mut registration = match repository.query_registration_by_id(registration_id).await {
|
||||
Ok(Some(reg)) => reg,
|
||||
Ok(None) => return common_response(StatusCode::NOT_FOUND, "Registration not found"),
|
||||
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
|
||||
};
|
||||
|
||||
// Perform check-in
|
||||
if let Err(e) = registration.check_in() {
|
||||
return common_response(StatusCode::BAD_REQUEST, &e);
|
||||
}
|
||||
|
||||
// Save updated registration
|
||||
match repository.update_registration(registration_id, registration.clone()).await {
|
||||
Ok(updated) => {
|
||||
let response = CheckInResponseDto {
|
||||
id: extract_id(&updated.id),
|
||||
user_fullname: None, // Would need to query user info
|
||||
checked_in: updated.checked_in,
|
||||
check_in_time: updated.check_in_time.unwrap_or_default(),
|
||||
message: "Participant checked in successfully".to_string(),
|
||||
};
|
||||
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Registration Statistics
|
||||
// ============================================
|
||||
pub async fn get_registration_stats(&self, hackathon_id: &Thing) -> Response {
|
||||
let repository = RegistrationsRepository::new(self.state);
|
||||
|
||||
match repository.query_registration_stats(hackathon_id).await {
|
||||
Ok(stats) => {
|
||||
let response = RegistrationStatsDto {
|
||||
hackathon_id: stats.hackathon_id,
|
||||
hackathon_name: stats.hackathon_name,
|
||||
total_registrations: stats.total_registrations,
|
||||
pending: stats.pending,
|
||||
approved: stats.approved,
|
||||
rejected: stats.rejected,
|
||||
waitlisted: stats.waitlisted,
|
||||
cancelled: stats.cancelled,
|
||||
checked_in: stats.checked_in,
|
||||
team_registrations: stats.team_registrations,
|
||||
individual_registrations: stats.individual_registrations,
|
||||
};
|
||||
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,5 +89,7 @@ pub use v1::teams::{
|
||||
PublicTeamsDetailItemDto, TeamsDetailQueryDto, TeamsListQueryDto,
|
||||
TeamMembersSchema, TeamInvitationsSchema, TeamMembersQueryDto,
|
||||
TeamInvitationsQueryDto, MemberTeamsDetailItemDto,
|
||||
AddTeamMemberRequestDto, UpdateMemberRoleRequestDto,
|
||||
TeamInvitationListDto, MyInvitationDto
|
||||
};
|
||||
pub use v1::users::{UsersRepository, UsersSchema, UsersDetailItemDto, UsersCreateRequestDto};
|
||||
|
||||
@@ -43,7 +43,11 @@ pub use teams_dto::{
|
||||
TeamsListQueryDto,
|
||||
TeamMembersQueryDto,
|
||||
TeamInvitationsQueryDto,
|
||||
MemberTeamsDetailItemDto
|
||||
MemberTeamsDetailItemDto,
|
||||
AddTeamMemberRequestDto,
|
||||
UpdateMemberRoleRequestDto,
|
||||
TeamInvitationListDto,
|
||||
MyInvitationDto
|
||||
};
|
||||
|
||||
pub use teams_repository::TeamsRepository;
|
||||
|
||||
@@ -4,7 +4,9 @@ use crate::{
|
||||
TeamsCreateRequestDto, TeamsDetailItemDto, TeamsListItemDto, permissions_guard,
|
||||
TeamsUpdateRequestDto, TeamInviteRequestDto, TeamAcceptInvitationRequestDto,
|
||||
TeamMemberDto, TeamsSearchQueryDto, PublicTeamsListItemDto, PublicTeamsDetailItemDto,
|
||||
AdminTeamsListItemDto, AdminTeamsDetailItemDto, PermissionsEnum
|
||||
AdminTeamsListItemDto, AdminTeamsDetailItemDto, PermissionsEnum,
|
||||
AddTeamMemberRequestDto, UpdateMemberRoleRequestDto,
|
||||
TeamInvitationListDto, MyInvitationDto
|
||||
};
|
||||
use super::super::teams::{TeamsRepository, TeamMembersSchema};
|
||||
use axum::response::Response;
|
||||
@@ -153,12 +155,24 @@ pub async fn put_update_team(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct AddTeamMemberRequestDto {
|
||||
pub user_id: String,
|
||||
pub role: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/teams/{id}/members",
|
||||
params(
|
||||
("id" = String, Path, description = "Team ID")
|
||||
),
|
||||
request_body = AddTeamMemberRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[AUTH] Add member to team successfully", body = ResponseSuccessDto<String>),
|
||||
(status = 401, description = "[AUTH] Unauthorized"),
|
||||
(status = 403, description = "[AUTH] Only team leader or members can add"),
|
||||
(status = 404, description = "[AUTH] Team not found")
|
||||
),
|
||||
tag = "Teams"
|
||||
)]
|
||||
pub async fn post_add_team_member(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
@@ -201,6 +215,24 @@ pub async fn post_add_team_member(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/teams/{id}/members/{user_id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Team ID"),
|
||||
("user_id" = String, Path, description = "User ID to remove")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[AUTH] Member removed successfully", body = ResponseSuccessDto<String>),
|
||||
(status = 401, description = "[AUTH] Unauthorized"),
|
||||
(status = 403, description = "[AUTH] Only team leader can remove members"),
|
||||
(status = 404, description = "[AUTH] Team not found")
|
||||
),
|
||||
tag = "Teams"
|
||||
)]
|
||||
pub async fn delete_remove_team_member(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
@@ -236,6 +268,63 @@ pub async fn delete_remove_team_member(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/teams/{id}/members/{user_id}/role",
|
||||
params(
|
||||
("id" = String, Path, description = "Team ID"),
|
||||
("user_id" = String, Path, description = "User ID")
|
||||
),
|
||||
request_body = UpdateMemberRoleRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[AUTH] Member role updated successfully", body = ResponseSuccessDto<String>),
|
||||
(status = 401, description = "[AUTH] Unauthorized"),
|
||||
(status = 403, description = "[AUTH] Only team leader can update roles"),
|
||||
(status = 404, description = "[AUTH] Team or member not found")
|
||||
),
|
||||
tag = "Teams"
|
||||
)]
|
||||
pub async fn put_update_member_role(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path((team_id, user_id)): Path<(String, String)>,
|
||||
Json(payload): Json<UpdateMemberRoleRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
let state_clone = state.clone();
|
||||
let is_admin = crate::permissions_guard(headers.clone(), axum::Extension(state_clone.clone()), vec![PermissionsEnum::ManageAllTeams]).await.is_ok();
|
||||
|
||||
let auth = permissions_guard(headers, axum::Extension(state.clone()), vec![]).await;
|
||||
let (claims, state) = match auth {
|
||||
Ok((c, s)) => (c, s),
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
let repo = TeamsRepository::new(&state);
|
||||
let thing_id = imphnen_utils::make_thing_from_enum(imphnen_libs::ResourceEnum::Teams, &team_id);
|
||||
let team = match repo.query_team_by_id(&thing_id).await {
|
||||
Ok(t) => t,
|
||||
Err(_) => return crate::common_response(axum::http::StatusCode::NOT_FOUND, "Team not found"),
|
||||
};
|
||||
|
||||
if !is_admin {
|
||||
// Only leader can update roles
|
||||
if team.leader_id.id.to_raw() != claims.user_id {
|
||||
return crate::common_response(axum::http::StatusCode::FORBIDDEN, "Only team leader can update member roles");
|
||||
}
|
||||
}
|
||||
|
||||
let user_thing = imphnen_utils::make_thing_from_enum(imphnen_libs::ResourceEnum::Users, &user_id);
|
||||
match repo.query_update_team_member_role(&thing_id, &user_thing, &payload.role).await {
|
||||
Ok(_) => crate::success_response(crate::ResponseSuccessDto {
|
||||
data: format!("Member role updated to: {}", payload.role)
|
||||
}),
|
||||
Err(e) => crate::common_response(axum::http::StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(
|
||||
@@ -410,6 +499,75 @@ pub async fn get_my_team(
|
||||
authenticated(headers, Extension(state), |claims, state| TeamsService::get_my_team(&state, claims)).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/teams/{id}/invitations",
|
||||
params(
|
||||
("id" = String, Path, description = "Team ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[AUTH] Get team invitations", body = ResponseSuccessDto<Vec<TeamInvitationListDto>>),
|
||||
(status = 401, description = "[AUTH] Unauthorized"),
|
||||
(status = 403, description = "[AUTH] Only team leader can view invitations"),
|
||||
(status = 404, description = "[AUTH] Team not found")
|
||||
),
|
||||
tag = "Teams"
|
||||
)]
|
||||
pub async fn get_team_invitations(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(team_id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
authenticated(headers, Extension(state), move |claims, state| TeamsService::get_team_invitations(&state, claims, team_id)).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/teams/invitations/{token}",
|
||||
params(
|
||||
("token" = String, Path, description = "Invitation token")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[AUTH] Invitation cancelled", body = ResponseSuccessDto<String>),
|
||||
(status = 401, description = "[AUTH] Unauthorized"),
|
||||
(status = 403, description = "[AUTH] Only team leader can cancel invitations"),
|
||||
(status = 404, description = "[AUTH] Invitation not found")
|
||||
),
|
||||
tag = "Teams"
|
||||
)]
|
||||
pub async fn delete_invitation(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(token): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
authenticated(headers, Extension(state), move |claims, state| TeamsService::cancel_invitation(&state, claims, token)).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/teams/me/invitations",
|
||||
responses(
|
||||
(status = 200, description = "[AUTH] Get my pending invitations", body = ResponseSuccessDto<Vec<MyInvitationDto>>),
|
||||
(status = 401, description = "[AUTH] Unauthorized")
|
||||
),
|
||||
tag = "Teams"
|
||||
)]
|
||||
pub async fn get_my_invitations(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
authenticated(headers, Extension(state), |claims, state| TeamsService::get_my_invitations(&state, claims)).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
security(
|
||||
@@ -504,7 +662,11 @@ pub fn teams_router() -> Router {
|
||||
.route("/{id}/members", axum::routing::get(get_team_members))
|
||||
.route("/{id}/members", axum::routing::post(post_add_team_member))
|
||||
.route("/{id}/members/{user_id}", axum::routing::delete(delete_remove_team_member))
|
||||
.route("/{id}/members/{user_id}/role", axum::routing::put(put_update_member_role))
|
||||
.route("/{id}/invitations", axum::routing::get(get_team_invitations))
|
||||
.route("/invitations/{token}", axum::routing::delete(delete_invitation))
|
||||
.route("/{id}/leave", axum::routing::post(post_leave_team))
|
||||
.route("/leave-me", axum::routing::post(post_leave_current_team))
|
||||
.route("/me", axum::routing::get(get_my_team))
|
||||
.route("/me/invitations", axum::routing::get(get_my_invitations))
|
||||
}
|
||||
|
||||
@@ -479,4 +479,51 @@ impl TeamsDetailQueryDto {
|
||||
}
|
||||
}
|
||||
|
||||
// Additional DTOs for Team Member Management
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct AddTeamMemberRequestDto {
|
||||
#[validate(length(min = 1, message = "User ID is required"))]
|
||||
pub user_id: String,
|
||||
|
||||
#[validate(length(max = 50, message = "Role cannot exceed 50 characters"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub role: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateMemberRoleRequestDto {
|
||||
#[validate(length(min = 1, max = 50, message = "Role must be between 1 and 50 characters"))]
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TeamInvitationListDto {
|
||||
pub id: String,
|
||||
pub team_id: String,
|
||||
pub team_name: String,
|
||||
pub email: String,
|
||||
pub inviter_id: String,
|
||||
pub inviter_name: String,
|
||||
pub status: String,
|
||||
pub invite_code: String,
|
||||
pub expires_at: String,
|
||||
pub invited_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MyInvitationDto {
|
||||
pub id: String,
|
||||
pub team_id: String,
|
||||
pub team_name: String,
|
||||
pub team_description: Option<String>,
|
||||
pub team_avatar: Option<String>,
|
||||
pub inviter_name: String,
|
||||
pub invite_code: String,
|
||||
pub status: String,
|
||||
pub expires_at: String,
|
||||
pub invited_at: String,
|
||||
}
|
||||
|
||||
// (previous custom validator removed; using validator::email(each = true) attribute)
|
||||
|
||||
|
||||
@@ -431,6 +431,102 @@ impl<'a> TeamsRepository<'a> {
|
||||
println!("Query 'query_remove_team_member' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok("Success remove team member".into())
|
||||
Ok("Success remove team member".into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_update_team_member_role(&self, team_id: &Thing, user_id: &Thing, role: &str) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let conditions = build_multi_thing_condition(&[("team_id", team_id), ("user_id", user_id)]);
|
||||
let sql = format!(
|
||||
"UPDATE {} SET role = '{}' WHERE {} AND is_active = true",
|
||||
ResourceEnum::TeamMembers,
|
||||
role,
|
||||
conditions
|
||||
);
|
||||
|
||||
execute_safe_update_query(db, sql).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_team_member_role' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok("Success update team member role".into())
|
||||
}
|
||||
|
||||
pub async fn query_team_invitations(&self, team_id: &Thing) -> Result<Vec<TeamInvitationsQueryDto>> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let team_id_clone = team_id.clone();
|
||||
|
||||
let sql = format!(
|
||||
"SELECT * FROM {} WHERE team_id = $team_id AND status = 'pending' ORDER BY invited_at DESC",
|
||||
ResourceEnum::TeamInvitations
|
||||
);
|
||||
|
||||
let mut result = db.query(&sql).bind(("team_id", team_id_clone)).await?;
|
||||
let invitations: Vec<TeamInvitationsQueryDto> = result.take(0)?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_team_invitations' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(invitations)
|
||||
}
|
||||
|
||||
pub async fn query_user_invitations(&self, email: &str) -> Result<Vec<TeamInvitationsQueryDto>> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let sql = format!(
|
||||
"SELECT * FROM {} WHERE email = '{}' AND status = 'pending' ORDER BY invited_at DESC",
|
||||
ResourceEnum::TeamInvitations,
|
||||
email
|
||||
);
|
||||
|
||||
let mut result = db.query(&sql).await?;
|
||||
let invitations: Vec<TeamInvitationsQueryDto> = result.take(0)?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_user_invitations' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(invitations)
|
||||
}
|
||||
|
||||
pub async fn query_delete_invitation(&self, token: &str) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let sql = format!(
|
||||
"UPDATE {} SET status = 'cancelled' WHERE invite_code = '{}'",
|
||||
ResourceEnum::TeamInvitations,
|
||||
token
|
||||
);
|
||||
|
||||
execute_safe_update_query(db, sql).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_invitation' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok("Invitation cancelled successfully".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ pub trait TeamsServiceTrait: Send + Sync + 'static {
|
||||
fn leave_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, team_id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn leave_current_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn get_my_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn get_team_invitations(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, team_id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn cancel_invitation(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, token: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn get_my_invitations(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn search_teams(state: &AppState, search_params: TeamsSearchQueryDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn get_admin_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn get_admin_team_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
@@ -1087,4 +1090,144 @@ impl TeamsServiceTrait for TeamsService {
|
||||
Self::get_public_team_by_id(&state, team_id).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_team_invitations(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, team_id: String) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
let repo = TeamsRepository::new(&state);
|
||||
let team_thing = make_thing_from_enum(ResourceEnum::Teams, &team_id);
|
||||
|
||||
// Check if team exists and user is leader
|
||||
let team = match repo.query_team_by_id(&team_thing).await {
|
||||
Ok(t) => t,
|
||||
Err(_) => return common_response(StatusCode::NOT_FOUND, "Team not found"),
|
||||
};
|
||||
|
||||
if team.leader_id.id.to_raw() != claims.user_id {
|
||||
return common_response(StatusCode::FORBIDDEN, "Only team leader can view invitations");
|
||||
}
|
||||
|
||||
// Get invitations
|
||||
match repo.query_team_invitations(&team_thing).await {
|
||||
Ok(invitations) => {
|
||||
use crate::{v1::teams::TeamInvitationListDto, UsersRepository};
|
||||
let users_repo = UsersRepository::new(&state);
|
||||
|
||||
let mut invitation_list = Vec::new();
|
||||
for inv in invitations {
|
||||
// Get inviter name
|
||||
let inviter_name = match users_repo.query_user_by_id(&inv.inviter_id).await {
|
||||
Ok(user) => user.fullname,
|
||||
Err(_) => "Unknown".to_string(),
|
||||
};
|
||||
|
||||
invitation_list.push(TeamInvitationListDto {
|
||||
id: inv.id.id.to_raw(),
|
||||
team_id: inv.team_id.id.to_raw(),
|
||||
team_name: team.name.clone(),
|
||||
email: inv.email,
|
||||
inviter_id: inv.inviter_id.id.to_raw(),
|
||||
inviter_name,
|
||||
status: inv.status,
|
||||
invite_code: inv.invite_code,
|
||||
expires_at: inv.expires_at,
|
||||
invited_at: inv.invited_at,
|
||||
});
|
||||
}
|
||||
|
||||
success_response(ResponseSuccessDto { data: invitation_list })
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to get team invitations: {}", e);
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to retrieve invitations")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn cancel_invitation(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, token: String) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
let repo = TeamsRepository::new(&state);
|
||||
|
||||
// Get invitation to check ownership
|
||||
let invitation = match repo.query_invitation_by_token(&token).await {
|
||||
Ok(inv) => inv,
|
||||
Err(_) => return common_response(StatusCode::NOT_FOUND, "Invitation not found"),
|
||||
};
|
||||
|
||||
// Check if user is the team leader
|
||||
let team_thing = invitation.team_id.clone();
|
||||
let team = match repo.query_team_by_id(&team_thing).await {
|
||||
Ok(t) => t,
|
||||
Err(_) => return common_response(StatusCode::NOT_FOUND, "Team not found"),
|
||||
};
|
||||
|
||||
if team.leader_id.id.to_raw() != claims.user_id {
|
||||
return common_response(StatusCode::FORBIDDEN, "Only team leader can cancel invitations");
|
||||
}
|
||||
|
||||
match repo.query_delete_invitation(&token).await {
|
||||
Ok(msg) => success_response(ResponseSuccessDto { data: msg }),
|
||||
Err(e) => {
|
||||
error!("Failed to cancel invitation: {}", e);
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to cancel invitation")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn get_my_invitations(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
use crate::{v1::teams::MyInvitationDto, UsersRepository};
|
||||
let users_repo = UsersRepository::new(&state);
|
||||
|
||||
// Get user email
|
||||
let user_thing = make_thing_from_enum(ResourceEnum::Users, &claims.user_id);
|
||||
let user = match users_repo.query_user_by_id(&user_thing).await {
|
||||
Ok(u) => u,
|
||||
Err(_) => return common_response(StatusCode::NOT_FOUND, "User not found"),
|
||||
};
|
||||
|
||||
let repo = TeamsRepository::new(&state);
|
||||
match repo.query_user_invitations(&user.email).await {
|
||||
Ok(invitations) => {
|
||||
let mut my_invitations = Vec::new();
|
||||
for inv in invitations {
|
||||
// Get team details
|
||||
let team = match repo.query_team_by_id(&inv.team_id).await {
|
||||
Ok(t) => t,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
// Get inviter name
|
||||
let inviter_name = match users_repo.query_user_by_id(&inv.inviter_id).await {
|
||||
Ok(u) => u.fullname,
|
||||
Err(_) => "Unknown".to_string(),
|
||||
};
|
||||
|
||||
my_invitations.push(MyInvitationDto {
|
||||
id: inv.id.id.to_raw(),
|
||||
team_id: inv.team_id.id.to_raw(),
|
||||
team_name: team.name,
|
||||
team_description: team.description,
|
||||
team_avatar: team.avatar,
|
||||
inviter_name,
|
||||
invite_code: inv.invite_code,
|
||||
status: inv.status,
|
||||
expires_at: inv.expires_at,
|
||||
invited_at: inv.invited_at,
|
||||
});
|
||||
}
|
||||
|
||||
success_response(ResponseSuccessDto { data: my_invitations })
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to get user invitations: {}", e);
|
||||
common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to retrieve invitations")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,10 +51,14 @@ pub enum ResourceEnum {
|
||||
HackathonTimeline,
|
||||
/// Hackathon submissions table for project submissions
|
||||
HackathonSubmissions,
|
||||
/// Hackathon registrations table for participant registrations
|
||||
HackathonRegistrations,
|
||||
/// Rate limiting table for IP-based rate limiting
|
||||
RateLimit,
|
||||
/// Audit log table for admin action tracking
|
||||
AuditLog,
|
||||
/// Sessions table for mentoring sessions
|
||||
Sessions,
|
||||
}
|
||||
|
||||
impl fmt::Display for ResourceEnum {
|
||||
@@ -80,8 +84,10 @@ impl fmt::Display for ResourceEnum {
|
||||
ResourceEnum::HackathonEvents => "app_hackathon_events",
|
||||
ResourceEnum::HackathonTimeline => "app_hackathon_timeline",
|
||||
ResourceEnum::HackathonSubmissions => "app_hackathon_submissions",
|
||||
ResourceEnum::HackathonRegistrations => "hackathon_registrations",
|
||||
ResourceEnum::RateLimit => "app_rate_limit",
|
||||
ResourceEnum::AuditLog => "app_audit_log",
|
||||
ResourceEnum::Sessions => "app_sessions",
|
||||
};
|
||||
write!(f, "{}", table_name)
|
||||
}
|
||||
@@ -122,8 +128,10 @@ impl ResourceEnum {
|
||||
ResourceEnum::HackathonEvents => "app_hackathon_events",
|
||||
ResourceEnum::HackathonTimeline => "app_hackathon_timeline",
|
||||
ResourceEnum::HackathonSubmissions => "app_hackathon_submissions",
|
||||
ResourceEnum::HackathonRegistrations => "hackathon_registrations",
|
||||
ResourceEnum::RateLimit => "app_rate_limit",
|
||||
ResourceEnum::AuditLog => "app_audit_log",
|
||||
ResourceEnum::Sessions => "app_sessions",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user