veza/veza-backend-api/internal/handlers/notification_handlers.go
senke a1000ce7fb style(backend): gofmt -w on 85 files (whitespace only)
backend-ci.yml's `test -z "$(gofmt -l .)"` strict gate (added in
13c21ac11) failed on a backlog of unformatted files. None of the
85 files in this commit had been edited since the gate was added
because no push touched veza-backend-api/** in between, so the
gate never fired until today's CI fixes triggered it.

The diff is exclusively whitespace alignment in struct literals
and trailing-space comments. `go build ./...` and the full test
suite (with VEZA_SKIP_INTEGRATION=1 -short) pass identically.
2026-04-14 12:22:14 +02:00

282 lines
9.1 KiB
Go

package handlers
import (
"net/http"
"strconv"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
var NotificationHandlersInstance *NotificationHandlers
// NotificationServiceInterface defines the interface for notification operations
// This allows for easier testing with mocks
type NotificationServiceInterface interface {
GetNotifications(userID uuid.UUID, params services.GetNotificationsParams) (*services.GetNotificationsResult, error)
MarkAsRead(userID uuid.UUID, notificationID uuid.UUID) error
MarkAllAsRead(userID uuid.UUID) error
GetUnreadCount(userID uuid.UUID) (int, error)
DeleteNotification(userID uuid.UUID, notificationID uuid.UUID) error
DeleteAllNotifications(userID uuid.UUID) error
GetPreferences(userID uuid.UUID) (*services.NotificationPrefs, error)
UpdatePreferences(userID uuid.UUID, pushFollow, pushLike, pushComment, pushMessage, pushMention *bool, quietHoursEnabled *bool, quietHoursStart, quietHoursEnd *string, weeklyDigestEnabled *bool) error
}
type NotificationHandlers struct {
notificationService NotificationServiceInterface
pushService *services.PushService
}
func NewNotificationHandlers(notificationService *services.NotificationService, pushService *services.PushService) {
NotificationHandlersInstance = &NotificationHandlers{
notificationService: notificationService,
pushService: pushService,
}
}
// NewNotificationHandlersWithInterface creates new notification handlers with an interface (for testing)
func NewNotificationHandlersWithInterface(notificationService NotificationServiceInterface) *NotificationHandlers {
return &NotificationHandlers{
notificationService: notificationService,
}
}
// GetNotifications retrieves all notifications for the authenticated user (v0.10.5 F555)
// GET /api/v1/notifications?type=follow|like|comment|...&page=1&limit=20&read=false
// BE-API-016: Implement notifications endpoints
func (nh *NotificationHandlers) GetNotifications(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
}
read := c.DefaultQuery("read", "")
unreadOnly := read == "false"
typeFilter := c.DefaultQuery("type", "")
page := 1
if p := c.Query("page"); p != "" {
if v, err := strconv.Atoi(p); err == nil && v > 0 {
page = v
}
}
limit := 20
if l := c.Query("limit"); l != "" {
if v, err := strconv.Atoi(l); err == nil && v > 0 && v <= 100 {
limit = v
}
}
result, err := nh.notificationService.GetNotifications(userID, services.GetNotificationsParams{
UnreadOnly: unreadOnly,
TypeFilter: typeFilter,
Page: page,
Limit: limit,
})
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get notifications", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{
"notifications": result.Notifications,
"total": result.Total,
"page": result.Page,
"limit": result.Limit,
"total_pages": result.TotalPages,
"unread_count": result.UnreadCount,
})
}
// MarkAsRead marks a notification as read
// POST /api/v1/notifications/:id/read
// BE-API-016: Implement notifications endpoints
func (nh *NotificationHandlers) MarkAsRead(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
}
notificationID, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid notification id"))
return
}
err = nh.notificationService.MarkAsRead(userID, notificationID)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to mark notification as read", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "Notification marked as read"})
}
// MarkAllAsRead marks all notifications as read for the user
// POST /api/v1/notifications/read-all
// BE-API-016: Implement notifications endpoints
func (nh *NotificationHandlers) MarkAllAsRead(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
}
if err := nh.notificationService.MarkAllAsRead(userID); err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to mark all notifications as read", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "All notifications marked as read"})
}
// GetUnreadCount returns the count of unread notifications
func (nh *NotificationHandlers) GetUnreadCount(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
count, err := nh.notificationService.GetUnreadCount(userID)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get unread count", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"count": count})
}
// DeleteNotification deletes a notification
func (nh *NotificationHandlers) DeleteNotification(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
notificationID, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid notification id"))
return
}
err = nh.notificationService.DeleteNotification(userID, notificationID)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to delete notification", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "Notification deleted"})
}
// DeleteAllNotifications deletes all notifications for the user
func (nh *NotificationHandlers) DeleteAllNotifications(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
if err := nh.notificationService.DeleteAllNotifications(userID); err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to delete all notifications", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "All notifications deleted"})
}
// SubscribePushRequest is the DTO for Web Push subscription (N1.1)
type SubscribePushRequest struct {
Endpoint string `json:"endpoint" binding:"required"`
Keys struct {
P256dh string `json:"p256dh" binding:"required"`
Auth string `json:"auth" binding:"required"`
} `json:"keys" binding:"required"`
}
// SubscribePush stores a Web Push subscription (N1.1)
func (nh *NotificationHandlers) SubscribePush(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
var req SubscribePushRequest
if err := c.ShouldBindJSON(&req); err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid request body"))
return
}
if nh.pushService == nil {
RespondSuccess(c, http.StatusOK, gin.H{"message": "Push not configured"})
return
}
if err := nh.pushService.SubscribePush(c.Request.Context(), userID, req.Endpoint, req.Keys.P256dh, req.Keys.Auth); err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to subscribe", err))
return
}
RespondSuccess(c, http.StatusCreated, gin.H{"message": "Subscribed"})
}
// GetPreferences returns notification preferences (N1.3)
func (nh *NotificationHandlers) GetPreferences(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
prefs, err := nh.notificationService.GetPreferences(userID)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get preferences", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{
"push_follow": prefs.PushFollow,
"push_like": prefs.PushLike,
"push_comment": prefs.PushComment,
"push_message": prefs.PushMessage,
"push_mention": prefs.PushMention,
"quiet_hours_enabled": prefs.QuietHoursEnabled,
"quiet_hours_start": prefs.QuietHoursStart,
"quiet_hours_end": prefs.QuietHoursEnd,
"weekly_digest_enabled": prefs.WeeklyDigestEnabled,
})
}
// UpdatePreferencesRequest is the DTO for updating preferences (F553: quiet hours)
type UpdatePreferencesRequest struct {
PushFollow *bool `json:"push_follow"`
PushLike *bool `json:"push_like"`
PushComment *bool `json:"push_comment"`
PushMessage *bool `json:"push_message"`
PushMention *bool `json:"push_mention"`
QuietHoursEnabled *bool `json:"quiet_hours_enabled"`
QuietHoursStart *string `json:"quiet_hours_start"` // "22:00"
QuietHoursEnd *string `json:"quiet_hours_end"` // "08:00"
WeeklyDigestEnabled *bool `json:"weekly_digest_enabled"`
}
// UpdatePreferences updates notification preferences (N1.3)
func (nh *NotificationHandlers) UpdatePreferences(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
var req UpdatePreferencesRequest
if err := c.ShouldBindJSON(&req); err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid request body"))
return
}
if err := nh.notificationService.UpdatePreferences(userID, req.PushFollow, req.PushLike, req.PushComment, req.PushMessage, req.PushMention,
req.QuietHoursEnabled, req.QuietHoursStart, req.QuietHoursEnd, req.WeeklyDigestEnabled); err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update preferences", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "Preferences updated"})
}