feat(discovery): add HTTP handlers and route registration
This commit is contained in:
118
backend/internal/domain/discovery/handler.go
Normal file
118
backend/internal/domain/discovery/handler.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"marktvogt.de/backend/internal/pkg/apierror"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
}
|
||||
|
||||
func NewHandler(s *Service) *Handler {
|
||||
return &Handler{service: s}
|
||||
}
|
||||
|
||||
func (h *Handler) Tick(c *gin.Context) {
|
||||
summary, err := h.service.Tick(c.Request.Context())
|
||||
if err != nil {
|
||||
slog.ErrorContext(c.Request.Context(), "discovery tick failed", "error", err)
|
||||
apiErr := apierror.Internal("tick failed")
|
||||
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": summary})
|
||||
}
|
||||
|
||||
func (h *Handler) ListQueue(c *gin.Context) {
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
if limit < 1 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
rows, err := h.service.ListPendingQueue(c.Request.Context(), limit, offset)
|
||||
if err != nil {
|
||||
apiErr := apierror.Internal("list queue failed")
|
||||
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": rows})
|
||||
}
|
||||
|
||||
func (h *Handler) Accept(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
apiErr := apierror.BadRequest("invalid_id", "invalid queue id")
|
||||
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
|
||||
return
|
||||
}
|
||||
reviewer, ok := currentUserID(c)
|
||||
if !ok {
|
||||
apiErr := apierror.Unauthorized("no user in context")
|
||||
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
|
||||
return
|
||||
}
|
||||
seriesID, editionID, err := h.service.Accept(c.Request.Context(), id, reviewer)
|
||||
if err != nil {
|
||||
slog.WarnContext(c.Request.Context(), "accept failed", "queue_id", id, "error", err)
|
||||
apiErr := apierror.Internal("accept failed")
|
||||
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": gin.H{"series_id": seriesID, "edition_id": editionID}})
|
||||
}
|
||||
|
||||
type rejectRequest struct {
|
||||
Reason string `json:"reason" validate:"max=2000"`
|
||||
}
|
||||
|
||||
func (h *Handler) Reject(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
apiErr := apierror.BadRequest("invalid_id", "invalid queue id")
|
||||
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
|
||||
return
|
||||
}
|
||||
var req rejectRequest
|
||||
// Empty body is OK (reason is optional). Non-EOF parse errors are tolerated;
|
||||
// we fall through with zero-value req in all error cases.
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
reviewer, ok := currentUserID(c)
|
||||
if !ok {
|
||||
apiErr := apierror.Unauthorized("no user in context")
|
||||
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
|
||||
return
|
||||
}
|
||||
if err := h.service.Reject(c.Request.Context(), id, reviewer, req.Reason); err != nil {
|
||||
slog.WarnContext(c.Request.Context(), "reject failed", "queue_id", id, "error", err)
|
||||
apiErr := apierror.Internal("reject failed")
|
||||
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func currentUserID(c *gin.Context) (uuid.UUID, bool) {
|
||||
raw, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
return uuid.Nil, false
|
||||
}
|
||||
switch v := raw.(type) {
|
||||
case uuid.UUID:
|
||||
return v, true
|
||||
case string:
|
||||
id, err := uuid.Parse(v)
|
||||
return id, err == nil
|
||||
default:
|
||||
return uuid.Nil, false
|
||||
}
|
||||
}
|
||||
22
backend/internal/domain/discovery/routes.go
Normal file
22
backend/internal/domain/discovery/routes.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package discovery
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// RegisterRoutes mounts both the admin-session routes (queue mgmt) and the
|
||||
// bearer-token route (tick). The two middlewares are passed in separately.
|
||||
func RegisterRoutes(
|
||||
rg *gin.RouterGroup,
|
||||
h *Handler,
|
||||
requireAuth, requireAdmin, requireTickToken gin.HandlerFunc,
|
||||
) {
|
||||
// Machine-driven tick (bearer token).
|
||||
rg.POST("/admin/discovery/tick", requireTickToken, h.Tick)
|
||||
|
||||
// Admin-session queue routes.
|
||||
admin := rg.Group("/admin/discovery", requireAuth, requireAdmin)
|
||||
{
|
||||
admin.GET("/queue", h.ListQueue)
|
||||
admin.POST("/queue/:id/accept", h.Accept)
|
||||
admin.POST("/queue/:id/reject", h.Reject)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user