Backend (Go): - Config: CORS, RabbitMQ, rate limit, main config updates - Routes: core, distribution, tracks routing changes - Middleware: rate limiter, endpoint limiter, response cache hardening - Handlers: distribution, search handler fixes - Workers: job worker improvements - Upload validator and logging config additions - New migrations: products, orders, performance indexes - Seed tooling and data Stream Server (Rust): - Audio processing, config, routes, simple stream server updates - Dockerfile improvements Infrastructure: - docker-compose.yml updates - nginx-rtmp config changes - Makefile improvements (config, dev, high, infra) - Root package.json and lock file updates - .env.example updates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
280 lines
7.9 KiB
Go
280 lines
7.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"veza-backend-api/internal/core/distribution"
|
|
apperrors "veza-backend-api/internal/errors"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// DistributionHandler handles distribution HTTP endpoints (v0.12.2 F501-F510)
|
|
type DistributionHandler struct {
|
|
service *distribution.Service
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewDistributionHandler creates a new DistributionHandler
|
|
func NewDistributionHandler(service *distribution.Service, logger *zap.Logger) *DistributionHandler {
|
|
return &DistributionHandler{
|
|
service: service,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Submit handles POST /distributions/submit
|
|
func (h *DistributionHandler) Submit(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req distribution.SubmitRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("Invalid request: track_id, platforms, and metadata required"))
|
|
return
|
|
}
|
|
|
|
resp, err := h.service.Submit(c.Request.Context(), userID, req)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, distribution.ErrNotEligible):
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("Distribution requires Creator or Premium plan"))
|
|
case errors.Is(err, distribution.ErrTrackNotPublic):
|
|
RespondWithAppError(c, apperrors.NewValidationError("Track must be public and belong to you"))
|
|
case errors.Is(err, distribution.ErrAlreadyDistributed):
|
|
RespondWithAppError(c, apperrors.NewValidationError("Track already has an active distribution"))
|
|
case errors.Is(err, distribution.ErrNoPlatformsSelected):
|
|
RespondWithAppError(c, apperrors.NewValidationError("At least one platform must be selected"))
|
|
case errors.Is(err, distribution.ErrInvalidPlatform):
|
|
RespondWithAppError(c, apperrors.NewValidationError(err.Error()))
|
|
default:
|
|
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to submit distribution", err))
|
|
}
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusCreated, resp)
|
|
}
|
|
|
|
// GetDistribution handles GET /distributions/:id
|
|
func (h *DistributionHandler) GetDistribution(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
distID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("Invalid distribution ID"))
|
|
return
|
|
}
|
|
|
|
dist, err := h.service.GetDistribution(c.Request.Context(), distID)
|
|
if err != nil {
|
|
if errors.Is(err, distribution.ErrDistributionNotFound) {
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("Distribution"))
|
|
return
|
|
}
|
|
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get distribution", err))
|
|
return
|
|
}
|
|
|
|
// Ensure the distribution belongs to the requesting user
|
|
if dist.CreatorID != userID {
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("Distribution"))
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, dist)
|
|
}
|
|
|
|
// ListDistributions handles GET /distributions
|
|
func (h *DistributionHandler) ListDistributions(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
|
limit = clampLimit(limit) // SECURITY(MEDIUM-004)
|
|
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
|
|
|
var trackID *uuid.UUID
|
|
if tidStr := c.Query("track_id"); tidStr != "" {
|
|
tid, err := uuid.Parse(tidStr)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("Invalid track_id"))
|
|
return
|
|
}
|
|
trackID = &tid
|
|
}
|
|
|
|
var status *distribution.DistributionStatus
|
|
if statusStr := c.Query("status"); statusStr != "" {
|
|
s := distribution.DistributionStatus(statusStr)
|
|
status = &s
|
|
}
|
|
|
|
dists, total, err := h.service.ListDistributions(c.Request.Context(), userID, trackID, status, limit, offset)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to list distributions", err))
|
|
return
|
|
}
|
|
|
|
page := (offset / max(limit, 1)) + 1
|
|
totalPages := int((total + int64(limit) - 1) / int64(limit))
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"data": dists,
|
|
"pagination": gin.H{
|
|
"page": page,
|
|
"limit": limit,
|
|
"total": total,
|
|
"total_pages": totalPages,
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetTrackDistributions handles GET /tracks/:track_id/distributions
|
|
func (h *DistributionHandler) GetTrackDistributions(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
trackID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("Invalid track ID"))
|
|
return
|
|
}
|
|
|
|
dists, err := h.service.GetTrackDistributions(c.Request.Context(), trackID)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get track distributions", err))
|
|
return
|
|
}
|
|
|
|
// Filter to only show creator's own distributions
|
|
var filtered []distribution.TrackDistribution
|
|
for _, d := range dists {
|
|
if d.CreatorID == userID {
|
|
filtered = append(filtered, d)
|
|
}
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"distributions": filtered})
|
|
}
|
|
|
|
// GetStatusHistory handles GET /distributions/:id/status-history
|
|
func (h *DistributionHandler) GetStatusHistory(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
distID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("Invalid distribution ID"))
|
|
return
|
|
}
|
|
|
|
// Verify ownership
|
|
dist, err := h.service.GetDistribution(c.Request.Context(), distID)
|
|
if err != nil {
|
|
if errors.Is(err, distribution.ErrDistributionNotFound) {
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("Distribution"))
|
|
return
|
|
}
|
|
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get distribution", err))
|
|
return
|
|
}
|
|
if dist.CreatorID != userID {
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("Distribution"))
|
|
return
|
|
}
|
|
|
|
entries, err := h.service.GetStatusHistory(c.Request.Context(), distID)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get status history", err))
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"history": entries})
|
|
}
|
|
|
|
// RemoveDistribution handles POST /distributions/:id/remove
|
|
func (h *DistributionHandler) RemoveDistribution(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
distID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("Invalid distribution ID"))
|
|
return
|
|
}
|
|
|
|
if err := h.service.RemoveDistribution(c.Request.Context(), userID, distID); err != nil {
|
|
switch {
|
|
case errors.Is(err, distribution.ErrDistributionNotFound):
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("Distribution"))
|
|
case errors.Is(err, distribution.ErrDistributionNotRemovable):
|
|
RespondWithAppError(c, apperrors.NewValidationError("Distribution can only be removed when live"))
|
|
default:
|
|
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to remove distribution", err))
|
|
}
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Distribution removal requested"})
|
|
}
|
|
|
|
// GetExternalRoyalties handles GET /creators/me/external-royalties
|
|
func (h *DistributionHandler) GetExternalRoyalties(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
|
limit = clampLimit(limit) // SECURITY(MEDIUM-004)
|
|
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
|
|
|
var startDate, endDate *time.Time
|
|
if sd := c.Query("start_date"); sd != "" {
|
|
t, err := time.Parse("2006-01-02", sd)
|
|
if err == nil {
|
|
startDate = &t
|
|
}
|
|
}
|
|
if ed := c.Query("end_date"); ed != "" {
|
|
t, err := time.Parse("2006-01-02", ed)
|
|
if err == nil {
|
|
endDate = &t
|
|
}
|
|
}
|
|
|
|
var platform *string
|
|
if p := c.Query("platform"); p != "" {
|
|
platform = &p
|
|
}
|
|
|
|
royalties, summary, err := h.service.GetExternalRoyalties(c.Request.Context(), userID, startDate, endDate, platform, limit, offset)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get royalties", err))
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"data": royalties,
|
|
"summary": summary,
|
|
})
|
|
}
|