Refactor API endpoints for consistency and clarity

- Updated route paths for hackathon submissions, notifications, registrations, and teams to include more descriptive actions (e.g., "update", "create", "delete").
- Removed deprecated routes and adjusted corresponding test cases to reflect new endpoint structures.
- Enhanced test scripts to ensure compatibility with updated API routes and improved error handling for OTP resend functionality.
- Adjusted server startup script for better Windows compatibility and streamlined process management.
This commit is contained in:
MythEclipse
2025-10-29 14:27:07 +07:00
parent 98c46611fb
commit 97c2fce7be
19 changed files with 361 additions and 155 deletions
@@ -35,7 +35,7 @@ use imphnen_iam::v1::teams::teams_repository::TeamsRepository;
security(
("Bearer" = [])
),
path = "/v1/hackathons",
path = "/v1/hackathons/create",
request_body = HackathonCreateRequestDto,
responses(
(status = 201, description = "[ADMIN] Hackathon created successfully", body = ResponseSuccessDto<HackathonDto>),
@@ -61,7 +61,7 @@ pub async fn create_hackathon(
#[utoipa::path(
get,
path = "/v1/hackathons/{id}",
path = "/v1/hackathons/detail/{id}",
params(
("id" = String, Path, description = "Hackathon ID")
),
@@ -115,7 +115,7 @@ pub async fn list_hackathons(
security(
("Bearer" = [])
),
path = "/v1/hackathons/{id}",
path = "/v1/hackathons/update/{id}",
params(
("id" = String, Path, description = "Hackathon ID")
),
@@ -149,7 +149,7 @@ pub async fn update_hackathon(
security(
("Bearer" = [])
),
path = "/v1/hackathons/{id}",
path = "/v1/hackathons/delete/{id}",
params(
("id" = String, Path, description = "Hackathon ID")
),
@@ -236,12 +236,35 @@ pub async fn list_hackathon_events(
}
}
#[utoipa::path(
get,
path = "/v1/hackathons/events/detail/{id}",
params(
("id" = String, Path, description = "Event ID")
),
responses(
(status = 200, description = "[PUBLIC] Event retrieved successfully", body = ResponseSuccessDto<HackathonEventDto>),
(status = 404, description = "[PUBLIC] Event not found", body = ErrorDto),
(status = 500, description = "[PUBLIC] Internal server error", body = ErrorDto)
),
tag = "Hackathon Events"
)]
pub async fn get_hackathon_event(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
match HackathonService::get_hackathon_event(id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
put,
security(
("Bearer" = [])
),
path = "/v1/hackathons/events/{id}",
path = "/v1/hackathons/events/update/{id}",
params(
("id" = String, Path, description = "Event ID")
),
@@ -271,7 +294,7 @@ pub async fn update_hackathon_event(
security(
("Bearer" = [])
),
path = "/v1/hackathons/events/{id}",
path = "/v1/hackathons/events/delete/{id}",
params(
("id" = String, Path, description = "Event ID")
),
@@ -299,7 +322,7 @@ pub async fn delete_hackathon_event(
security(
("Bearer" = [])
),
path = "/v1/hackathons/{hackathon_id}/timeline",
path = "/v1/hackathons/{hackathon_id}/timeline/create",
params(
("hackathon_id" = String, Path, description = "Hackathon ID")
),
@@ -354,12 +377,35 @@ pub async fn list_hackathon_timeline(
}
}
#[utoipa::path(
get,
path = "/v1/hackathons/timeline/detail/{id}",
params(
("id" = String, Path, description = "Timeline ID")
),
responses(
(status = 200, description = "[PUBLIC] Timeline retrieved successfully", body = ResponseSuccessDto<HackathonTimelineDto>),
(status = 404, description = "[PUBLIC] Timeline not found", body = ErrorDto),
(status = 500, description = "[PUBLIC] Internal server error", body = ErrorDto)
),
tag = "Hackathon Timeline"
)]
pub async fn get_hackathon_timeline(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
match HackathonService::get_hackathon_timeline(id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
put,
security(
("Bearer" = [])
),
path = "/v1/hackathons/timeline/{id}",
path = "/v1/hackathons/timeline/update/{id}",
params(
("id" = String, Path, description = "Timeline ID")
),
@@ -389,7 +435,7 @@ pub async fn update_hackathon_timeline(
security(
("Bearer" = [])
),
path = "/v1/hackathons/timeline/{id}",
path = "/v1/hackathons/timeline/delete/{id}",
params(
("id" = String, Path, description = "Timeline ID")
),
@@ -417,7 +463,7 @@ pub async fn delete_hackathon_timeline(
security(
("Bearer" = [])
),
path = "/v1/hackathons/{hackathon_id}/teams/{team_id}/submissions",
path = "/v1/hackathons/{hackathon_id}/teams/{team_id}/submissions/create",
params(
("hackathon_id" = String, Path, description = "Hackathon ID"),
("team_id" = String, Path, description = "Team ID")
@@ -524,7 +570,7 @@ pub async fn list_hackathon_submissions(
#[utoipa::path(
get,
path = "/v1/hackathons/submissions/{id}",
path = "/v1/hackathons/submissions/detail/{id}",
params(
("id" = String, Path, description = "Submission ID")
),
@@ -550,7 +596,7 @@ pub async fn get_hackathon_submission(
security(
("Bearer" = [])
),
path = "/v1/hackathons/submissions/{id}",
path = "/v1/hackathons/submissions/update/{id}",
params(
("id" = String, Path, description = "Submission ID")
),
@@ -610,7 +656,7 @@ pub async fn submit_hackathon_submission(
security(
("Bearer" = [])
),
path = "/v1/hackathons/submissions/{id}",
path = "/v1/hackathons/submissions/delete/{id}",
params(
("id" = String, Path, description = "Submission ID")
),
@@ -687,7 +733,7 @@ pub struct UpdateStatusPayload {
security(
("Bearer" = [])
),
path = "/v1/hackathons/submissions/{id}/status",
path = "/v1/hackathons/submissions/update/{id}/status",
params(
("id" = String, Path, description = "Submission ID")
),
@@ -1201,42 +1247,42 @@ pub async fn change_hackathon_status(
pub fn hackathon_routes() -> Router {
Router::new()
// Hackathon routes
.route("/", post(create_hackathon))
.route("/complete", post(create_hackathon_complete))
.route("/{id}", put(update_hackathon))
.route("/create", post(create_hackathon))
.route("/create-complete", post(create_hackathon_complete))
.route("/detail/{id}", get(get_hackathon))
.route("/update/{id}", put(update_hackathon))
.route("/delete/{id}", delete(delete_hackathon))
.route("/{id}/status", patch(change_hackathon_status))
.route("/{id}", delete(delete_hackathon))
// Hackathon Events routes
.route("/{hackathon_id}/events", post(create_hackathon_event))
.route("/{hackathon_id}/events/create", post(create_hackathon_event))
.route("/{hackathon_id}/events", get(list_hackathon_events))
.route("/events/{id}", put(update_hackathon_event))
.route("/events/{id}", delete(delete_hackathon_event))
.route("/events/detail/{id}", get(get_hackathon_event))
.route("/events/update/{id}", put(update_hackathon_event))
.route("/events/delete/{id}", delete(delete_hackathon_event))
// Hackathon Timeline routes
.route("/{hackathon_id}/timeline", post(create_hackathon_timeline))
.route("/{hackathon_id}/timeline/create", post(create_hackathon_timeline))
.route("/{hackathon_id}/timeline", get(list_hackathon_timeline))
.route("/timeline/{id}", put(update_hackathon_timeline))
.route("/timeline/{id}", delete(delete_hackathon_timeline))
.route("/timeline/detail/{id}", get(get_hackathon_timeline))
.route("/timeline/update/{id}", put(update_hackathon_timeline))
.route("/timeline/delete/{id}", delete(delete_hackathon_timeline))
// Hackathon Submissions routes
.route("/{hackathon_id}/teams/{team_id}/submissions", post(create_hackathon_submission))
.route("/{hackathon_id}/teams/{team_id}/submissions/create", post(create_hackathon_submission))
.route("/{hackathon_id}/submissions", get(list_hackathon_submissions))
.route("/submissions/{id}", get(get_hackathon_submission))
.route("/submissions/{id}", put(update_hackathon_submission))
.route("/submissions/detail/{id}", get(get_hackathon_submission))
.route("/submissions/update/{id}", put(update_hackathon_submission))
.route("/submissions/delete/{id}", delete(delete_hackathon_submission))
.route("/submissions/{id}/submit", post(submit_hackathon_submission))
.route("/submissions/{id}", delete(delete_hackathon_submission))
// Admin-only submission status endpoint
.route("/submissions/{id}/status", put(update_submission_status))
.route("/submissions/update/{id}/status", put(update_submission_status))
// Admin sensitive data endpoint
.route("/{hackathon_id}/admin/sensitive-data", post(post_admin_manage_sensitive_data))
// alias route used by the integration tests
.route("/{hackathon_id}/admin/manage", post(post_admin_manage_sensitive_data))
// Participants routes
.route("/{id}/participants", post(register_participant))
.route("/{id}/participants/create", post(register_participant))
.route("/{id}/participants", get(list_participants))
}
@@ -332,6 +332,23 @@ impl<'a> HackathonRepository<'a> {
Ok(result)
}
#[instrument(skip(self, id), err)]
pub async fn get_hackathon_event_by_id(&self, id: String) -> Result<HackathonEventsSchema> {
let table = ResourceEnum::HackathonEvents.to_string();
let existing: Option<HackathonEventsSchema> = self.state.surrealdb_ws
.select((table, id.clone()))
.await?;
let event = existing.ok_or_else(|| anyhow!("Event not found"))?;
if event.is_deleted {
bail!("Event not found");
}
Ok(event)
}
#[instrument(skip(self, id, updates), err)]
pub async fn update_hackathon_event(&self, id: String, updates: HackathonEventUpdateRequestDto) -> Result<HackathonEventsSchema> {
let table = ResourceEnum::HackathonEvents.to_string();
@@ -465,6 +482,23 @@ impl<'a> HackathonRepository<'a> {
Ok(result)
}
#[instrument(skip(self, id), err)]
pub async fn get_hackathon_timeline_by_id(&self, id: String) -> Result<HackathonTimelineSchema> {
let table = ResourceEnum::HackathonTimeline.to_string();
let existing: Option<HackathonTimelineSchema> = self.state.surrealdb_ws
.select((table, id.clone()))
.await?;
let timeline = existing.ok_or_else(|| anyhow!("Timeline not found"))?;
if timeline.is_deleted {
bail!("Timeline not found");
}
Ok(timeline)
}
#[instrument(skip(self, id, updates), err)]
pub async fn update_hackathon_timeline(&self, id: String, updates: HackathonTimelineUpdateRequestDto) -> Result<HackathonTimelineSchema> {
let table = ResourceEnum::HackathonTimeline.to_string();
@@ -58,6 +58,10 @@ pub trait HackathonServiceTrait: Send + Sync + 'static {
payload: HackathonEventCreateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonEventDto>, ErrorDto>> + Send>>;
fn get_hackathon_event(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonEventDto>, ErrorDto>> + Send>>;
fn list_hackathon_events(
meta: MetaRequestDto,
hackathon_id: String,
@@ -79,6 +83,10 @@ pub trait HackathonServiceTrait: Send + Sync + 'static {
payload: HackathonTimelineCreateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonTimelineDto>, ErrorDto>> + Send>>;
fn get_hackathon_timeline(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonTimelineDto>, ErrorDto>> + Send>>;
fn list_hackathon_timeline(
meta: MetaRequestDto,
hackathon_id: String,
@@ -503,6 +511,40 @@ impl HackathonServiceTrait for HackathonService {
})
}
fn get_hackathon_event(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonEventDto>, ErrorDto>> + Send>> {
let state = state.to_owned();
Box::pin(async move {
let repo = HackathonRepository::new(&state);
match repo.get_hackathon_event_by_id(id).await {
Ok(event) => {
let dto = HackathonEventDto::from(event);
Ok(ResponseSuccessDto { data: dto })
}
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("not found") {
Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Event not found".to_string(),
details: None,
})
} else {
error!("Failed to get event: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to get event".to_string(),
details: None,
})
}
}
}
})
}
fn list_hackathon_events(
meta: MetaRequestDto,
hackathon_id: String,
@@ -662,6 +704,40 @@ impl HackathonServiceTrait for HackathonService {
})
}
fn get_hackathon_timeline(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonTimelineDto>, ErrorDto>> + Send>> {
let state = state.to_owned();
Box::pin(async move {
let repo = HackathonRepository::new(&state);
match repo.get_hackathon_timeline_by_id(id).await {
Ok(timeline) => {
let dto = HackathonTimelineDto::from(timeline);
Ok(ResponseSuccessDto { data: dto })
}
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("not found") {
Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Timeline not found".to_string(),
details: None,
})
} else {
error!("Failed to get timeline: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to get timeline".to_string(),
details: None,
})
}
}
}
})
}
fn list_hackathon_timeline(
meta: MetaRequestDto,
hackathon_id: String,
+1 -3
View File
@@ -16,7 +16,7 @@ pub fn hackathon_protected_routes() -> Router {
use hackathon::hackathon_controller::{update_submission_status, get_admin_hackathon_results};
Router::new()
.nest("/hackathons", hackathon_router())
.route("/hackathons/submissions/{id}/status", axum::routing::patch(update_submission_status))
.route("/hackathons/submissions/update/{id}/status", axum::routing::patch(update_submission_status))
.route("/hackathons/{hackathon_id}/admin/results", axum::routing::get(get_admin_hackathon_results))
.merge(registrations_router())
.merge(notifications_router())
@@ -26,7 +26,6 @@ pub fn hackathon_protected_routes() -> Router {
pub fn hackathon_public_routes() -> Router {
use hackathon::hackathon_controller::{
list_hackathons,
get_hackathon,
search_hackathons,
get_user_hackathon_submissions,
get_public_hackathon_results,
@@ -35,7 +34,6 @@ pub fn hackathon_public_routes() -> Router {
Router::new()
.nest("/hackathons", Router::new()
.route("/", axum::routing::get(list_hackathons))
.route("/{id}", axum::routing::get(get_hackathon))
.route("/{id}/results", axum::routing::get(get_public_hackathon_results))
.route("/search", axum::routing::post(search_hackathons))
)
@@ -48,14 +48,13 @@ pub async fn get_notifications_handler(
/// Mark a notification as read
#[utoipa::path(
put,
path = "/v1/notifications/{id}/read",
path = "/v1/notifications/update/{id}/read",
tags = ["notifications"],
params(
("id" = String, Path, description = "Notification ID"),
("id" = String, Path, description = "Notification ID")
),
responses(
(status = 200, description = "Successfully marked notification as read", body = MarkAsReadResponseDto),
(status = 400, description = "Notification already marked as read"),
(status = 200, description = "Successfully marked as read", body = MarkAsReadResponseDto),
(status = 401, description = "Unauthorized - Invalid or missing token"),
(status = 403, description = "Forbidden - Not the notification owner"),
(status = 404, description = "Notification not found"),
@@ -107,7 +106,7 @@ pub async fn mark_all_as_read_handler(
/// Delete a notification
#[utoipa::path(
delete,
path = "/v1/notifications/{id}",
path = "/v1/notifications/delete/{id}",
tags = ["notifications"],
params(
("id" = String, Path, description = "Notification ID"),
@@ -165,8 +164,8 @@ pub async fn get_unread_count_handler(
pub fn notifications_router() -> Router {
Router::new()
.route("/notifications", get(get_notifications_handler))
.route("/notifications/{id}/read", put(mark_as_read_handler))
.route("/notifications/update/{id}/read", put(mark_as_read_handler))
.route("/notifications/read-all", put(mark_all_as_read_handler))
.route("/notifications/{id}", delete(delete_notification_handler))
.route("/notifications/delete/{id}", delete(delete_notification_handler))
.route("/notifications/unread/count", get(get_unread_count_handler))
}
@@ -18,11 +18,11 @@ use super::{
};
// ============================================
// POST /v1/hackathons/{id}/register
// POST /v1/hackathons/{id}/registrations/create
// ============================================
#[utoipa::path(
post,
path = "/v1/hackathons/{id}/register",
path = "/v1/hackathons/{id}/registrations/create",
tag = "registrations",
summary = "Register for a hackathon",
description = "Submit a registration for a hackathon. User must be authenticated.",
@@ -137,14 +137,14 @@ pub async fn get_my_hackathons(
}
// ============================================
// PUT /v1/hackathons/{hackathon_id}/registrations/{registration_id}/status
// PUT /v1/hackathons/{hackathon_id}/registrations/update/{registration_id}/status
// ============================================
#[utoipa::path(
put,
path = "/v1/hackathons/{hackathon_id}/registrations/{registration_id}/status",
path = "/v1/hackathons/{hackathon_id}/registrations/update/{registration_id}/status",
tag = "registrations",
summary = "Update registration status",
description = "Approve, reject, or update the status of a registration. Requires admin/organizer permissions.",
description = "Update the status of a hackathon registration (admin/organizer only).",
params(
("hackathon_id" = String, Path, description = "Hackathon ID"),
("registration_id" = String, Path, description = "Registration ID")
@@ -268,7 +268,7 @@ pub async fn get_registration_stats(
pub fn registrations_router() -> Router {
Router::new()
.route(
"/hackathons/{id}/register",
"/hackathons/{id}/registrations/create",
post(post_register_hackathon),
)
.route(
@@ -280,7 +280,7 @@ pub fn registrations_router() -> Router {
get(get_registration_stats),
)
.route(
"/hackathons/{hackathon_id}/registrations/{registration_id}/status",
"/hackathons/{hackathon_id}/registrations/update/{registration_id}/status",
put(put_update_registration_status),
)
.route(