Ctrl K
ComunidadGuías…

internal/handler/notification_handler.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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",
	})
}