152 lines
4.9 KiB
Go
152 lines
4.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/prabhupadasays/spsays-bff/internal/middleware"
|
|
"github.com/prabhupadasays/spsays-bff/internal/repository"
|
|
"github.com/prabhupadasays/spsays-bff/internal/service"
|
|
"github.com/prabhupadasays/spsays-bff/pkg/response"
|
|
)
|
|
|
|
// BroadcastHandler serves both sides of announcements: the admin routes that
|
|
// compose and publish them, and the two app routes that fetch and dismiss.
|
|
type BroadcastHandler struct {
|
|
svc *service.BroadcastService
|
|
}
|
|
|
|
func NewBroadcastHandler(svc *service.BroadcastService) *BroadcastHandler {
|
|
return &BroadcastHandler{svc: svc}
|
|
}
|
|
|
|
func writeBroadcastError(w http.ResponseWriter, err error) {
|
|
switch {
|
|
case errors.Is(err, repository.ErrBroadcastGone):
|
|
response.Error(w, http.StatusNotFound, "broadcast not found")
|
|
case errors.Is(err, service.ErrBroadcastTitle),
|
|
errors.Is(err, service.ErrBroadcastBody),
|
|
errors.Is(err, service.ErrBroadcastCTA),
|
|
errors.Is(err, service.ErrBroadcastBanner):
|
|
response.Error(w, http.StatusBadRequest, err.Error())
|
|
default:
|
|
response.Error(w, http.StatusInternalServerError, "broadcast request failed")
|
|
}
|
|
}
|
|
|
|
// ── App routes ────────────────────────────────────────────────────────────
|
|
|
|
// Pending — GET /broadcasts/pending. Returns null when nothing is waiting, so
|
|
// the client can call it on load and after a nudge without special-casing.
|
|
func (h *BroadcastHandler) Pending(w http.ResponseWriter, r *http.Request) {
|
|
b, err := h.svc.Pending(r.Context(), middleware.UserID(r.Context()))
|
|
if err != nil {
|
|
writeBroadcastError(w, err)
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusOK, b)
|
|
}
|
|
|
|
// Seen — POST /broadcasts/{id}/seen. The "at most once" half; idempotent.
|
|
func (h *BroadcastHandler) Seen(w http.ResponseWriter, r *http.Request) {
|
|
if err := h.svc.MarkSeen(r.Context(), chi.URLParam(r, "id"),
|
|
middleware.UserID(r.Context())); err != nil {
|
|
writeBroadcastError(w, err)
|
|
return
|
|
}
|
|
response.NoContent(w)
|
|
}
|
|
|
|
// ── Admin routes ──────────────────────────────────────────────────────────
|
|
|
|
type createBroadcastReq struct {
|
|
Title string `json:"title"`
|
|
Body string `json:"body"`
|
|
BannerKey *string `json:"banner_key"`
|
|
CTALabel *string `json:"cta_label"`
|
|
CTAURL *string `json:"cta_url"`
|
|
// ExpiresAt stops the announcement being offered. Without one, someone
|
|
// signing up months later would still be shown it.
|
|
ExpiresAt *time.Time `json:"expires_at"`
|
|
// Publish immediately rather than saving a draft.
|
|
Publish bool `json:"publish"`
|
|
}
|
|
|
|
// Create — POST /admin/broadcasts
|
|
func (h *BroadcastHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|
var req createBroadcastReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
response.Error(w, http.StatusBadRequest, "invalid JSON body")
|
|
return
|
|
}
|
|
b, err := h.svc.Create(r.Context(), middleware.UserID(r.Context()), service.CreateParams{
|
|
Title: req.Title, Body: req.Body, BannerKey: req.BannerKey,
|
|
CTALabel: req.CTALabel, CTAURL: req.CTAURL, ExpiresAt: req.ExpiresAt,
|
|
})
|
|
if err != nil {
|
|
writeBroadcastError(w, err)
|
|
return
|
|
}
|
|
if req.Publish {
|
|
published, err := h.svc.Publish(r.Context(), b.ID)
|
|
if err != nil {
|
|
writeBroadcastError(w, err)
|
|
return
|
|
}
|
|
b = published
|
|
}
|
|
response.JSON(w, http.StatusCreated, b)
|
|
}
|
|
|
|
// Publish — POST /admin/broadcasts/{id}/publish
|
|
func (h *BroadcastHandler) Publish(w http.ResponseWriter, r *http.Request) {
|
|
b, err := h.svc.Publish(r.Context(), chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
writeBroadcastError(w, err)
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusOK, b)
|
|
}
|
|
|
|
// List — GET /admin/broadcasts
|
|
func (h *BroadcastHandler) List(w http.ResponseWriter, r *http.Request) {
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
items, err := h.svc.List(r.Context(), limit)
|
|
if err != nil {
|
|
writeBroadcastError(w, err)
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusOK, items)
|
|
}
|
|
|
|
// Delete — DELETE /admin/broadcasts/{id}
|
|
func (h *BroadcastHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
|
if err := h.svc.Delete(r.Context(), chi.URLParam(r, "id")); err != nil {
|
|
writeBroadcastError(w, err)
|
|
return
|
|
}
|
|
response.NoContent(w)
|
|
}
|
|
|
|
// PresignBanner — POST /admin/broadcasts/banner-presign
|
|
func (h *BroadcastHandler) PresignBanner(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
ContentType string `json:"content_type"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
response.Error(w, http.StatusBadRequest, "invalid JSON body")
|
|
return
|
|
}
|
|
upload, key, err := h.svc.PresignBanner(r.Context(), strings.TrimSpace(req.ContentType))
|
|
if err != nil {
|
|
response.Error(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusOK, map[string]string{"upload_url": upload, "key": key})
|
|
}
|