veza/veza-backend-api/internal/handlers/distribution_handler.go
senke 73eca4f6ad feat: backend, stream server & infra improvements
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>
2026-03-18 11:36:06 +01:00

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,
})
}