feat: Implement hackathon registration module with controller, DTOs, repository, schema, and service
- Added registration_controller.rs to handle registration-related routes and logic. - Created registration_dto.rs for data transfer objects related to registrations. - Implemented registration_repository.rs for database interactions concerning registrations. - Defined registration_schema.rs to represent the registration data structure. - Developed registration_service.rs to encapsulate business logic for registrations. - Established routes for registering, listing, updating, and checking in participants for hackathons. - Added validation for registration requests and status updates. - Included statistics retrieval for hackathon registrations.
This commit is contained in:
@@ -42,6 +42,13 @@ use imphnen_hackathon::v1::registrations::{
|
||||
CheckInResponseDto, RegistrationStatsDto, UserHackathonsResponseDto, UserHackathonDto,
|
||||
RegistrationStatus, ParticipantRole,
|
||||
};
|
||||
use imphnen_hackathon::v1::notifications::{
|
||||
notification_controller,
|
||||
notification_dto::{
|
||||
NotificationDto, NotificationListResponseDto, MarkAsReadResponseDto,
|
||||
MarkAllAsReadResponseDto, DeleteNotificationResponseDto, UnreadCountResponseDto,
|
||||
},
|
||||
};
|
||||
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};
|
||||
@@ -157,6 +164,11 @@ use utoipa::{
|
||||
registration_controller::put_update_registration_status,
|
||||
registration_controller::post_check_in_participant,
|
||||
registration_controller::get_registration_stats,
|
||||
notification_controller::get_notifications_handler,
|
||||
notification_controller::mark_as_read_handler,
|
||||
notification_controller::mark_all_as_read_handler,
|
||||
notification_controller::delete_notification_handler,
|
||||
notification_controller::get_unread_count_handler,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
@@ -267,12 +279,23 @@ use utoipa::{
|
||||
UserHackathonDto,
|
||||
RegistrationStatus,
|
||||
ParticipantRole,
|
||||
NotificationDto,
|
||||
NotificationListResponseDto,
|
||||
MarkAsReadResponseDto,
|
||||
MarkAllAsReadResponseDto,
|
||||
DeleteNotificationResponseDto,
|
||||
UnreadCountResponseDto,
|
||||
ResponseSuccessDto<RegistrationResponseDto>,
|
||||
ResponseSuccessDto<RegistrationListResponseDto>,
|
||||
ResponseSuccessDto<UpdateRegistrationStatusResponseDto>,
|
||||
ResponseSuccessDto<CheckInResponseDto>,
|
||||
ResponseSuccessDto<RegistrationStatsDto>,
|
||||
ResponseSuccessDto<UserHackathonsResponseDto>,
|
||||
ResponseSuccessDto<NotificationListResponseDto>,
|
||||
ResponseSuccessDto<MarkAsReadResponseDto>,
|
||||
ResponseSuccessDto<MarkAllAsReadResponseDto>,
|
||||
ResponseSuccessDto<DeleteNotificationResponseDto>,
|
||||
ResponseSuccessDto<UnreadCountResponseDto>,
|
||||
ResponseListSuccessDto<Vec<HackathonDto>>,
|
||||
ResponseSuccessDto<HackathonDto>,
|
||||
ResponseListSuccessDto<Vec<HackathonEventDto>>,
|
||||
@@ -313,6 +336,8 @@ use utoipa::{
|
||||
(name = "Hackathon Timeline", description = "Hackathon Timeline Management Endpoints"),
|
||||
(name = "Hackathon Submissions", description = "Hackathon Submission Management Endpoints"),
|
||||
(name = "registrations", description = "Hackathon Registration Management API"),
|
||||
(name = "notifications", description = "User Notifications Management API"),
|
||||
(name = "Teams", description = "Team Management Endpoints"),
|
||||
)
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use axum::Router;
|
||||
|
||||
pub mod hackathon;
|
||||
pub mod notifications;
|
||||
pub mod registrations;
|
||||
|
||||
// Export the router function from hackathon module
|
||||
pub use hackathon::hackathon_router;
|
||||
pub use notifications::notifications_router;
|
||||
pub use registrations::registrations_router;
|
||||
|
||||
// Main route constructor
|
||||
@@ -17,6 +19,7 @@ pub fn hackathon_protected_routes() -> 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())
|
||||
.merge(notifications_router())
|
||||
}
|
||||
|
||||
// Public routes for hackathons (only listing and retrieving)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod notification_controller;
|
||||
pub mod notification_dto;
|
||||
pub mod notification_repository;
|
||||
pub mod notification_schema;
|
||||
pub mod notification_service;
|
||||
|
||||
pub use notification_controller::notifications_router;
|
||||
@@ -0,0 +1,175 @@
|
||||
use super::notification_dto::{
|
||||
DeleteNotificationResponseDto, MarkAllAsReadResponseDto, MarkAsReadResponseDto,
|
||||
NotificationDto, NotificationListQueryDto, NotificationListResponseDto,
|
||||
UnreadCountResponseDto,
|
||||
};
|
||||
use super::notification_service::Service;
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
http::{HeaderMap, Response, StatusCode},
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, put},
|
||||
Router, body::Body,
|
||||
};
|
||||
use imphnen_entities::common_dto::ResponseSuccessDto;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::{extract_email::extract_email, response_format::common_response};
|
||||
|
||||
/// Get user's notifications with optional filtering
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/notifications",
|
||||
tags = ["notifications"],
|
||||
params(
|
||||
("page_size" = Option<usize>, Query, description = "Number of notifications per page (1-100, default: 20)"),
|
||||
("page" = Option<usize>, Query, description = "Page number (min: 1, default: 1)"),
|
||||
("is_read" = Option<bool>, Query, description = "Filter by read status"),
|
||||
("notification_type" = Option<String>, Query, description = "Filter by notification type"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Successfully retrieved notifications", body = NotificationListResponseDto),
|
||||
(status = 401, description = "Unauthorized - Invalid or missing token"),
|
||||
),
|
||||
security(
|
||||
("bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_notifications_handler(
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<NotificationListQueryDto>,
|
||||
Extension(state): Extension<AppState>,
|
||||
) -> Response<Body> {
|
||||
match extract_email(&headers) {
|
||||
Some(email) => {
|
||||
let service = Service::new(&state);
|
||||
service.get_notifications(&email, query).await
|
||||
}
|
||||
None => common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a notification as read
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/notifications/{id}/read",
|
||||
tags = ["notifications"],
|
||||
params(
|
||||
("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 = 401, description = "Unauthorized - Invalid or missing token"),
|
||||
(status = 403, description = "Forbidden - Not the notification owner"),
|
||||
(status = 404, description = "Notification not found"),
|
||||
),
|
||||
security(
|
||||
("bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn mark_as_read_handler(
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Extension(state): Extension<AppState>,
|
||||
) -> Response<Body> {
|
||||
match extract_email(&headers) {
|
||||
Some(email) => {
|
||||
let service = Service::new(&state);
|
||||
service.mark_as_read(&email, &id).await
|
||||
}
|
||||
None => common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark all notifications as read
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/notifications/read-all",
|
||||
tags = ["notifications"],
|
||||
responses(
|
||||
(status = 200, description = "Successfully marked all notifications as read", body = MarkAllAsReadResponseDto),
|
||||
(status = 401, description = "Unauthorized - Invalid or missing token"),
|
||||
),
|
||||
security(
|
||||
("bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn mark_all_as_read_handler(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
) -> Response<Body> {
|
||||
match extract_email(&headers) {
|
||||
Some(email) => {
|
||||
let service = Service::new(&state);
|
||||
service.mark_all_as_read(&email).await
|
||||
}
|
||||
None => common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a notification
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/notifications/{id}",
|
||||
tags = ["notifications"],
|
||||
params(
|
||||
("id" = String, Path, description = "Notification ID"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Successfully deleted notification", body = DeleteNotificationResponseDto),
|
||||
(status = 401, description = "Unauthorized - Invalid or missing token"),
|
||||
(status = 403, description = "Forbidden - Not the notification owner"),
|
||||
(status = 404, description = "Notification not found"),
|
||||
),
|
||||
security(
|
||||
("bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn delete_notification_handler(
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Extension(state): Extension<AppState>,
|
||||
) -> Response<Body> {
|
||||
match extract_email(&headers) {
|
||||
Some(email) => {
|
||||
let service = Service::new(&state);
|
||||
service.delete_notification(&email, &id).await
|
||||
}
|
||||
None => common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get unread notifications count
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/notifications/unread/count",
|
||||
tags = ["notifications"],
|
||||
responses(
|
||||
(status = 200, description = "Successfully retrieved unread count", body = UnreadCountResponseDto),
|
||||
(status = 401, description = "Unauthorized - Invalid or missing token"),
|
||||
),
|
||||
security(
|
||||
("bearer" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn get_unread_count_handler(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
) -> Response<Body> {
|
||||
match extract_email(&headers) {
|
||||
Some(email) => {
|
||||
let service = Service::new(&state);
|
||||
service.get_unread_count(&email).await
|
||||
}
|
||||
None => common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notifications_router() -> Router {
|
||||
Router::new()
|
||||
.route("/notifications", get(get_notifications_handler))
|
||||
.route("/notifications/: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/unread/count", get(get_unread_count_handler))
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct NotificationDto {
|
||||
pub id: String,
|
||||
pub notification_type: String,
|
||||
pub title: String,
|
||||
pub message: String,
|
||||
pub is_read: bool,
|
||||
pub created_at: String,
|
||||
pub read_at: Option<String>,
|
||||
pub related_id: Option<String>,
|
||||
pub action_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct NotificationListResponseDto {
|
||||
pub notifications: Vec<NotificationDto>,
|
||||
pub total: usize,
|
||||
pub unread_count: usize,
|
||||
pub page: usize,
|
||||
pub page_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct NotificationListQueryDto {
|
||||
#[validate(range(min = 1, max = 100))]
|
||||
#[serde(default = "default_page_size")]
|
||||
pub page_size: usize,
|
||||
|
||||
#[validate(range(min = 1))]
|
||||
#[serde(default = "default_page")]
|
||||
pub page: usize,
|
||||
|
||||
pub is_read: Option<bool>,
|
||||
pub notification_type: Option<String>,
|
||||
}
|
||||
|
||||
fn default_page_size() -> usize {
|
||||
20
|
||||
}
|
||||
|
||||
fn default_page() -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MarkAsReadResponseDto {
|
||||
pub id: String,
|
||||
pub is_read: bool,
|
||||
pub read_at: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MarkAllAsReadResponseDto {
|
||||
pub updated_count: usize,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct DeleteNotificationResponseDto {
|
||||
pub id: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UnreadCountResponseDto {
|
||||
pub unread_count: usize,
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
use crate::v1::notifications::notification_schema::NotificationSchema;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::{get_id, make_thing};
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
pub struct Repository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> Repository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
pub async fn query_user_notifications(
|
||||
&self,
|
||||
user_id: &Thing,
|
||||
is_read: Option<bool>,
|
||||
notification_type: Option<String>,
|
||||
page: usize,
|
||||
page_size: usize,
|
||||
) -> Result<Vec<NotificationSchema>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let offset = (page - 1) * page_size;
|
||||
|
||||
let mut query = "SELECT * FROM notifications WHERE user_id = $user_id ".to_string();
|
||||
|
||||
if let Some(is_read_val) = is_read {
|
||||
query.push_str(&format!(" AND is_read = {} ", is_read_val));
|
||||
}
|
||||
|
||||
if let Some(ref notif_type) = notification_type {
|
||||
query.push_str(&format!(" AND notification_type = '{}' ", notif_type));
|
||||
}
|
||||
|
||||
query.push_str(&format!(
|
||||
" ORDER BY created_at DESC LIMIT {} START {} ",
|
||||
page_size, offset
|
||||
));
|
||||
|
||||
let user_id_clone = user_id.clone();
|
||||
let mut result = db
|
||||
.query(&query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.await
|
||||
.map_err(|e| format!("Query failed: {}", e))?;
|
||||
let notifications: Vec<NotificationSchema> = result.take(0).map_err(|e| format!("Failed to parse results: {}", e))?;
|
||||
Ok(notifications)
|
||||
}
|
||||
|
||||
pub async fn count_user_notifications(
|
||||
&self,
|
||||
user_id: &Thing,
|
||||
is_read: Option<bool>,
|
||||
notification_type: Option<String>,
|
||||
) -> Result<usize, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let mut query = "SELECT count() as total FROM notifications WHERE user_id = $user_id ".to_string();
|
||||
|
||||
if let Some(is_read_val) = is_read {
|
||||
query.push_str(&format!(" AND is_read = {} ", is_read_val));
|
||||
}
|
||||
|
||||
if let Some(ref notif_type) = notification_type {
|
||||
query.push_str(&format!(" AND notification_type = '{}' ", notif_type));
|
||||
}
|
||||
|
||||
query.push_str(" GROUP ALL ");
|
||||
|
||||
let user_id_clone = user_id.clone();
|
||||
let mut result = db
|
||||
.query(&query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.await
|
||||
.map_err(|e| format!("Query failed: {}", e))?;
|
||||
let count: Option<usize> = result.take("total").map_err(|e| format!("Failed to get count: {}", e))?;
|
||||
Ok(count.unwrap_or(0))
|
||||
}
|
||||
|
||||
pub async fn query_notification_by_id(
|
||||
&self,
|
||||
notification_id: &Thing,
|
||||
) -> Result<NotificationSchema, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(notification_id).map_err(|e| e.to_string())?;
|
||||
let notification: Option<NotificationSchema> = db
|
||||
.select(record_key)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch notification: {}", e))?;
|
||||
notification.ok_or("Notification not found".to_string())
|
||||
}
|
||||
|
||||
pub async fn update_notification(
|
||||
&self,
|
||||
notification_id: &Thing,
|
||||
notification: NotificationSchema,
|
||||
) -> Result<NotificationSchema, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(notification_id).map_err(|e| e.to_string())?;
|
||||
let updated: Option<NotificationSchema> = db
|
||||
.update(record_key)
|
||||
.content(notification)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to update notification: {}", e))?;
|
||||
updated.ok_or("Failed to update notification".to_string())
|
||||
}
|
||||
|
||||
pub async fn mark_all_as_read(&self, user_id: &Thing) -> Result<usize, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let query = "UPDATE notifications SET is_read = true, read_at = time::now() WHERE user_id = $user_id AND is_read = false";
|
||||
|
||||
let user_id_clone = user_id.clone();
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.await
|
||||
.map_err(|e| format!("Query failed: {}", e))?;
|
||||
let updated: Vec<NotificationSchema> = result.take(0).map_err(|e| format!("Failed to parse results: {}", e))?;
|
||||
Ok(updated.len())
|
||||
}
|
||||
|
||||
pub async fn delete_notification(&self, notification_id: &Thing) -> Result<(), String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(notification_id).map_err(|e| e.to_string())?;
|
||||
let _: Option<NotificationSchema> = db
|
||||
.delete(record_key)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete notification: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn count_unread_notifications(&self, user_id: &Thing) -> Result<usize, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let query = "SELECT count() as total FROM notifications WHERE user_id = $user_id AND is_read = false GROUP ALL";
|
||||
|
||||
let user_id_clone = user_id.clone();
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.await
|
||||
.map_err(|e| format!("Query failed: {}", e))?;
|
||||
let count: Option<usize> = result.take("total").map_err(|e| format!("Failed to get count: {}", e))?;
|
||||
Ok(count.unwrap_or(0))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum NotificationType {
|
||||
#[serde(rename = "registration_approved")]
|
||||
RegistrationApproved,
|
||||
#[serde(rename = "registration_rejected")]
|
||||
RegistrationRejected,
|
||||
#[serde(rename = "registration_waitlisted")]
|
||||
RegistrationWaitlisted,
|
||||
#[serde(rename = "hackathon_reminder")]
|
||||
HackathonReminder,
|
||||
#[serde(rename = "team_invite")]
|
||||
TeamInvite,
|
||||
#[serde(rename = "team_update")]
|
||||
TeamUpdate,
|
||||
#[serde(rename = "hackathon_update")]
|
||||
HackathonUpdate,
|
||||
#[serde(rename = "check_in_reminder")]
|
||||
CheckInReminder,
|
||||
#[serde(rename = "announcement")]
|
||||
Announcement,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NotificationSchema {
|
||||
pub id: Thing,
|
||||
pub user_id: Thing,
|
||||
pub notification_type: NotificationType,
|
||||
pub title: String,
|
||||
pub message: String,
|
||||
pub is_read: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub read_at: Option<DateTime<Utc>>,
|
||||
pub related_id: Option<Thing>, // Could be hackathon_id, registration_id, team_id, etc.
|
||||
pub action_url: Option<String>,
|
||||
pub metadata: Option<serde_json::Value>, // For additional flexible data
|
||||
}
|
||||
|
||||
impl NotificationSchema {
|
||||
pub fn mark_as_read(&mut self) {
|
||||
self.is_read = true;
|
||||
self.read_at = Some(Utc::now());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
use super::notification_dto::{
|
||||
DeleteNotificationResponseDto, MarkAllAsReadResponseDto, MarkAsReadResponseDto,
|
||||
NotificationDto, NotificationListQueryDto, NotificationListResponseDto,
|
||||
UnreadCountResponseDto,
|
||||
};
|
||||
use super::notification_repository::Repository;
|
||||
use super::notification_schema::NotificationSchema;
|
||||
use axum::http::{Response, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::body::Body;
|
||||
use imphnen_entities::common_dto::ResponseSuccessDto;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::{
|
||||
extract_id, make_thing, response_format::success_response, validator::validate_request,
|
||||
};
|
||||
|
||||
pub struct Service<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> Service<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
pub async fn get_notifications(
|
||||
&self,
|
||||
user_email: &str,
|
||||
query: NotificationListQueryDto,
|
||||
) -> Response<Body> {
|
||||
if let Err((status, message)) = validate_request(&query) {
|
||||
return (status, message).into_response();
|
||||
}
|
||||
|
||||
let user_id = make_thing("users", user_email);
|
||||
let repository = Repository::new(self.state);
|
||||
|
||||
let notifications_result = repository
|
||||
.query_user_notifications(
|
||||
&user_id,
|
||||
query.is_read,
|
||||
query.notification_type.clone(),
|
||||
query.page,
|
||||
query.page_size,
|
||||
)
|
||||
.await;
|
||||
|
||||
let notifications = match notifications_result {
|
||||
Ok(notifs) => notifs,
|
||||
Err(err) => {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, err).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let total_result = repository
|
||||
.count_user_notifications(&user_id, query.is_read, query.notification_type)
|
||||
.await;
|
||||
|
||||
let total = match total_result {
|
||||
Ok(count) => count,
|
||||
Err(err) => {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, err).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let unread_count_result = repository.count_unread_notifications(&user_id).await;
|
||||
|
||||
let unread_count = match unread_count_result {
|
||||
Ok(count) => count,
|
||||
Err(err) => {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, err).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let notification_dtos: Vec<NotificationDto> = notifications
|
||||
.into_iter()
|
||||
.map(|n| NotificationDto {
|
||||
id: extract_id(&n.id),
|
||||
notification_type: format!("{:?}", n.notification_type),
|
||||
title: n.title,
|
||||
message: n.message,
|
||||
is_read: n.is_read,
|
||||
created_at: n.created_at.to_rfc3339(),
|
||||
read_at: n.read_at.map(|dt| dt.to_rfc3339()),
|
||||
related_id: n.related_id.map(|id| extract_id(&id)),
|
||||
action_url: n.action_url,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let response = NotificationListResponseDto {
|
||||
notifications: notification_dtos,
|
||||
total,
|
||||
unread_count,
|
||||
page: query.page,
|
||||
page_size: query.page_size,
|
||||
};
|
||||
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
|
||||
pub async fn mark_as_read(
|
||||
&self,
|
||||
user_email: &str,
|
||||
notification_id: &str,
|
||||
) -> Response<Body> {
|
||||
let user_id = make_thing("users", user_email);
|
||||
let notif_id = make_thing("notifications", notification_id);
|
||||
let repository = Repository::new(self.state);
|
||||
|
||||
let notification_result = repository.query_notification_by_id(¬if_id).await;
|
||||
|
||||
let mut notification = match notification_result {
|
||||
Ok(notif) => notif,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
"Notification not found".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// Verify ownership
|
||||
if notification.user_id != user_id {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
"You don't have permission to access this notification".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if notification.is_read {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Notification is already marked as read".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
notification.mark_as_read();
|
||||
|
||||
match repository.update_notification(¬if_id, notification.clone()).await {
|
||||
Ok(updated) => {
|
||||
let response = MarkAsReadResponseDto {
|
||||
id: extract_id(&updated.id),
|
||||
is_read: updated.is_read,
|
||||
read_at: updated.read_at.unwrap().to_rfc3339(),
|
||||
message: "Notification marked as read".to_string(),
|
||||
};
|
||||
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mark_all_as_read(&self, user_email: &str) -> Response<Body> {
|
||||
let user_id = make_thing("users", user_email);
|
||||
let repository = Repository::new(self.state);
|
||||
|
||||
match repository.mark_all_as_read(&user_id).await {
|
||||
Ok(count) => {
|
||||
let response = MarkAllAsReadResponseDto {
|
||||
updated_count: count,
|
||||
message: format!("{} notification(s) marked as read", count),
|
||||
};
|
||||
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_notification(
|
||||
&self,
|
||||
user_email: &str,
|
||||
notification_id: &str,
|
||||
) -> Response<Body> {
|
||||
let user_id = make_thing("users", user_email);
|
||||
let notif_id = make_thing("notifications", notification_id);
|
||||
let repository = Repository::new(self.state);
|
||||
|
||||
let notification_result = repository.query_notification_by_id(¬if_id).await;
|
||||
|
||||
let notification = match notification_result {
|
||||
Ok(notif) => notif,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
"Notification not found".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// Verify ownership
|
||||
if notification.user_id != user_id {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
"You don't have permission to delete this notification".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
match repository.delete_notification(¬if_id).await {
|
||||
Ok(_) => {
|
||||
let response = DeleteNotificationResponseDto {
|
||||
id: notification_id.to_string(),
|
||||
message: "Notification deleted successfully".to_string(),
|
||||
};
|
||||
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_unread_count(&self, user_email: &str) -> Response<Body> {
|
||||
let user_id = make_thing("users", user_email);
|
||||
let repository = Repository::new(self.state);
|
||||
|
||||
match repository.count_unread_notifications(&user_id).await {
|
||||
Ok(count) => {
|
||||
let response = UnreadCountResponseDto {
|
||||
unread_count: count,
|
||||
};
|
||||
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,8 @@ pub enum ResourceEnum {
|
||||
HackathonSubmissions,
|
||||
/// Hackathon registrations table for participant registrations
|
||||
HackathonRegistrations,
|
||||
/// Notifications table for user notifications
|
||||
Notifications,
|
||||
/// Rate limiting table for IP-based rate limiting
|
||||
RateLimit,
|
||||
/// Audit log table for admin action tracking
|
||||
@@ -85,6 +87,7 @@ impl fmt::Display for ResourceEnum {
|
||||
ResourceEnum::HackathonTimeline => "app_hackathon_timeline",
|
||||
ResourceEnum::HackathonSubmissions => "app_hackathon_submissions",
|
||||
ResourceEnum::HackathonRegistrations => "hackathon_registrations",
|
||||
ResourceEnum::Notifications => "notifications",
|
||||
ResourceEnum::RateLimit => "app_rate_limit",
|
||||
ResourceEnum::AuditLog => "app_audit_log",
|
||||
ResourceEnum::Sessions => "app_sessions",
|
||||
@@ -129,6 +132,7 @@ impl ResourceEnum {
|
||||
ResourceEnum::HackathonTimeline => "app_hackathon_timeline",
|
||||
ResourceEnum::HackathonSubmissions => "app_hackathon_submissions",
|
||||
ResourceEnum::HackathonRegistrations => "hackathon_registrations",
|
||||
ResourceEnum::Notifications => "notifications",
|
||||
ResourceEnum::RateLimit => "app_rate_limit",
|
||||
ResourceEnum::AuditLog => "app_audit_log",
|
||||
ResourceEnum::Sessions => "app_sessions",
|
||||
|
||||
Reference in New Issue
Block a user