e881d37de0
Every public method on these five services now takes ctx context.Context as the first arg and routes its repo calls through .WithContext(ctx). With TaskService and ResidenceService already migrated, this means every in-process service that touches Postgres now produces a flame graph in Jaeger where the SQL spans nest under the parent HTTP request span. Endpoints now fully traced (HTTP → service → SQL): - /api/auth/login, /register, /logout, /me, /verify-email, /resend-verification - /api/auth/forgot-password, /verify-reset, /reset-password, /update-profile - /api/contractors/* (CRUD + favorite + by-residence + tasks) - /api/documents/* (CRUD + activate/deactivate + image upload/delete) - /api/notifications/* (list, count, mark-read, prefs, devices) - /api/subscription/* (status, purchase, cancel, triggers, promotions) - All previously-migrated /api/tasks/* and /api/residences/* paths Internal helpers also threaded: - TaskService.sendTaskCompletedNotification → forwards ctx - TaskService.UpdateUserTimezone → forwards ctx to NotificationService - ResidenceService.CreateResidence → forwards ctx to SubscriptionService.CheckLimit - NotificationService.registerAPNSDevice / registerGCMDevice → both take ctx ~75 method signatures, ~120 handler/test call sites updated. Tests pass green; the only failure is the pre-existing flaky TaskHandler_QuickComplete SQLite race that fails ~60% of runs on master. Step 3 of the observability plan is now genuinely complete: every API endpoint backed by a Go service emits a per-request flame graph with HTTP → service → SQL spans, plus B2/APNs/FCM/asynq spans where applicable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
246 lines
6.6 KiB
Go
246 lines
6.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
|
|
"github.com/treytartt/honeydue-api/internal/apperrors"
|
|
"github.com/treytartt/honeydue-api/internal/dto/responses"
|
|
"github.com/treytartt/honeydue-api/internal/i18n"
|
|
"github.com/treytartt/honeydue-api/internal/middleware"
|
|
"github.com/treytartt/honeydue-api/internal/services"
|
|
)
|
|
|
|
// NotificationHandler handles notification-related HTTP requests
|
|
type NotificationHandler struct {
|
|
notificationService *services.NotificationService
|
|
}
|
|
|
|
// NewNotificationHandler creates a new notification handler
|
|
func NewNotificationHandler(notificationService *services.NotificationService) *NotificationHandler {
|
|
return &NotificationHandler{notificationService: notificationService}
|
|
}
|
|
|
|
// ListNotifications handles GET /api/notifications/
|
|
func (h *NotificationHandler) ListNotifications(c echo.Context) error {
|
|
user, err := middleware.MustGetAuthUser(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
limit := 50
|
|
offset := 0
|
|
if l := c.QueryParam("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
if o := c.QueryParam("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
notifications, err := h.notificationService.GetNotifications(c.Request().Context(), user.ID, limit, offset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"count": len(notifications),
|
|
"results": notifications,
|
|
})
|
|
}
|
|
|
|
// GetUnreadCount handles GET /api/notifications/unread-count/
|
|
func (h *NotificationHandler) GetUnreadCount(c echo.Context) error {
|
|
user, err := middleware.MustGetAuthUser(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
count, err := h.notificationService.GetUnreadCount(c.Request().Context(), user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{"unread_count": count})
|
|
}
|
|
|
|
// MarkAsRead handles POST /api/notifications/:id/read/
|
|
func (h *NotificationHandler) MarkAsRead(c echo.Context) error {
|
|
user, err := middleware.MustGetAuthUser(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
notificationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
return apperrors.BadRequest("error.invalid_notification_id")
|
|
}
|
|
|
|
err = h.notificationService.MarkAsRead(c.Request().Context(), uint(notificationID), user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, responses.MessageResponse{Message: i18n.LocalizedMessage(c, "message.notification_marked_read")})
|
|
}
|
|
|
|
// MarkAllAsRead handles POST /api/notifications/mark-all-read/
|
|
func (h *NotificationHandler) MarkAllAsRead(c echo.Context) error {
|
|
user, err := middleware.MustGetAuthUser(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = h.notificationService.MarkAllAsRead(c.Request().Context(), user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, responses.MessageResponse{Message: i18n.LocalizedMessage(c, "message.all_notifications_marked_read")})
|
|
}
|
|
|
|
// GetPreferences handles GET /api/notifications/preferences/
|
|
func (h *NotificationHandler) GetPreferences(c echo.Context) error {
|
|
user, err := middleware.MustGetAuthUser(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
prefs, err := h.notificationService.GetPreferences(c.Request().Context(), user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, prefs)
|
|
}
|
|
|
|
// UpdatePreferences handles PUT/PATCH /api/notifications/preferences/
|
|
func (h *NotificationHandler) UpdatePreferences(c echo.Context) error {
|
|
user, err := middleware.MustGetAuthUser(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var req services.UpdatePreferencesRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return apperrors.BadRequest("error.invalid_request")
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return err
|
|
}
|
|
|
|
prefs, err := h.notificationService.UpdatePreferences(c.Request().Context(), user.ID, &req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, prefs)
|
|
}
|
|
|
|
// RegisterDevice handles POST /api/notifications/devices/
|
|
func (h *NotificationHandler) RegisterDevice(c echo.Context) error {
|
|
user, err := middleware.MustGetAuthUser(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var req services.RegisterDeviceRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return apperrors.BadRequest("error.invalid_request")
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return err
|
|
}
|
|
|
|
device, err := h.notificationService.RegisterDevice(c.Request().Context(), user.ID, &req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusCreated, device)
|
|
}
|
|
|
|
// ListDevices handles GET /api/notifications/devices/
|
|
func (h *NotificationHandler) ListDevices(c echo.Context) error {
|
|
user, err := middleware.MustGetAuthUser(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
devices, err := h.notificationService.ListDevices(c.Request().Context(), user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, devices)
|
|
}
|
|
|
|
// UnregisterDevice handles POST /api/notifications/devices/unregister/
|
|
// Accepts {registration_id, platform} and deactivates the matching device
|
|
func (h *NotificationHandler) UnregisterDevice(c echo.Context) error {
|
|
user, err := middleware.MustGetAuthUser(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var req struct {
|
|
RegistrationID string `json:"registration_id"`
|
|
Platform string `json:"platform"`
|
|
}
|
|
if err := c.Bind(&req); err != nil {
|
|
return apperrors.BadRequest("error.invalid_request")
|
|
}
|
|
if req.RegistrationID == "" {
|
|
return apperrors.BadRequest("error.registration_id_required")
|
|
}
|
|
if req.Platform == "" {
|
|
return apperrors.BadRequest("error.platform_required")
|
|
}
|
|
if req.Platform != "ios" && req.Platform != "android" {
|
|
return apperrors.BadRequest("error.invalid_platform")
|
|
}
|
|
|
|
err = h.notificationService.UnregisterDevice(c.Request().Context(), req.RegistrationID, req.Platform, user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, responses.MessageResponse{Message: i18n.LocalizedMessage(c, "message.device_removed")})
|
|
}
|
|
|
|
// DeleteDevice handles DELETE /api/notifications/devices/:id/
|
|
func (h *NotificationHandler) DeleteDevice(c echo.Context) error {
|
|
user, err := middleware.MustGetAuthUser(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
deviceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
return apperrors.BadRequest("error.invalid_device_id")
|
|
}
|
|
|
|
platform := c.QueryParam("platform")
|
|
if platform == "" {
|
|
return apperrors.BadRequest("error.platform_required")
|
|
}
|
|
if platform != "ios" && platform != "android" {
|
|
return apperrors.BadRequest("error.invalid_platform")
|
|
}
|
|
|
|
err = h.notificationService.DeleteDevice(c.Request().Context(), uint(deviceID), platform, user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, responses.MessageResponse{Message: i18n.LocalizedMessage(c, "message.device_removed")})
|
|
}
|