inapp notification fix
This commit is contained in:
parent
0f44e63692
commit
7d626d059f
14
db/data/005_issue_reporting_seed.sql
Normal file
14
db/data/005_issue_reporting_seed.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
INSERT INTO reported_issues (user_id, user_role, subject, description, issue_type, status, metadata, created_at, updated_at) VALUES
|
||||
(10, 'USER', 'Video not loading on mobile', 'When I try to play the Algebra Fundamentals introduction video on my phone, it shows a blank screen with a spinner that never stops.', 'video', 'pending', '{"course": "Algebra Fundamentals", "device": "iPhone 14", "browser": "Safari 17"}', now() - interval '14 days', now() - interval '14 days'),
|
||||
(10, 'USER', 'Payment confirmation not received', 'I subscribed to the premium plan yesterday and the money was deducted from my account, but I have not received any confirmation email or SMS.', 'payment', 'in_progress', '{"plan": "Premium", "amount": 500, "payment_method": "telebirr"}', now() - interval '10 days', now() - interval '8 days'),
|
||||
(10, 'USER', 'Cannot change profile picture', 'I am trying to upload a new profile picture but the upload button does not respond when I click it.', 'account', 'resolved', '{"browser": "Chrome 120", "file_type": "jpg", "file_size_kb": 2048}', now() - interval '20 days', now() - interval '15 days'),
|
||||
(10, 'USER', 'Add dark mode support', 'It would be great if the platform had a dark mode option. Studying at night with the bright white background is hard on the eyes.', 'feature_request', 'pending', '{"platform": "web"}', now() - interval '7 days', now() - interval '7 days'),
|
||||
(10, 'USER', 'Quiz results not saving', 'I completed the Biology 101 quiz but when I go back to check my results, it shows as incomplete.', 'bug', 'in_progress', '{"course": "Biology 101", "quiz_id": 5, "attempts": 3}', now() - interval '5 days', now() - interval '3 days'),
|
||||
(12, 'SUPPORT', 'Course content displays incorrectly on tablets', 'Multiple users have reported that course text overlaps with images on tablet devices in landscape mode.', 'content', 'pending', '{"affected_devices": ["iPad Air", "Samsung Galaxy Tab S9"], "orientation": "landscape"}', now() - interval '12 days', now() - interval '12 days'),
|
||||
(12, 'SUPPORT', 'Login fails after password reset', 'After resetting my password through the forgot password flow, the new password is not accepted for login.', 'login', 'resolved', '{"browser": "Firefox 121", "reset_method": "email"}', now() - interval '25 days', now() - interval '18 days'),
|
||||
(12, 'SUPPORT', 'Slow page load times', 'The course listing page takes over 10 seconds to load, especially when filtering by category.', 'performance', 'in_progress', '{"page": "/courses", "avg_load_time_ms": 12500, "filter": "category=Science"}', now() - interval '9 days', now() - interval '6 days'),
|
||||
(10, 'USER', 'Subscription auto-renewal not working', 'My monthly subscription expired even though I had auto-renewal enabled. I had to manually resubscribe.', 'subscription', 'rejected', '{"plan": "Monthly Basic", "expected_renewal": "2026-01-15"}', now() - interval '30 days', now() - interval '22 days'),
|
||||
(12, 'SUPPORT', 'Screen reader cannot read course navigation', 'The course sidebar navigation is not accessible with screen readers. ARIA labels are missing on several interactive elements.', 'accessibility', 'pending', '{"screen_reader": "NVDA", "browser": "Chrome 120", "affected_elements": ["sidebar nav", "progress bar", "video controls"]}', now() - interval '4 days', now() - interval '4 days'),
|
||||
(10, 'USER', 'Certificate download gives 404 error', 'After completing the English Grammar course, clicking the download certificate button returns a page not found error.', 'course', 'pending', '{"course": "English Grammar", "completion_date": "2026-01-28"}', now() - interval '2 days', now() - interval '2 days'),
|
||||
(10, 'USER', 'Cannot access course after subscription renewal', 'I renewed my subscription but I still cannot access premium courses. It says my subscription is inactive.', 'subscription', 'in_progress', '{"plan": "Premium Annual", "renewal_date": "2026-02-01"}', now() - interval '1 day', now() - interval '12 hours')
|
||||
ON CONFLICT DO NOTHING;
|
||||
26
db/data/006_notifications_seed.sql
Normal file
26
db/data/006_notifications_seed.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
INSERT INTO notifications (user_id, type, level, channel, title, message, payload, is_read, created_at) VALUES
|
||||
-- Student (user_id=10) notifications
|
||||
(10, 'course_created', 'info', 'in_app', 'New Course Available', 'A new course "Algebra Fundamentals" has been added. Check it out!', '{"course_title": "Algebra Fundamentals", "category": "Mathematics"}', false, now() - interval '30 days'),
|
||||
(10, 'course_created', 'info', 'in_app', 'New Course Available', 'A new course "English Grammar 101" has been added. Check it out!', '{"course_title": "English Grammar 101", "category": "Language"}', false, now() - interval '25 days'),
|
||||
(10, 'sub_course_created', 'info', 'in_app', 'New Content Available', 'A new sub-course "Linear Equations" has been added.', '{"sub_course_title": "Linear Equations", "course": "Algebra Fundamentals"}', false, now() - interval '24 days'),
|
||||
(10, 'video_added', 'info', 'in_app', 'New Video Available', 'A new video "Introduction to Variables" has been added.', '{"video_title": "Introduction to Variables", "sub_course": "Linear Equations"}', false, now() - interval '23 days'),
|
||||
(10, 'payment_verified', 'success', 'in_app', 'Payment Successful', 'Your payment has been verified successfully. Your subscription is now active.', '{"plan": "Premium Monthly", "amount": 500}', true, now() - interval '20 days'),
|
||||
(10, 'subscription_activated', 'success', 'in_app', 'Subscription Activated', 'Your Premium Monthly subscription is now active until March 20, 2026.', '{"plan": "Premium Monthly", "expires": "2026-03-20"}', true, now() - interval '20 days'),
|
||||
(10, 'knowledge_level_update', 'info', 'in_app', 'Knowledge Level Updated', 'Your knowledge level has been updated to: Intermediate', '{"previous_level": "Beginner", "new_level": "Intermediate"}', false, now() - interval '15 days'),
|
||||
(10, 'issue_status_updated', 'info', 'in_app', 'Issue Status Updated', 'Your issue "Video not loading on mobile" has been updated to: in_progress', '{"issue_id": 1, "subject": "Video not loading on mobile", "status": "in_progress"}', true, now() - interval '12 days'),
|
||||
(10, 'issue_status_updated', 'success', 'in_app', 'Issue Status Updated', 'Your issue "Cannot change profile picture" has been updated to: resolved', '{"issue_id": 3, "subject": "Cannot change profile picture", "status": "resolved"}', true, now() - interval '10 days'),
|
||||
(10, 'course_enrolled', 'success', 'in_app', 'Course Enrolled', 'You have been enrolled in "Biology 101".', '{"course_title": "Biology 101"}', false, now() - interval '8 days'),
|
||||
(10, 'assessment_assigned', 'info', 'in_app', 'New Assessment Available', 'A new assessment is available for "Algebra Fundamentals".', '{"course": "Algebra Fundamentals", "assessment_type": "quiz"}', false, now() - interval '5 days'),
|
||||
(10, 'announcement', 'info', 'in_app', 'Platform Maintenance', 'Scheduled maintenance on Feb 15, 2026 from 2:00 AM - 4:00 AM EAT.', '{"scheduled_at": "2026-02-15T02:00:00+03:00", "duration_hours": 2}', false, now() - interval '2 days'),
|
||||
(10, 'video_added', 'info', 'in_app', 'New Video Available', 'A new video "Solving Quadratic Equations" has been added.', '{"video_title": "Solving Quadratic Equations", "sub_course": "Quadratics"}', false, now() - interval '1 day'),
|
||||
|
||||
-- Admin (user_id=12) notifications
|
||||
(12, 'issue_created', 'info', 'in_app', 'New Issue Reported', 'A new issue "Video not loading on mobile" has been reported.', '{"issue_id": 1, "subject": "Video not loading on mobile", "reporter_id": 10}', false, now() - interval '14 days'),
|
||||
(12, 'issue_created', 'info', 'in_app', 'New Issue Reported', 'A new issue "Payment confirmation not received" has been reported.', '{"issue_id": 2, "subject": "Payment confirmation not received", "reporter_id": 10}', false, now() - interval '10 days'),
|
||||
(12, 'issue_created', 'info', 'in_app', 'New Issue Reported', 'A new issue "Quiz results not saving" has been reported.', '{"issue_id": 5, "subject": "Quiz results not saving", "reporter_id": 10}', false, now() - interval '5 days'),
|
||||
(12, 'user_deleted', 'warning', 'in_app', 'User Deleted', 'User ID 99 has been deleted.', '{"deleted_user_id": 99, "deleted_by": 12}', true, now() - interval '18 days'),
|
||||
(12, 'admin_created', 'info', 'in_app', 'New Admin Created', 'A new admin account has been created for admin@yimaru.com.', '{"admin_email": "admin@yimaru.com"}', true, now() - interval '28 days'),
|
||||
(12, 'team_member_created', 'info', 'in_app', 'New Team Member', 'A new team member has been added.', '{"member_email": "support@yimaru.com", "role": "support"}', true, now() - interval '26 days'),
|
||||
(12, 'system_alert', 'warning', 'in_app', 'High Error Rate Detected', 'The notification delivery failure rate exceeded 5% in the last hour.', '{"failure_rate": 5.2, "window": "1h"}', false, now() - interval '3 days'),
|
||||
(12, 'announcement', 'info', 'in_app', 'New Student Registrations', '15 new students registered this week.', '{"count": 15, "period": "weekly"}', false, now() - interval '1 day')
|
||||
ON CONFLICT DO NOTHING;
|
||||
19
db/migrations/000015_notification_types.down.sql
Normal file
19
db/migrations/000015_notification_types.down.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
ALTER TABLE notifications
|
||||
DROP CONSTRAINT IF EXISTS notifications_type_check;
|
||||
|
||||
ALTER TABLE notifications
|
||||
ADD CONSTRAINT notifications_type_check CHECK (
|
||||
type IN (
|
||||
'course_enrolled',
|
||||
'lesson_completed',
|
||||
'assessment_assigned',
|
||||
'assessment_submitted',
|
||||
'assessment_graded',
|
||||
'course_completed',
|
||||
'certificate_issued',
|
||||
'announcement',
|
||||
'otp_sent',
|
||||
'signup_welcome',
|
||||
'system_alert'
|
||||
)
|
||||
);
|
||||
31
db/migrations/000015_notification_types.up.sql
Normal file
31
db/migrations/000015_notification_types.up.sql
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
-- Drop the existing CHECK constraint on notifications.type and replace with an expanded one
|
||||
ALTER TABLE notifications
|
||||
DROP CONSTRAINT IF EXISTS notifications_type_check;
|
||||
|
||||
ALTER TABLE notifications
|
||||
ADD CONSTRAINT notifications_type_check CHECK (
|
||||
type IN (
|
||||
'course_enrolled',
|
||||
'lesson_completed',
|
||||
'assessment_assigned',
|
||||
'assessment_submitted',
|
||||
'assessment_graded',
|
||||
'course_completed',
|
||||
'certificate_issued',
|
||||
'announcement',
|
||||
'otp_sent',
|
||||
'signup_welcome',
|
||||
'system_alert',
|
||||
'knowledge_level_update',
|
||||
'payment_verified',
|
||||
'subscription_activated',
|
||||
'course_created',
|
||||
'sub_course_created',
|
||||
'video_added',
|
||||
'issue_status_updated',
|
||||
'issue_created',
|
||||
'admin_created',
|
||||
'team_member_created',
|
||||
'user_deleted'
|
||||
)
|
||||
);
|
||||
|
|
@ -67,6 +67,20 @@ SET is_read = TRUE,
|
|||
WHERE user_id = $1
|
||||
AND is_read = FALSE;
|
||||
|
||||
-- name: MarkNotificationAsUnread :one
|
||||
UPDATE notifications
|
||||
SET is_read = FALSE,
|
||||
read_at = NULL
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: MarkAllUserNotificationsAsUnread :exec
|
||||
UPDATE notifications
|
||||
SET is_read = FALSE,
|
||||
read_at = NULL
|
||||
WHERE user_id = $1
|
||||
AND is_read = TRUE;
|
||||
|
||||
-- name: DeleteUserNotifications :exec
|
||||
DELETE FROM notifications
|
||||
WHERE user_id = $1;
|
||||
|
|
|
|||
|
|
@ -248,6 +248,19 @@ func (q *Queries) MarkAllUserNotificationsAsRead(ctx context.Context, userID int
|
|||
return err
|
||||
}
|
||||
|
||||
const MarkAllUserNotificationsAsUnread = `-- name: MarkAllUserNotificationsAsUnread :exec
|
||||
UPDATE notifications
|
||||
SET is_read = FALSE,
|
||||
read_at = NULL
|
||||
WHERE user_id = $1
|
||||
AND is_read = TRUE
|
||||
`
|
||||
|
||||
func (q *Queries) MarkAllUserNotificationsAsUnread(ctx context.Context, userID int64) error {
|
||||
_, err := q.db.Exec(ctx, MarkAllUserNotificationsAsUnread, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
const MarkNotificationAsRead = `-- name: MarkNotificationAsRead :one
|
||||
UPDATE notifications
|
||||
SET is_read = TRUE,
|
||||
|
|
@ -274,3 +287,30 @@ func (q *Queries) MarkNotificationAsRead(ctx context.Context, id int64) (Notific
|
|||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const MarkNotificationAsUnread = `-- name: MarkNotificationAsUnread :one
|
||||
UPDATE notifications
|
||||
SET is_read = FALSE,
|
||||
read_at = NULL
|
||||
WHERE id = $1
|
||||
RETURNING id, user_id, type, level, channel, title, message, payload, is_read, created_at, read_at
|
||||
`
|
||||
|
||||
func (q *Queries) MarkNotificationAsUnread(ctx context.Context, id int64) (Notification, error) {
|
||||
row := q.db.QueryRow(ctx, MarkNotificationAsUnread, id)
|
||||
var i Notification
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Type,
|
||||
&i.Level,
|
||||
&i.Channel,
|
||||
&i.Title,
|
||||
&i.Message,
|
||||
&i.Payload,
|
||||
&i.IsRead,
|
||||
&i.CreatedAt,
|
||||
&i.ReadAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,16 @@ type DeliveryChannel string
|
|||
|
||||
const (
|
||||
NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE NotificationType = "knowledge_level_update"
|
||||
NOTIFICATION_TYPE_PAYMENT_VERIFIED NotificationType = "payment_verified"
|
||||
NOTIFICATION_TYPE_SUBSCRIPTION_ACTIVATED NotificationType = "subscription_activated"
|
||||
NOTIFICATION_TYPE_COURSE_CREATED NotificationType = "course_created"
|
||||
NOTIFICATION_TYPE_SUB_COURSE_CREATED NotificationType = "sub_course_created"
|
||||
NOTIFICATION_TYPE_VIDEO_ADDED NotificationType = "video_added"
|
||||
NOTIFICATION_TYPE_ISSUE_STATUS_UPDATED NotificationType = "issue_status_updated"
|
||||
NOTIFICATION_TYPE_ISSUE_CREATED NotificationType = "issue_created"
|
||||
NOTIFICATION_TYPE_ADMIN_CREATED NotificationType = "admin_created"
|
||||
NOTIFICATION_TYPE_TEAM_MEMBER_CREATED NotificationType = "team_member_created"
|
||||
NOTIFICATION_TYPE_USER_DELETED NotificationType = "user_deleted"
|
||||
|
||||
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
|
||||
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import (
|
|||
|
||||
type NotificationStore interface {
|
||||
GetUserNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, int64, error)
|
||||
// ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) // New method
|
||||
CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error)
|
||||
GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error)
|
||||
// GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error)
|
||||
|
||||
CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error)
|
||||
// UpdateNotificationStatus(ctx context.Context, id, status string, isRead bool, metadata []byte) (*domain.Notification, error)
|
||||
// ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error)
|
||||
// DeleteOldNotifications(ctx context.Context) error
|
||||
MarkNotificationAsRead(ctx context.Context, id int64) (*domain.Notification, error)
|
||||
MarkAllUserNotificationsAsRead(ctx context.Context, userID int64) error
|
||||
MarkNotificationAsUnread(ctx context.Context, id int64) (*domain.Notification, error)
|
||||
MarkAllUserNotificationsAsUnread(ctx context.Context, userID int64) error
|
||||
DeleteUserNotifications(ctx context.Context, userID int64) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,26 @@ func (r *Store) MarkAllUserNotificationsAsRead(
|
|||
return r.queries.MarkAllUserNotificationsAsRead(ctx, userID)
|
||||
}
|
||||
|
||||
func (r *Store) MarkNotificationAsUnread(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (*domain.Notification, error) {
|
||||
|
||||
dbNotif, err := r.queries.MarkNotificationAsUnread(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mapDBToDomain(&dbNotif), nil
|
||||
}
|
||||
|
||||
func (r *Store) MarkAllUserNotificationsAsUnread(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
) error {
|
||||
return r.queries.MarkAllUserNotificationsAsUnread(ctx, userID)
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Delete
|
||||
========================= */
|
||||
|
|
@ -161,7 +181,7 @@ func mapDBToDomain(db *dbgen.Notification) *domain.Notification {
|
|||
Type: domain.NotificationType(db.Type),
|
||||
Level: domain.NotificationLevel(db.Level),
|
||||
DeliveryChannel: channel,
|
||||
DeliveryStatus: "PENDING",
|
||||
DeliveryStatus: domain.DeliveryStatusPending,
|
||||
Payload: domain.NotificationPayload{
|
||||
Headline: payload.Headline,
|
||||
Message: payload.Message,
|
||||
|
|
|
|||
|
|
@ -457,14 +457,15 @@ func (s *Service) handleNotification(notification *domain.Notification) {
|
|||
notification.DeliveryStatus = domain.DeliveryStatusSent
|
||||
}
|
||||
|
||||
case domain.DeliveryChannelInApp:
|
||||
notification.DeliveryStatus = domain.DeliveryStatusSent
|
||||
|
||||
default:
|
||||
if notification.DeliveryChannel != domain.DeliveryChannelInApp {
|
||||
s.mongoLogger.Warn("[NotificationSvc.HandleNotification] Unsupported delivery channel",
|
||||
zap.String("channel", string(notification.DeliveryChannel)),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
notification.DeliveryStatus = domain.DeliveryStatusFailed
|
||||
}
|
||||
s.mongoLogger.Warn("[NotificationSvc.HandleNotification] Unsupported delivery channel",
|
||||
zap.String("channel", string(notification.DeliveryChannel)),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
notification.DeliveryStatus = domain.DeliveryStatusFailed
|
||||
}
|
||||
|
||||
// if _, err := s.store.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
|
||||
|
|
@ -697,6 +698,71 @@ func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int
|
|||
return s.store.CountUnreadNotifications(ctx, recipient_id)
|
||||
}
|
||||
|
||||
func (s *Service) MarkNotificationAsRead(ctx context.Context, id int64) (*domain.Notification, error) {
|
||||
notification, err := s.store.MarkNotificationAsRead(ctx, id)
|
||||
if err != nil {
|
||||
s.mongoLogger.Error("[NotificationSvc.MarkNotificationAsRead] Failed to mark notification as read",
|
||||
zap.Int64("notificationID", id),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
return notification, nil
|
||||
}
|
||||
|
||||
func (s *Service) MarkAllUserNotificationsAsRead(ctx context.Context, userID int64) error {
|
||||
err := s.store.MarkAllUserNotificationsAsRead(ctx, userID)
|
||||
if err != nil {
|
||||
s.mongoLogger.Error("[NotificationSvc.MarkAllUserNotificationsAsRead] Failed to mark all notifications as read",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) MarkNotificationAsUnread(ctx context.Context, id int64) (*domain.Notification, error) {
|
||||
notification, err := s.store.MarkNotificationAsUnread(ctx, id)
|
||||
if err != nil {
|
||||
s.mongoLogger.Error("[NotificationSvc.MarkNotificationAsUnread] Failed to mark notification as unread",
|
||||
zap.Int64("notificationID", id),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
return notification, nil
|
||||
}
|
||||
|
||||
func (s *Service) MarkAllUserNotificationsAsUnread(ctx context.Context, userID int64) error {
|
||||
err := s.store.MarkAllUserNotificationsAsUnread(ctx, userID)
|
||||
if err != nil {
|
||||
s.mongoLogger.Error("[NotificationSvc.MarkAllUserNotificationsAsUnread] Failed to mark all notifications as unread",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteUserNotifications(ctx context.Context, userID int64) error {
|
||||
err := s.store.DeleteUserNotifications(ctx, userID)
|
||||
if err != nil {
|
||||
s.mongoLogger.Error("[NotificationSvc.DeleteUserNotifications] Failed to delete user notifications",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// func (s *Service) DeleteOldNotifications(ctx context.Context) error {
|
||||
// return s.store.DeleteOldNotifications(ctx)
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
|
|||
meta, _ := json.Marshal(map[string]interface{}{"email": req.Email, "admin_id": newUser.ID})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionAdminCreated, domain.ResourceAdmin, &newUser.ID, "Created admin: "+req.Email, meta, &ip, &ua)
|
||||
|
||||
h.sendInAppNotification(newUser.ID, domain.NOTIFICATION_TYPE_ADMIN_CREATED, "Welcome to Yimaru", "Your admin account has been created successfully.")
|
||||
|
||||
return response.WriteJSON(c, fiber.StatusOK, "Admin created successfully", nil, nil)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,10 @@ func (h *Handler) VerifyPayment(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
if payment.Status == string(domain.PaymentStatusSuccess) {
|
||||
h.sendInAppNotification(payment.UserID, domain.NOTIFICATION_TYPE_PAYMENT_VERIFIED, "Payment Successful", "Your payment has been verified successfully. Your subscription is now active.")
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Payment status retrieved",
|
||||
Data: paymentToRes(payment),
|
||||
|
|
|
|||
|
|
@ -320,6 +320,16 @@ func (h *Handler) CreateCourse(c *fiber.Ctx) error {
|
|||
meta, _ := json.Marshal(map[string]interface{}{"title": course.Title, "category_id": course.CategoryID})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseCreated, domain.ResourceCourse, &course.ID, "Created course: "+course.Title, meta, &ip, &ua)
|
||||
|
||||
go func() {
|
||||
students, _, err := h.userSvc.GetAllUsers(context.Background(), domain.UserFilter{Role: string(domain.RoleStudent)})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, s := range students {
|
||||
h.sendInAppNotification(s.ID, domain.NOTIFICATION_TYPE_COURSE_CREATED, "New Course Available", "A new course \""+course.Title+"\" has been added. Check it out!")
|
||||
}
|
||||
}()
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Course created successfully",
|
||||
Data: courseRes{
|
||||
|
|
@ -604,6 +614,16 @@ func (h *Handler) CreateSubCourse(c *fiber.Ctx) error {
|
|||
meta, _ := json.Marshal(map[string]interface{}{"title": subCourse.Title, "course_id": subCourse.CourseID, "level": subCourse.Level})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionSubCourseCreated, domain.ResourceSubCourse, &subCourse.ID, "Created sub-course: "+subCourse.Title, meta, &ip, &ua)
|
||||
|
||||
go func() {
|
||||
students, _, err := h.userSvc.GetAllUsers(context.Background(), domain.UserFilter{Role: string(domain.RoleStudent)})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, s := range students {
|
||||
h.sendInAppNotification(s.ID, domain.NOTIFICATION_TYPE_SUB_COURSE_CREATED, "New Content Available", "A new sub-course \""+subCourse.Title+"\" has been added.")
|
||||
}
|
||||
}()
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Sub-course created successfully",
|
||||
Data: subCourseRes{
|
||||
|
|
@ -1038,6 +1058,16 @@ func (h *Handler) CreateSubCourseVideo(c *fiber.Ctx) error {
|
|||
meta, _ := json.Marshal(map[string]interface{}{"title": video.Title, "sub_course_id": video.SubCourseID})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionVideoCreated, domain.ResourceVideo, &video.ID, "Created video: "+video.Title, meta, &ip, &ua)
|
||||
|
||||
go func() {
|
||||
students, _, err := h.userSvc.GetAllUsers(context.Background(), domain.UserFilter{Role: string(domain.RoleStudent)})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, s := range students {
|
||||
h.sendInAppNotification(s.ID, domain.NOTIFICATION_TYPE_VIDEO_ADDED, "New Video Available", "A new video \""+req.Title+"\" has been added.")
|
||||
}
|
||||
}()
|
||||
|
||||
var publishDate *string
|
||||
if video.PublishDate != nil {
|
||||
pd := video.PublishDate.String()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"Yimaru-Backend/internal/config"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
||||
"Yimaru-Backend/internal/services/arifpay"
|
||||
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||
|
|
@ -96,3 +99,21 @@ func New(
|
|||
mongoLoggerSvc: mongoLoggerSvc,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) sendInAppNotification(recipientID int64, notifType domain.NotificationType, headline, message string) {
|
||||
go func() {
|
||||
notification := &domain.Notification{
|
||||
RecipientID: recipientID,
|
||||
Type: notifType,
|
||||
Level: domain.NotificationLevelInfo,
|
||||
DeliveryChannel: domain.DeliveryChannelInApp,
|
||||
DeliveryStatus: domain.DeliveryStatusPending,
|
||||
IsRead: false,
|
||||
Payload: domain.NotificationPayload{
|
||||
Headline: headline,
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
_ = h.notificationSvc.SendNotification(context.Background(), notification)
|
||||
}()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,16 @@ func (h *Handler) CreateIssue(c *fiber.Ctx) error {
|
|||
meta, _ := json.Marshal(map[string]interface{}{"subject": issue.Subject, "issue_type": string(issue.IssueType)})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &userID, &actorRole, domain.ActionIssueCreated, domain.ResourceIssue, &issue.ID, "Reported issue: "+issue.Subject, meta, &ip, &ua)
|
||||
|
||||
go func() {
|
||||
admins, _, err := h.userSvc.GetAllUsers(context.Background(), domain.UserFilter{Role: string(domain.RoleAdmin)})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, admin := range admins {
|
||||
h.sendInAppNotification(admin.ID, domain.NOTIFICATION_TYPE_ISSUE_CREATED, "New Issue Reported", "A new issue \""+issue.Subject+"\" has been reported.")
|
||||
}
|
||||
}()
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Issue reported successfully",
|
||||
Data: mapIssueToRes(issue),
|
||||
|
|
@ -352,6 +362,14 @@ func (h *Handler) UpdateIssueStatus(c *fiber.Ctx) error {
|
|||
meta, _ := json.Marshal(map[string]interface{}{"issue_id": id, "new_status": req.Status})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionIssueStatusUpdated, domain.ResourceIssue, &id, fmt.Sprintf("Updated issue %d status to %s", id, req.Status), meta, &ip, &ua)
|
||||
|
||||
go func() {
|
||||
issue, err := h.issueReportingSvc.GetIssueByID(context.Background(), id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h.sendInAppNotification(issue.UserID, domain.NOTIFICATION_TYPE_ISSUE_STATUS_UPDATED, "Issue Status Updated", fmt.Sprintf("Your issue \"%s\" has been updated to: %s", issue.Subject, req.Status))
|
||||
}()
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Issue status updated successfully",
|
||||
Success: true,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package handlers
|
|||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/web_server/ws"
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
|
@ -12,38 +13,10 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/adaptor"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/valyala/fasthttp/fasthttpadaptor"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func hijackHTTP(c *fiber.Ctx) (net.Conn, http.ResponseWriter, error) {
|
||||
var rw http.ResponseWriter
|
||||
var conn net.Conn
|
||||
|
||||
// This is a trick: fasthttpadaptor gives us the HTTP interfaces
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
hj, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
conn, _, err = hj.Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rw = w
|
||||
})
|
||||
|
||||
fasthttpadaptor.NewFastHTTPHandler(handler)(c.Context())
|
||||
|
||||
if conn == nil || rw == nil {
|
||||
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to hijack connection")
|
||||
}
|
||||
return conn, rw, nil
|
||||
}
|
||||
|
||||
func (h *Handler) ConnectSocket(c *fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(int64)
|
||||
if !ok || userID == 0 {
|
||||
|
|
@ -55,120 +28,246 @@ func (h *Handler) ConnectSocket(c *fiber.Ctx) error {
|
|||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
|
||||
}
|
||||
|
||||
// Convert *fiber.Ctx to *http.Request
|
||||
req, err := adaptor.ConvertRequest(c, false)
|
||||
if err != nil {
|
||||
h.mongoLoggerSvc.Error("Failed to convert socket request",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert request")
|
||||
}
|
||||
// Hijack the underlying net.Conn from fasthttp
|
||||
done := make(chan struct{})
|
||||
c.Context().HijackSetNoResponse(true)
|
||||
c.Context().Hijack(func(netConn net.Conn) {
|
||||
defer close(done)
|
||||
|
||||
// Create a net.Conn hijacked from the fasthttp context
|
||||
netConn, rw, err := hijackHTTP(c)
|
||||
if err != nil {
|
||||
h.mongoLoggerSvc.Error("Failed to hijack connection",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to hijack connection:"+err.Error())
|
||||
}
|
||||
|
||||
// Upgrade the connection using Gorilla's Upgrader
|
||||
conn, err := ws.Upgrader.Upgrade(rw, req, nil)
|
||||
if err != nil {
|
||||
h.mongoLoggerSvc.Error("WebSocket upgrade failed",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
netConn.Close()
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "WebSocket upgrade failed:"+err.Error())
|
||||
}
|
||||
|
||||
client := &ws.Client{
|
||||
Conn: conn,
|
||||
RecipientID: userID,
|
||||
}
|
||||
|
||||
h.notificationSvc.Hub.Register <- client
|
||||
// h.logger.Info("WebSocket connection established", "userID", userID)
|
||||
|
||||
defer func() {
|
||||
h.notificationSvc.Hub.Unregister <- client
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
_, _, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
|
||||
h.mongoLoggerSvc.Info("WebSocket closed normally",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Int("status_code", fiber.StatusBadRequest),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
} else {
|
||||
h.mongoLoggerSvc.Info("Unexpected WebSocket closure",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Int("status_code", fiber.StatusBadRequest),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
}
|
||||
break
|
||||
// Build a minimal http.Request for gorilla's upgrader
|
||||
stdReq := &http.Request{
|
||||
Method: http.MethodGet,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
c.Context().Request.Header.VisitAll(func(key, value []byte) {
|
||||
stdReq.Header.Set(string(key), string(value))
|
||||
})
|
||||
stdReq.Host = string(c.Context().Host())
|
||||
stdReq.RequestURI = string(c.Context().RequestURI())
|
||||
|
||||
// Create a hijackable response writer around the raw connection
|
||||
hjRW := &hijackResponseWriter{
|
||||
conn: netConn,
|
||||
brw: bufio.NewReadWriter(bufio.NewReader(netConn), bufio.NewWriter(netConn)),
|
||||
h: make(http.Header),
|
||||
}
|
||||
|
||||
wsConn, err := ws.Upgrader.Upgrade(hjRW, stdReq, nil)
|
||||
if err != nil {
|
||||
h.mongoLoggerSvc.Error("WebSocket upgrade failed",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
netConn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
client := &ws.Client{
|
||||
Conn: wsConn,
|
||||
RecipientID: userID,
|
||||
}
|
||||
|
||||
h.notificationSvc.Hub.Register <- client
|
||||
defer func() {
|
||||
h.notificationSvc.Hub.Unregister <- client
|
||||
wsConn.Close()
|
||||
}()
|
||||
|
||||
h.mongoLoggerSvc.Info("WebSocket connection established",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
|
||||
for {
|
||||
_, _, err := wsConn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
|
||||
h.mongoLoggerSvc.Info("WebSocket closed normally",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
} else {
|
||||
h.mongoLoggerSvc.Info("Unexpected WebSocket closure",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
<-done
|
||||
return nil
|
||||
}
|
||||
|
||||
// func (h *Handler) MarkNotificationAsRead(c *fiber.Ctx) error {
|
||||
// type Request struct {
|
||||
// NotificationIDs []string `json:"notification_ids" validate:"required"`
|
||||
// }
|
||||
// hijackResponseWriter implements http.ResponseWriter and http.Hijacker
|
||||
// so gorilla/websocket can upgrade over a raw net.Conn.
|
||||
type hijackResponseWriter struct {
|
||||
conn net.Conn
|
||||
brw *bufio.ReadWriter
|
||||
h http.Header
|
||||
}
|
||||
|
||||
// var req Request
|
||||
// if err := c.BodyParser(&req); err != nil {
|
||||
// h.mongoLoggerSvc.Info("Failed to parse request body",
|
||||
// zap.Int("status_code", fiber.StatusBadRequest),
|
||||
// zap.Error(err),
|
||||
// zap.Time("timestamp", time.Now()),
|
||||
// )
|
||||
// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
// }
|
||||
func (w *hijackResponseWriter) Header() http.Header { return w.h }
|
||||
func (w *hijackResponseWriter) WriteHeader(statusCode int) {}
|
||||
func (w *hijackResponseWriter) Write(b []byte) (int, error) { return w.conn.Write(b) }
|
||||
func (w *hijackResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
return w.conn, w.brw, nil
|
||||
}
|
||||
|
||||
// userID, ok := c.Locals("user_id").(int64)
|
||||
// if !ok || userID == 0 {
|
||||
// h.mongoLoggerSvc.Error("Invalid user ID in context",
|
||||
// zap.Int64("userID", userID),
|
||||
// zap.Int("status_code", fiber.StatusInternalServerError),
|
||||
// zap.Time("timestamp", time.Now()),
|
||||
// )
|
||||
// return fiber.NewError(fiber.StatusInternalServerError, "invalid user ID in context")
|
||||
// }
|
||||
func (h *Handler) MarkNotificationAsRead(c *fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid notification ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// fmt.Printf("Notification IDs: %v \n", req.NotificationIDs)
|
||||
// if err := h.notificationSvc.MarkAsRead(context.Background(), req.NotificationIDs, userID); err != nil {
|
||||
// h.mongoLoggerSvc.Error("Failed to mark notifications as read",
|
||||
// zap.String("notificationID", strings.Join(req.NotificationIDs, ",")),
|
||||
// zap.Int64("userID", userID),
|
||||
// zap.Int("status_code", fiber.StatusInternalServerError),
|
||||
// zap.Error(err),
|
||||
// zap.Time("timestamp", time.Now()),
|
||||
// )
|
||||
// return fiber.NewError(fiber.StatusInternalServerError, "Failed to update notification status:", err.Error())
|
||||
// }
|
||||
notification, err := h.notificationSvc.MarkNotificationAsRead(context.Background(), id)
|
||||
if err != nil {
|
||||
h.mongoLoggerSvc.Error("[NotificationHandler.MarkNotificationAsRead] Failed to mark notification as read",
|
||||
zap.Int64("notificationID", id),
|
||||
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to mark notification as read",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Notification marked as read"})
|
||||
// }
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Notification marked as read",
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
Data: notification,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) MarkAllNotificationsAsRead(c *fiber.Ctx) error {
|
||||
userID, ok := c.Locals("user_id").(int64)
|
||||
if !ok || userID == 0 {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid user identification",
|
||||
Error: "User ID not found in context",
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.notificationSvc.MarkAllUserNotificationsAsRead(context.Background(), userID); err != nil {
|
||||
h.mongoLoggerSvc.Error("[NotificationHandler.MarkAllNotificationsAsRead] Failed to mark all notifications as read",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to mark all notifications as read",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "All notifications marked as read",
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) MarkNotificationAsUnread(c *fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid notification ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
notification, err := h.notificationSvc.MarkNotificationAsUnread(context.Background(), id)
|
||||
if err != nil {
|
||||
h.mongoLoggerSvc.Error("[NotificationHandler.MarkNotificationAsUnread] Failed to mark notification as unread",
|
||||
zap.Int64("notificationID", id),
|
||||
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to mark notification as unread",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Notification marked as unread",
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
Data: notification,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) MarkAllNotificationsAsUnread(c *fiber.Ctx) error {
|
||||
userID, ok := c.Locals("user_id").(int64)
|
||||
if !ok || userID == 0 {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid user identification",
|
||||
Error: "User ID not found in context",
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.notificationSvc.MarkAllUserNotificationsAsUnread(context.Background(), userID); err != nil {
|
||||
h.mongoLoggerSvc.Error("[NotificationHandler.MarkAllNotificationsAsUnread] Failed to mark all notifications as unread",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to mark all notifications as unread",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "All notifications marked as unread",
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteUserNotifications(c *fiber.Ctx) error {
|
||||
userID, ok := c.Locals("user_id").(int64)
|
||||
if !ok || userID == 0 {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid user identification",
|
||||
Error: "User ID not found in context",
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.notificationSvc.DeleteUserNotifications(context.Background(), userID); err != nil {
|
||||
h.mongoLoggerSvc.Error("[NotificationHandler.DeleteUserNotifications] Failed to delete notifications",
|
||||
zap.Int64("userID", userID),
|
||||
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to delete notifications",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Notifications deleted successfully",
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error {
|
||||
type Request struct {
|
||||
|
|
|
|||
|
|
@ -245,6 +245,8 @@ func (h *Handler) CreateTeamMember(c *fiber.Ctx) error {
|
|||
meta, _ := json.Marshal(map[string]interface{}{"email": member.Email, "team_role": string(member.TeamRole)})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &creatorID, &actorRole, domain.ActionTeamMemberCreated, domain.ResourceTeamMember, &member.ID, "Created team member: "+member.Email, meta, &ip, &ua)
|
||||
|
||||
h.sendInAppNotification(member.ID, domain.NOTIFICATION_TYPE_TEAM_MEMBER_CREATED, "Welcome to the Team", "You have been added as a team member.")
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Team member created successfully",
|
||||
Data: toTeamMemberResponse(&member),
|
||||
|
|
|
|||
|
|
@ -197,6 +197,8 @@ func (h *Handler) UpdateUserKnowledgeLevel(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
h.sendInAppNotification(userID, domain.NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE, "Knowledge Level Updated", "Your knowledge level has been updated to: "+req.KnowledgeLevel)
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "User knowledge level updated successfully",
|
||||
})
|
||||
|
|
@ -1609,6 +1611,18 @@ func (h *Handler) DeleteUser(c *fiber.Ctx) error {
|
|||
meta, _ := json.Marshal(map[string]interface{}{"deleted_user_id": userID})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionUserDeleted, domain.ResourceUser, &userID, fmt.Sprintf("Deleted user ID: %d", userID), meta, &ip, &ua)
|
||||
|
||||
go func() {
|
||||
admins, _, err := h.userSvc.GetAllUsers(context.Background(), domain.UserFilter{Role: string(domain.RoleAdmin)})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, admin := range admins {
|
||||
if admin.ID != actorID {
|
||||
h.sendInAppNotification(admin.ID, domain.NOTIFICATION_TYPE_USER_DELETED, "User Deleted", fmt.Sprintf("User ID %d has been deleted.", userID))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return response.WriteJSON(c, fiber.StatusOK, "User deleted successfully", nil, nil)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -330,7 +330,11 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Get("/ws/connect", a.WebsocketAuthMiddleware, h.ConnectSocket)
|
||||
groupV1.Get("/notifications", a.authMiddleware, h.GetUserNotification)
|
||||
groupV1.Get("/notifications/all", a.authMiddleware, h.GetAllNotifications)
|
||||
// groupV1.Post("/notifications/mark-as-read", a.authMiddleware, h.MarkNotificationAsRead)
|
||||
groupV1.Patch("/notifications/:id/read", a.authMiddleware, h.MarkNotificationAsRead)
|
||||
groupV1.Post("/notifications/mark-all-read", a.authMiddleware, h.MarkAllNotificationsAsRead)
|
||||
groupV1.Patch("/notifications/:id/unread", a.authMiddleware, h.MarkNotificationAsUnread)
|
||||
groupV1.Post("/notifications/mark-all-unread", a.authMiddleware, h.MarkAllNotificationsAsUnread)
|
||||
groupV1.Delete("/notifications", a.authMiddleware, h.DeleteUserNotifications)
|
||||
groupV1.Get("/notifications/unread", a.authMiddleware, h.CountUnreadNotifications)
|
||||
groupV1.Post("/notifications/create", a.authMiddleware, h.CreateAndSendNotification)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user