This commit is contained in:
MythEclipse
2025-10-27 19:23:29 +07:00
parent cb6eef2054
commit 1caaa8404b
25 changed files with 2936 additions and 11 deletions
Generated
+2
View File
@@ -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",
+2
View File
@@ -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
+7
View File
@@ -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,
};
+9
View File
@@ -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;
+11
View File
@@ -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),
}
}
}
+61
View File
@@ -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;
+3
View File
@@ -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, &registration_id);
let service = RegistrationsService::new(&state);
service.update_registration_status(&reg_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, &registration_id);
let service = RegistrationsService::new(&state);
service.check_in_participant(&reg_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),
}
}
}
+2
View File
@@ -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};
+5 -1
View File
@@ -43,7 +43,11 @@ pub use teams_dto::{
TeamsListQueryDto,
TeamMembersQueryDto,
TeamInvitationsQueryDto,
MemberTeamsDetailItemDto
MemberTeamsDetailItemDto,
AddTeamMemberRequestDto,
UpdateMemberRoleRequestDto,
TeamInvitationListDto,
MyInvitationDto
};
pub use teams_repository::TeamsRepository;
+169 -7
View File
@@ -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))
}
+47
View File
@@ -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)
+98 -2
View File
@@ -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())
}
}
+144 -1
View File
@@ -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")
}
}
})
}
}
+8
View File
@@ -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",
}
}