diff --git a/imphnen-gateway/src/docs.rs b/imphnen-gateway/src/docs.rs index 92e12a2..b3b56bf 100644 --- a/imphnen-gateway/src/docs.rs +++ b/imphnen-gateway/src/docs.rs @@ -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, ResponseSuccessDto, ResponseSuccessDto, ResponseSuccessDto, ResponseSuccessDto, ResponseSuccessDto, + ResponseSuccessDto, + ResponseSuccessDto, + ResponseSuccessDto, + ResponseSuccessDto, + ResponseSuccessDto, ResponseListSuccessDto>, ResponseSuccessDto, ResponseListSuccessDto>, @@ -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; diff --git a/imphnen-hackathon/src/v1/mod.rs b/imphnen-hackathon/src/v1/mod.rs index 26a6cf9..c3db634 100644 --- a/imphnen-hackathon/src/v1/mod.rs +++ b/imphnen-hackathon/src/v1/mod.rs @@ -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) diff --git a/imphnen-hackathon/src/v1/notifications/mod.rs b/imphnen-hackathon/src/v1/notifications/mod.rs new file mode 100644 index 0000000..1e94c5f --- /dev/null +++ b/imphnen-hackathon/src/v1/notifications/mod.rs @@ -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; diff --git a/imphnen-hackathon/src/v1/notifications/notification_controller.rs b/imphnen-hackathon/src/v1/notifications/notification_controller.rs new file mode 100644 index 0000000..d52e558 --- /dev/null +++ b/imphnen-hackathon/src/v1/notifications/notification_controller.rs @@ -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, Query, description = "Number of notifications per page (1-100, default: 20)"), + ("page" = Option, Query, description = "Page number (min: 1, default: 1)"), + ("is_read" = Option, Query, description = "Filter by read status"), + ("notification_type" = Option, 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, + Extension(state): Extension, +) -> Response { + 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, + Extension(state): Extension, +) -> Response { + 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, +) -> Response { + 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, + Extension(state): Extension, +) -> Response { + 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, +) -> Response { + 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)) +} diff --git a/imphnen-hackathon/src/v1/notifications/notification_dto.rs b/imphnen-hackathon/src/v1/notifications/notification_dto.rs new file mode 100644 index 0000000..2cce7db --- /dev/null +++ b/imphnen-hackathon/src/v1/notifications/notification_dto.rs @@ -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, + pub related_id: Option, + pub action_url: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct NotificationListResponseDto { + pub notifications: Vec, + 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, + pub notification_type: Option, +} + +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, +} diff --git a/imphnen-hackathon/src/v1/notifications/notification_repository.rs b/imphnen-hackathon/src/v1/notifications/notification_repository.rs new file mode 100644 index 0000000..85d654c --- /dev/null +++ b/imphnen-hackathon/src/v1/notifications/notification_repository.rs @@ -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, + notification_type: Option, + page: usize, + page_size: usize, + ) -> Result, 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 = 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, + notification_type: Option, + ) -> Result { + 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 = 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 { + let db = &self.state.surrealdb_ws; + let record_key = get_id(notification_id).map_err(|e| e.to_string())?; + let notification: Option = 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 { + let db = &self.state.surrealdb_ws; + let record_key = get_id(notification_id).map_err(|e| e.to_string())?; + let updated: Option = 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 { + 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 = 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 = 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 { + 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 = result.take("total").map_err(|e| format!("Failed to get count: {}", e))?; + Ok(count.unwrap_or(0)) + } +} diff --git a/imphnen-hackathon/src/v1/notifications/notification_schema.rs b/imphnen-hackathon/src/v1/notifications/notification_schema.rs new file mode 100644 index 0000000..a4177e6 --- /dev/null +++ b/imphnen-hackathon/src/v1/notifications/notification_schema.rs @@ -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, + pub read_at: Option>, + pub related_id: Option, // Could be hackathon_id, registration_id, team_id, etc. + pub action_url: Option, + pub metadata: Option, // For additional flexible data +} + +impl NotificationSchema { + pub fn mark_as_read(&mut self) { + self.is_read = true; + self.read_at = Some(Utc::now()); + } +} diff --git a/imphnen-hackathon/src/v1/notifications/notification_service.rs b/imphnen-hackathon/src/v1/notifications/notification_service.rs new file mode 100644 index 0000000..9b1c5e0 --- /dev/null +++ b/imphnen-hackathon/src/v1/notifications/notification_service.rs @@ -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 { + 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 = 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 { + 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 { + 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 { + 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 { + 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(), + } + } +} diff --git a/imphnen-libs/src/surrealdb/resource.rs b/imphnen-libs/src/surrealdb/resource.rs index 0330d55..9d660cd 100644 --- a/imphnen-libs/src/surrealdb/resource.rs +++ b/imphnen-libs/src/surrealdb/resource.rs @@ -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",