draft grpc

This commit is contained in:
2026-03-13 02:17:18 +00:00
parent ea2edbb9e0
commit 91e5e3542b
116 changed files with 44505 additions and 558 deletions

View File

@@ -0,0 +1,338 @@
//go:build ignore
// +build ignore
package adtemplates
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"stream.api/internal/database/model"
"stream.api/pkg/logger"
"stream.api/pkg/response"
)
const upgradeRequiredMessage = "Upgrade required to manage Ads & VAST"
type Handler struct {
logger logger.Logger
db *gorm.DB
}
type SaveAdTemplateRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
VASTTagURL string `json:"vast_tag_url" binding:"required"`
AdFormat string `json:"ad_format"`
Duration *int `json:"duration"`
IsActive *bool `json:"is_active"`
IsDefault *bool `json:"is_default"`
}
type TemplatePayload struct {
Template *model.AdTemplate `json:"template"`
}
type TemplateListPayload struct {
Templates []model.AdTemplate `json:"templates"`
}
func NewHandler(l logger.Logger, db *gorm.DB) *Handler {
return &Handler{logger: l, db: db}
}
// @Summary List Ad Templates
// @Description Get all VAST ad templates for the current user
// @Tags ad-templates
// @Produce json
// @Success 200 {object} response.Response{data=TemplateListPayload}
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /ad-templates [get]
// @Security BearerAuth
func (h *Handler) ListTemplates(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
var items []model.AdTemplate
if err := h.db.WithContext(c.Request.Context()).
Where("user_id = ?", userID).
Order("is_default DESC").
Order("created_at DESC").
Find(&items).Error; err != nil {
h.logger.Error("Failed to list ad templates", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to load ad templates")
return
}
response.Success(c, gin.H{"templates": items})
}
// @Summary Create Ad Template
// @Description Create a VAST ad template for the current user
// @Tags ad-templates
// @Accept json
// @Produce json
// @Param request body SaveAdTemplateRequest true "Ad template payload"
// @Success 201 {object} response.Response{data=TemplatePayload}
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 403 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /ad-templates [post]
// @Security BearerAuth
func (h *Handler) CreateTemplate(c *gin.Context) {
h.saveTemplate(c, true)
}
// @Summary Update Ad Template
// @Description Update a VAST ad template for the current user
// @Tags ad-templates
// @Accept json
// @Produce json
// @Param id path string true "Ad Template ID"
// @Param request body SaveAdTemplateRequest true "Ad template payload"
// @Success 200 {object} response.Response{data=TemplatePayload}
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 403 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /ad-templates/{id} [put]
// @Security BearerAuth
func (h *Handler) UpdateTemplate(c *gin.Context) {
h.saveTemplate(c, false)
}
// @Summary Delete Ad Template
// @Description Delete a VAST ad template for the current user
// @Tags ad-templates
// @Produce json
// @Param id path string true "Ad Template ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 403 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /ad-templates/{id} [delete]
// @Security BearerAuth
func (h *Handler) DeleteTemplate(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
if !requirePaidPlan(c) {
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
result := h.db.WithContext(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("ad_template_id = ? AND user_id = ?", id, userID).
Delete(&model.VideoAdConfig{}).Error; err != nil {
return err
}
res := tx.Where("id = ? AND user_id = ?", id, userID).Delete(&model.AdTemplate{})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
})
if result != nil {
if result == gorm.ErrRecordNotFound {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
h.logger.Error("Failed to delete ad template", "error", result)
response.Error(c, http.StatusInternalServerError, "Failed to delete ad template")
return
}
response.Success(c, gin.H{"message": "Ad template deleted"})
}
func (h *Handler) saveTemplate(c *gin.Context, create bool) {
userID := c.GetString("userID")
if userID == "" {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return
}
if !requirePaidPlan(c) {
return
}
var req SaveAdTemplateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
name := strings.TrimSpace(req.Name)
vastURL := strings.TrimSpace(req.VASTTagURL)
if name == "" || vastURL == "" {
response.Error(c, http.StatusBadRequest, "Name and VAST URL are required")
return
}
format := normalizeAdFormat(req.AdFormat)
if format == "mid-roll" && (req.Duration == nil || *req.Duration <= 0) {
response.Error(c, http.StatusBadRequest, "Duration is required for mid-roll templates")
return
}
ctx := c.Request.Context()
if create {
item := &model.AdTemplate{
ID: uuid.New().String(),
UserID: userID,
Name: name,
Description: stringPointer(strings.TrimSpace(req.Description)),
VastTagURL: vastURL,
AdFormat: model.StringPtr(format),
Duration: intPtrToInt64Ptr(req.Duration),
IsActive: model.BoolPtr(req.IsActive == nil || *req.IsActive),
IsDefault: req.IsDefault != nil && *req.IsDefault,
}
if !adTemplateIsActive(item.IsActive) {
item.IsDefault = false
}
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := unsetDefaultTemplates(tx, userID, ""); err != nil {
return err
}
}
return tx.Create(item).Error
}); err != nil {
h.logger.Error("Failed to create ad template", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to save ad template")
return
}
response.Created(c, gin.H{"template": item})
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
var item model.AdTemplate
if err := h.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).First(&item).Error; err != nil {
if err == gorm.ErrRecordNotFound {
response.Error(c, http.StatusNotFound, "Ad template not found")
return
}
h.logger.Error("Failed to load ad template", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to save ad template")
return
}
item.Name = name
item.Description = stringPointer(strings.TrimSpace(req.Description))
item.VastTagURL = vastURL
item.AdFormat = model.StringPtr(format)
item.Duration = intPtrToInt64Ptr(req.Duration)
if req.IsActive != nil {
item.IsActive = model.BoolPtr(*req.IsActive)
}
if req.IsDefault != nil {
item.IsDefault = *req.IsDefault
}
if !adTemplateIsActive(item.IsActive) {
item.IsDefault = false
}
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if item.IsDefault {
if err := unsetDefaultTemplates(tx, userID, item.ID); err != nil {
return err
}
}
return tx.Save(&item).Error
}); err != nil {
h.logger.Error("Failed to update ad template", "error", err)
response.Error(c, http.StatusInternalServerError, "Failed to save ad template")
return
}
response.Success(c, gin.H{"template": item})
}
func requirePaidPlan(c *gin.Context) bool {
userValue, exists := c.Get("user")
if !exists {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return false
}
user, ok := userValue.(*model.User)
if !ok || user == nil {
response.Error(c, http.StatusUnauthorized, "Unauthorized")
return false
}
if user.PlanID == nil || strings.TrimSpace(*user.PlanID) == "" {
response.Error(c, http.StatusForbidden, upgradeRequiredMessage)
return false
}
return true
}
func unsetDefaultTemplates(tx *gorm.DB, userID, excludeID string) error {
query := tx.Model(&model.AdTemplate{}).Where("user_id = ?", userID)
if excludeID != "" {
query = query.Where("id <> ?", excludeID)
}
return query.Update("is_default", false).Error
}
func normalizeAdFormat(value string) string {
switch strings.TrimSpace(strings.ToLower(value)) {
case "mid-roll", "post-roll":
return strings.TrimSpace(strings.ToLower(value))
default:
return "pre-roll"
}
}
func stringPointer(value string) *string {
if value == "" {
return nil
}
return &value
}
func intPtrToInt64Ptr(value *int) *int64 {
if value == nil {
return nil
}
converted := int64(*value)
return &converted
}
func adTemplateIsActive(value *bool) bool {
return value == nil || *value
}