internal/handler/notification_handler.go
Rama main · réplica de lectura del repositorio original.
package handler
import (
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/johnzastrow/actalog/internal/service"
"github.com/johnzastrow/actalog/pkg/logger"
"github.com/johnzastrow/actalog/pkg/middleware"
)
// NotificationHandler handles notification endpoints
type NotificationHandler struct {
notificationService *service.NotificationService
logger *logger.Logger
}
// NewNotificationHandler creates a new notification handler
func NewNotificationHandler(notificationService *service.NotificationService, l *logger.Logger) *NotificationHandler {
return &NotificationHandler{
notificationService: notificationService,
logger: l,
}
}
// ListNotifications handles GET /api/notifications
// @Summary List notifications
// @Description Retrieve all notifications for the authenticated user
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param limit query int false "Max results (default 20, max 100)"
// @Param offset query int false "Skip N results (default 0)"
// @Success 200 {object} map[string]interface{} "List of notifications"
// @Failure 401 {object} ErrorResponse "Unauthorized"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /notifications [get]
func (h *NotificationHandler) ListNotifications(w http.ResponseWriter, r *http.Request) {
// Get user ID from context (set by auth middleware)
userID, ok := middleware.GetUserID(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "Unauthorized")
return
}
// Parse pagination parameters
limitStr := r.URL.Query().Get("limit")
offsetStr := r.URL.Query().Get("offset")
limit := 20 // Default limit
offset := 0 // Default offset
if limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
if limit > 100 {
limit = 100 // Max limit
}
}
}
if offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
// Get notifications
notifications, err := h.notificationService.GetNotifications(userID, limit, offset)
if err != nil {
if h.logger != nil {
h.logger.Error("action=list_notifications outcome=failure user_id=%d error=%v", userID, err)
}
respondError(w, http.StatusInternalServerError, "Failed to retrieve notifications")
return
}
if h.logger != nil {
h.logger.Info("action=list_notifications outcome=success user_id=%d count=%d", userID, len(notifications))
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"notifications": notifications,
"limit": limit,
"offset": offset,
})
}
// ListUnreadNotifications handles GET /api/notifications/unread
// @Summary List unread notifications
// @Description Retrieve only unread notifications for the authenticated user
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param limit query int false "Max results (default 20, max 100)"
// @Param offset query int false "Skip N results (default 0)"
// @Success 200 {object} map[string]interface{} "List of unread notifications"
// @Failure 401 {object} ErrorResponse "Unauthorized"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /notifications/unread [get]
func (h *NotificationHandler) ListUnreadNotifications(w http.ResponseWriter, r *http.Request) {
// Get user ID from context (set by auth middleware)
userID, ok := middleware.GetUserID(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "Unauthorized")
return
}
// Parse pagination parameters
limitStr := r.URL.Query().Get("limit")
offsetStr := r.URL.Query().Get("offset")
limit := 20 // Default limit
offset := 0 // Default offset
if limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
if limit > 100 {
limit = 100 // Max limit
}
}
}
if offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
// Get unread notifications
notifications, err := h.notificationService.GetUnreadNotifications(userID, limit, offset)
if err != nil {
if h.logger != nil {
h.logger.Error("action=list_unread_notifications outcome=failure user_id=%d error=%v", userID, err)
}
respondError(w, http.StatusInternalServerError, "Failed to retrieve unread notifications")
return
}
if h.logger != nil {
h.logger.Info("action=list_unread_notifications outcome=success user_id=%d count=%d", userID, len(notifications))
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"notifications": notifications,
"limit": limit,
"offset": offset,
})
}
// GetUnreadCount handles GET /api/notifications/count
// @Summary Get unread notification count
// @Description Get the count of unread notifications (for badge display)
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{} "Unread count"
// @Failure 401 {object} ErrorResponse "Unauthorized"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /notifications/count [get]
func (h *NotificationHandler) GetUnreadCount(w http.ResponseWriter, r *http.Request) {
// Get user ID from context (set by auth middleware)
userID, ok := middleware.GetUserID(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "Unauthorized")
return
}
// Get unread count
count, err := h.notificationService.GetUnreadCount(userID)
if err != nil {
if h.logger != nil {
h.logger.Error("action=get_unread_count outcome=failure user_id=%d error=%v", userID, err)
}
respondError(w, http.StatusInternalServerError, "Failed to get unread count")
return
}
if h.logger != nil {
h.logger.Debug("action=get_unread_count outcome=success user_id=%d count=%d", userID, count)
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"count": count,
})
}
// MarkAsRead handles PUT /api/notifications/{id}/read
// @Summary Mark notification as read
// @Description Mark a specific notification as read
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Notification ID"
// @Success 200 {object} MessageResponse "Marked as read"
// @Failure 400 {object} ErrorResponse "Invalid notification ID"
// @Failure 401 {object} ErrorResponse "Unauthorized"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /notifications/{id}/read [put]
func (h *NotificationHandler) MarkAsRead(w http.ResponseWriter, r *http.Request) {
// Get user ID from context (set by auth middleware)
userID, ok := middleware.GetUserID(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "Unauthorized")
return
}
// Get notification ID from URL
notificationIDStr := chi.URLParam(r, "id")
notificationID, err := strconv.ParseInt(notificationIDStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid notification ID")
return
}
// Mark as read
if err := h.notificationService.MarkAsRead(notificationID); err != nil {
if h.logger != nil {
h.logger.Error("action=mark_as_read outcome=failure user_id=%d notification_id=%d error=%v", userID, notificationID, err)
}
respondError(w, http.StatusInternalServerError, "Failed to mark notification as read")
return
}
if h.logger != nil {
h.logger.Info("action=mark_as_read outcome=success user_id=%d notification_id=%d", userID, notificationID)
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"message": "Notification marked as read",
})
}
// MarkAllAsRead handles PUT /api/notifications/read-all
// @Summary Mark all notifications as read
// @Description Mark all notifications for the user as read
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} MessageResponse "All marked as read"
// @Failure 401 {object} ErrorResponse "Unauthorized"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /notifications/read-all [put]
func (h *NotificationHandler) MarkAllAsRead(w http.ResponseWriter, r *http.Request) {
// Get user ID from context (set by auth middleware)
userID, ok := middleware.GetUserID(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "Unauthorized")
return
}
// Mark all as read
if err := h.notificationService.MarkAllAsRead(userID); err != nil {
if h.logger != nil {
h.logger.Error("action=mark_all_as_read outcome=failure user_id=%d error=%v", userID, err)
}
respondError(w, http.StatusInternalServerError, "Failed to mark all notifications as read")
return
}
if h.logger != nil {
h.logger.Info("action=mark_all_as_read outcome=success user_id=%d", userID)
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"message": "All notifications marked as read",
})
}
// DeleteNotification handles DELETE /api/notifications/{id}
// @Summary Delete notification
// @Description Delete a specific notification
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Notification ID"
// @Success 200 {object} MessageResponse "Notification deleted"
// @Failure 400 {object} ErrorResponse "Invalid notification ID"
// @Failure 401 {object} ErrorResponse "Unauthorized"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /notifications/{id} [delete]
func (h *NotificationHandler) DeleteNotification(w http.ResponseWriter, r *http.Request) {
// Get user ID from context (set by auth middleware)
userID, ok := middleware.GetUserID(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "Unauthorized")
return
}
// Get notification ID from URL
notificationIDStr := chi.URLParam(r, "id")
notificationID, err := strconv.ParseInt(notificationIDStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid notification ID")
return
}
// Delete notification
if err := h.notificationService.DeleteNotification(notificationID); err != nil {
if h.logger != nil {
h.logger.Error("action=delete_notification outcome=failure user_id=%d notification_id=%d error=%v", userID, notificationID, err)
}
respondError(w, http.StatusInternalServerError, "Failed to delete notification")
return
}
if h.logger != nil {
h.logger.Info("action=delete_notification outcome=success user_id=%d notification_id=%d", userID, notificationID)
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"message": "Notification deleted",
})
}
// CreateAnnouncementRequest represents an announcement creation request
// @Description Admin announcement request
type CreateAnnouncementRequest struct {
Title string `json:"title" example:"System Update"`
Message string `json:"message" example:"Scheduled maintenance tonight"`
TargetType string `json:"target_type" example:"all"` // "all", "organization", or "users"
TargetIDs []int64 `json:"target_ids"` // org IDs or user IDs
OrganizationID *int64 `json:"organization_id,omitempty"`
}
// CreateAnnouncement handles POST /api/admin/notifications/announce (admin only)
// @Summary Create announcement (Admin)
// @Description Send an announcement to all users, organization members, or specific users
// @Tags admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body CreateAnnouncementRequest true "Announcement details"
// @Success 201 {object} MessageResponse "Announcement created"
// @Failure 400 {object} ErrorResponse "Invalid request"
// @Failure 403 {object} ErrorResponse "Admin access required"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /admin/notifications/announce [post]
func (h *NotificationHandler) CreateAnnouncement(w http.ResponseWriter, r *http.Request) {
// Get user role from context (set by auth middleware)
userRole, ok := middleware.GetUserRole(r.Context())
if !ok || userRole != "admin" {
respondError(w, http.StatusForbidden, "Admin access required")
return
}
// Parse request body
var req struct {
Title string `json:"title"`
Message string `json:"message"`
TargetType string `json:"target_type"` // "all", "organization", or "users"
TargetIDs []int64 `json:"target_ids"` // org IDs or user IDs
OrganizationID *int64 `json:"organization_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
// Validate required fields
if req.Title == "" {
respondError(w, http.StatusBadRequest, "Title is required")
return
}
if req.Message == "" {
respondError(w, http.StatusBadRequest, "Message is required")
return
}
if req.TargetType == "" {
respondError(w, http.StatusBadRequest, "Target type is required")
return
}
// Validate target type
if req.TargetType != "all" && req.TargetType != "organization" && req.TargetType != "users" {
respondError(w, http.StatusBadRequest, "Invalid target type (must be 'all', 'organization', or 'users')")
return
}
// Validate target IDs for non-"all" target types
if req.TargetType != "all" && len(req.TargetIDs) == 0 {
respondError(w, http.StatusBadRequest, "Target IDs required for non-'all' target types")
return
}
// Create announcement
if err := h.notificationService.CreateAnnouncement(req.Title, req.Message, req.TargetType, req.TargetIDs, req.OrganizationID); err != nil {
if h.logger != nil {
h.logger.Error("action=create_announcement outcome=failure error=%v", err)
}
respondError(w, http.StatusInternalServerError, "Failed to create announcement")
return
}
if h.logger != nil {
h.logger.Info("action=create_announcement outcome=success target_type=%s title=%s", req.TargetType, req.Title)
}
respondJSON(w, http.StatusCreated, map[string]interface{}{
"message": "Announcement created successfully",
})
}