use chrono::{DateTime, Utc}; use lazy_static::lazy_static; use regex::Regex; use serde::{Deserialize, Serialize}; use utoipa::{ToSchema, schema}; use validator::{Validate, ValidationError}; // Custom validators pub fn validate_url_format(url: &str) -> Result<(), ValidationError> { lazy_static! { static ref URL_REGEX: Regex = Regex::new(r"^https?://[^\s$.?#].[^\s]*$").unwrap(); } if URL_REGEX.is_match(url) { Ok(()) } else { Err(ValidationError::new("invalid_url")) } } pub fn validate_github_url(url: &str) -> Result<(), ValidationError> { lazy_static! { static ref GITHUB_REGEX: Regex = Regex::new(r"^https?://github\.com/[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)?$").unwrap(); } if GITHUB_REGEX.is_match(url) { Ok(()) } else { Err(ValidationError::new("invalid_github_url")) } } pub fn validate_demo_url(url: &str) -> Result<(), ValidationError> { lazy_static! { static ref DEMO_URL_REGEX: Regex = Regex::new(r"^https?://(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+(/[^\s]*)?$").unwrap(); } if DEMO_URL_REGEX.is_match(url) { Ok(()) } else { Err(ValidationError::new("invalid_demo_url")) } } use crate::v1::hackathon::hackathon_schema::{ HackathonEventType, HackathonEventsSchema, HackathonPhase, HackathonSchema, HackathonStatus, HackathonSubmissionsSchema, HackathonTimelineSchema, SubmissionStatus, HackathonParticipantSchema, }; // Hackathon DTOs #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonCreateRequestDto { #[validate(length(min = 1, max = 100, message = "Hackathon name must be between 1 and 100 characters"))] pub name: String, #[validate(length(min = 1, max = 1000, message = "Description must be between 1 and 1000 characters"))] pub description: String, #[schema(value_type = String, format = DateTime)] pub start_date: DateTime, #[schema(value_type = String, format = DateTime)] pub end_date: DateTime, #[schema(value_type = String, format = DateTime)] pub registration_deadline: DateTime, #[validate(range(min = 1, max = 10000, message = "Max participants must be between 1 and 10000"))] pub max_participants: Option, #[validate(length(max = 200, message = "Theme cannot exceed 200 characters"))] pub theme: Option, #[validate(length(max = 2000, message = "Rules cannot exceed 2000 characters"))] pub rules: Option, pub prizes: Option>, pub previous_winners: Option>, #[validate(length(min = 1, message = "Organizers list cannot be empty"))] pub organizers: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonUpdateRequestDto { #[validate(length(min = 1, max = 100, message = "Hackathon name must be between 1 and 100 characters"))] #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, #[validate(length(min = 1, max = 1000, message = "Description must be between 1 and 1000 characters"))] #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schema(value_type = String, format = DateTime)] pub start_date: Option>, #[schema(value_type = String, format = DateTime)] pub end_date: Option>, #[schema(value_type = String, format = DateTime)] pub registration_deadline: Option>, #[validate(range(min = 1, max = 10000, message = "Max participants must be between 1 and 10000"))] #[serde(skip_serializing_if = "Option::is_none")] pub max_participants: Option, #[serde(skip_serializing_if = "Option::is_none")] pub theme: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rules: Option, #[serde(skip_serializing_if = "Option::is_none")] pub prizes: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub previous_winners: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub organizers: Option>, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct HackathonDto { pub id: String, pub name: String, pub description: String, #[schema(value_type = String, format = DateTime)] pub start_date: DateTime, #[schema(value_type = String, format = DateTime)] pub end_date: DateTime, #[schema(value_type = String, format = DateTime)] pub registration_deadline: DateTime, pub max_participants: Option, pub status: HackathonStatus, pub theme: Option, pub rules: Option, pub prizes: Option>, pub previous_winners: Option>, pub organizers: Vec, pub is_deleted: bool, pub created_at: Option, pub updated_at: Option, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct PrizeDto { #[validate(range(min = 1, message = "Position must be at least 1"))] pub position: u32, #[validate(length(min = 1, message = "Prize title cannot be empty"))] pub title: String, pub description: Option, pub value: Option, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct WinnerDto { #[validate(range(min = 1, message = "Position must be at least 1"))] pub position: u32, pub team_id: String, #[validate(length(min = 1, message = "Project name cannot be empty"))] pub project_name: String, pub team_name: Option, } // Hackathon Events DTOs #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonEventCreateRequestDto { #[validate(length(min = 1, message = "Event title cannot be empty"))] pub title: String, pub description: Option, pub event_type: HackathonEventType, #[schema(value_type = String, format = DateTime)] pub start_time: DateTime, #[schema(value_type = String, format = DateTime)] pub end_time: DateTime, pub location: Option, pub virtual_link: Option, #[validate(range(min = 1, message = "Max attendees must be at least 1"))] pub max_attendees: Option, pub is_mandatory: bool, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonEventUpdateRequestDto { #[validate(length(min = 1, message = "Event title cannot be empty"))] #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] pub event_type: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schema(value_type = String, format = DateTime)] pub start_time: Option>, #[serde(skip_serializing_if = "Option::is_none")] #[schema(value_type = String, format = DateTime)] pub end_time: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub location: Option, #[serde(skip_serializing_if = "Option::is_none")] pub virtual_link: Option, #[validate(range(min = 1, message = "Max attendees must be at least 1"))] #[serde(skip_serializing_if = "Option::is_none")] pub max_attendees: Option, #[serde(skip_serializing_if = "Option::is_none")] pub is_mandatory: Option, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct HackathonEventDto { pub id: String, pub hackathon_id: String, pub title: String, pub description: Option, pub event_type: HackathonEventType, #[schema(value_type = String, format = DateTime)] pub start_time: DateTime, #[schema(value_type = String, format = DateTime)] pub end_time: DateTime, pub location: Option, pub virtual_link: Option, pub max_attendees: Option, pub is_mandatory: bool, pub is_deleted: bool, pub created_at: Option, pub updated_at: Option, } // Hackathon Timeline DTOs #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonTimelineCreateRequestDto { pub phase: HackathonPhase, // Accept either `title` or `name` in incoming JSON (tests may send `name`). // Make it optional so missing title doesn't cause a 422; service/repo will // fallback to an empty title or a sensible default. #[serde(alias = "name")] #[serde(default)] pub title: Option, pub description: Option, #[schema(value_type = String, format = DateTime)] pub start_date: DateTime, #[schema(value_type = String, format = DateTime)] pub end_date: DateTime, #[serde(default)] pub is_active: Option, #[serde(default)] #[validate(range(min = 0, message = "Order must be non-negative"))] pub order: Option, } // Custom validator for HackathonPhase (case-insensitive) pub fn validate_hackathon_phase(phase: &str) -> Result<(), ValidationError> { let normalized = phase.to_lowercase(); match normalized.as_str() { "registration" | "ideation" | "development" | "submission" | "judging" | "awards" => Ok(()), _ => Err(ValidationError::new("invalid_hackathon_phase")), } } // Custom validator to ensure start_date is in the future pub fn validate_future_date(date: &DateTime) -> Result<(), ValidationError> { let now = Utc::now(); if date <= &now { Err(ValidationError::new("start_date_must_be_in_future")) } else { Ok(()) } } // Custom validator to ensure end_date is in the future or current pub fn validate_future_or_current_date(date: &DateTime) -> Result<(), ValidationError> { let now = Utc::now(); if date < &now { Err(ValidationError::new("end_date_must_be_in_future_or_current")) } else { Ok(()) } } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonTimelineUpdateRequestDto { #[serde(skip_serializing_if = "Option::is_none")] pub phase: Option, #[validate(length(min = 1, message = "Timeline title cannot be empty"))] #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schema(value_type = String, format = DateTime)] pub start_date: Option>, #[serde(skip_serializing_if = "Option::is_none")] #[schema(value_type = String, format = DateTime)] pub end_date: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub is_active: Option, #[validate(range(min = 0, message = "Order must be non-negative"))] #[serde(skip_serializing_if = "Option::is_none")] pub order: Option, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct HackathonTimelineDto { pub id: String, pub hackathon_id: String, pub phase: HackathonPhase, pub title: String, pub description: Option, #[schema(value_type = String, format = DateTime)] pub start_date: DateTime, #[schema(value_type = String, format = DateTime)] pub end_date: DateTime, pub is_active: bool, pub order: u32, pub is_deleted: bool, pub created_at: Option, pub updated_at: Option, } // Hackathon Submissions DTOs #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonSubmissionCreateRequestDto { #[validate(length(min = 1, message = "Project name cannot be empty"))] pub project_name: String, #[validate(length(min = 1, message = "Description cannot be empty"))] pub description: String, pub repository_url: Option, pub upload_file_url: Option, // URL to uploaded zip/pdf file pub demo_url: Option, pub slides_url: Option, pub technologies: Vec, // Social media contacts for demo (at least one required) pub contact_instagram: Option, pub contact_twitter: Option, pub contact_linkedin: Option, pub contact_facebook: Option, pub contact_youtube: Option, pub contact_tiktok: Option, pub contact_other: Option, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonSubmissionUpdateRequestDto { #[validate(length(min = 1, message = "Project name cannot be empty"))] #[serde(skip_serializing_if = "Option::is_none")] pub project_name: Option, #[validate(length(min = 1, message = "Description cannot be empty"))] #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] pub repository_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub upload_file_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub demo_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub slides_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub technologies: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub contact_instagram: Option, #[serde(skip_serializing_if = "Option::is_none")] pub contact_twitter: Option, #[serde(skip_serializing_if = "Option::is_none")] pub contact_linkedin: Option, #[serde(skip_serializing_if = "Option::is_none")] pub contact_facebook: Option, #[serde(skip_serializing_if = "Option::is_none")] pub contact_youtube: Option, #[serde(skip_serializing_if = "Option::is_none")] pub contact_tiktok: Option, #[serde(skip_serializing_if = "Option::is_none")] pub contact_other: Option, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct HackathonSubmissionDto { pub id: String, pub hackathon_id: String, pub team_id: String, pub project_name: String, pub description: String, pub repository_url: Option, pub upload_file_url: Option, pub demo_url: Option, pub slides_url: Option, pub technologies: Vec, pub contact_instagram: Option, pub contact_twitter: Option, pub contact_linkedin: Option, pub contact_facebook: Option, pub contact_youtube: Option, pub contact_tiktok: Option, pub contact_other: Option, #[serde(rename = "status")] pub submission_status: SubmissionStatus, pub judge_feedback: Option, #[schema(value_type = String, format = DateTime)] pub submitted_at: DateTime, pub is_deleted: bool, pub created_at: Option, pub updated_at: Option, } // Query DTOs #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonQueryDto { #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, #[serde(skip_serializing_if = "Option::is_none")] pub organizer_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub offset: Option, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonEventQueryDto { pub hackathon_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub event_type: Option, #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub offset: Option, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonTimelineQueryDto { pub hackathon_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub phase: Option, #[serde(skip_serializing_if = "Option::is_none")] pub is_active: Option, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonSubmissionQueryDto { pub hackathon_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub team_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub submission_status: Option, #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub offset: Option, } // Conversion implementations impl From for HackathonDto { fn from(schema: HackathonSchema) -> Self { Self { id: schema.id.id.to_raw(), name: schema.name, description: schema.description, start_date: schema.start_date, end_date: schema.end_date, registration_deadline: schema.registration_deadline, max_participants: schema.max_participants, status: schema.status, theme: schema.theme, rules: schema.rules, prizes: schema.prizes.map(|prizes| { prizes .into_iter() .map(|p| PrizeDto { position: p.position, title: p.title, description: p.description, value: p.value, }) .collect() }), previous_winners: schema.previous_winners.map(|winners| { winners .into_iter() .map(|w| WinnerDto { position: w.position, team_id: w.team_id, project_name: w.project_name, team_name: w.team_name, }) .collect() }), organizers: schema.organizers, is_deleted: schema.is_deleted, created_at: schema.created_at, updated_at: schema.updated_at, } } } impl From for HackathonEventDto { fn from(schema: HackathonEventsSchema) -> Self { Self { id: schema.id.id.to_raw(), hackathon_id: schema.hackathon_id.id.to_raw(), title: schema.title, description: schema.description, event_type: schema.event_type, start_time: schema.start_time, end_time: schema.end_time, location: schema.location, virtual_link: schema.virtual_link, max_attendees: schema.max_attendees, is_mandatory: schema.is_mandatory, is_deleted: schema.is_deleted, created_at: schema.created_at, updated_at: schema.updated_at, } } } impl From for HackathonTimelineDto { fn from(schema: HackathonTimelineSchema) -> Self { Self { id: schema.id.id.to_raw(), hackathon_id: schema.hackathon_id.id.to_raw(), phase: schema.phase, title: schema.title, description: schema.description, start_date: schema.start_date, end_date: schema.end_date, is_active: schema.is_active, order: schema.order, is_deleted: schema.is_deleted, created_at: schema.created_at, updated_at: schema.updated_at, } } } impl From for HackathonSubmissionDto { fn from(schema: HackathonSubmissionsSchema) -> Self { Self { id: schema.id.id.to_raw(), hackathon_id: schema.hackathon_id.id.to_raw(), team_id: schema.team_id.map(|t| t.id.to_raw()).unwrap_or_default(), project_name: schema.project_name.unwrap_or_default(), description: schema.description.unwrap_or_default(), repository_url: schema.repository_url, upload_file_url: schema.upload_file_url, demo_url: schema.demo_url, slides_url: schema.slides_url, technologies: schema.technologies.unwrap_or_default(), contact_instagram: schema.contact_instagram, contact_twitter: schema.contact_twitter, contact_linkedin: schema.contact_linkedin, contact_facebook: schema.contact_facebook, contact_youtube: schema.contact_youtube, contact_tiktok: schema.contact_tiktok, contact_other: schema.contact_other, submission_status: schema.submission_status.unwrap_or(super::hackathon_schema::SubmissionStatus::Draft), judge_feedback: schema.judge_feedback, submitted_at: schema.submitted_at.unwrap_or(chrono::Utc::now()), is_deleted: schema.is_deleted, created_at: schema.created_at, updated_at: schema.updated_at, } } } // Hackathon Participant DTOs #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct RegisterParticipantRequestDto { #[validate(length(min = 1, message = "user_id cannot be empty"))] pub user_id: String, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct HackathonParticipantDto { pub id: String, pub hackathon_id: String, pub user_id: String, pub is_deleted: bool, pub created_at: Option, pub updated_at: Option, } impl From for HackathonParticipantDto { fn from(schema: HackathonParticipantSchema) -> Self { Self { id: schema.id.id.to_raw(), hackathon_id: schema.hackathon_id.id.to_raw(), user_id: schema.user_id, is_deleted: schema.is_deleted, created_at: schema.created_at, updated_at: schema.updated_at, } } } // Admin Sensitive Data Management DTOs #[derive(Debug, Deserialize, Serialize, Validate, ToSchema)] pub struct AdminManageSensitiveDataRequestDto { #[validate(length(min = 1, message = "At least one user ID is required"))] pub user_ids: Vec, #[validate(length(min = 1, message = "At least one raw score is required"))] pub raw_scores: Vec, pub personal_info: bool, } #[derive(Debug, Serialize, ToSchema)] pub struct AdminSensitiveDataMemberDto { pub user_id: String, pub masked_email: String, pub masked_phone: String, pub name: String, pub role: String, } #[derive(Debug, Serialize, ToSchema)] pub struct AdminSensitiveDataDto { pub submission_id: String, pub team_id: String, pub project_name: String, pub description: String, pub technologies: Vec, pub score: Option, pub members: Vec, pub raw_scores: Option>, pub submission_date: String, } #[derive(Debug, Serialize, ToSchema)] pub struct AdminSensitiveDataResponseDto { pub data: Vec, pub message: String, } // Status Change DTOs #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonStatusChangeRequestDto { pub status: HackathonStatus, #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, #[serde(skip_serializing_if = "Option::is_none")] pub actor_id: Option, }