feat(admin): feature flags CRUD with DB persistence

This commit is contained in:
senke 2026-02-25 19:56:24 +01:00
parent 99b7cd8d97
commit c782bcb5b3
5 changed files with 140 additions and 0 deletions

View file

@ -436,6 +436,12 @@ func (r *APIRouter) setupCoreProtectedRoutes(v1 *gin.RouterGroup) {
admin.POST("/announcements", announcementHandler.Create)
admin.DELETE("/announcements/:id", announcementHandler.Delete)
// v0.803 ADM1-05: Feature flags CRUD
featureFlagSvc := services.NewFeatureFlagService(r.db.GormDB, r.logger)
featureFlagHandler := handlers.NewFeatureFlagHandler(featureFlagSvc)
admin.GET("/feature-flags", featureFlagHandler.List)
admin.PUT("/feature-flags/:name", featureFlagHandler.Toggle)
// v0.701: Admin Transfer Dashboard
var adminTransferHandler *handlers.AdminTransferHandler
if r.config != nil && r.config.StripeConnectEnabled && r.config.StripeConnectSecretKey != "" {

View file

@ -0,0 +1,53 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"veza-backend-api/internal/services"
)
// FeatureFlagHandler handles feature flag endpoints (v0.803 ADM1-05)
type FeatureFlagHandler struct {
svc *services.FeatureFlagService
}
// NewFeatureFlagHandler creates a new FeatureFlagHandler
func NewFeatureFlagHandler(svc *services.FeatureFlagService) *FeatureFlagHandler {
return &FeatureFlagHandler{svc: svc}
}
// List returns all feature flags (admin)
func (h *FeatureFlagHandler) List(c *gin.Context) {
list, err := h.svc.List(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list feature flags"})
return
}
c.JSON(http.StatusOK, gin.H{"feature_flags": list})
}
// Toggle enables or disables a feature flag (admin)
func (h *FeatureFlagHandler) Toggle(c *gin.Context) {
name := c.Param("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
return
}
var req struct {
Enabled bool `json:"enabled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "enabled is required"})
return
}
flag, err := h.svc.Toggle(c.Request.Context(), name, req.Enabled)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, flag)
}

View file

@ -0,0 +1,18 @@
package models
import (
"time"
)
// FeatureFlag represents a feature flag (v0.803 ADM1-05)
type FeatureFlag struct {
Name string `gorm:"primaryKey;size:100" json:"name"`
Enabled bool `gorm:"not null;default:false" json:"enabled"`
Description string `gorm:"type:text" json:"description,omitempty"`
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
}
// TableName returns the table name
func (FeatureFlag) TableName() string {
return "feature_flags"
}

View file

@ -0,0 +1,49 @@
package services
import (
"context"
"fmt"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"veza-backend-api/internal/models"
)
// FeatureFlagService handles feature flags (v0.803 ADM1-05)
type FeatureFlagService struct {
db *gorm.DB
logger *zap.Logger
}
// NewFeatureFlagService creates a new FeatureFlagService
func NewFeatureFlagService(db *gorm.DB, logger *zap.Logger) *FeatureFlagService {
return &FeatureFlagService{db: db, logger: logger}
}
// List returns all feature flags
func (s *FeatureFlagService) List(ctx context.Context) ([]models.FeatureFlag, error) {
var list []models.FeatureFlag
if err := s.db.WithContext(ctx).Order("name ASC").Find(&list).Error; err != nil {
return nil, fmt.Errorf("failed to list feature flags: %w", err)
}
return list, nil
}
// Toggle enables or disables a feature flag by name
func (s *FeatureFlagService) Toggle(ctx context.Context, name string, enabled bool) (*models.FeatureFlag, error) {
var flag models.FeatureFlag
if err := s.db.WithContext(ctx).Where("name = ?", name).First(&flag).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("feature flag not found: %s", name)
}
return nil, fmt.Errorf("failed to get feature flag: %w", err)
}
flag.Enabled = enabled
flag.UpdatedAt = time.Now()
if err := s.db.WithContext(ctx).Save(&flag).Error; err != nil {
return nil, fmt.Errorf("failed to update feature flag: %w", err)
}
return &flag, nil
}

View file

@ -0,0 +1,14 @@
-- v0.803 ADM1-05: Feature flags with DB persistence
CREATE TABLE IF NOT EXISTS feature_flags (
name VARCHAR(100) PRIMARY KEY,
enabled BOOLEAN NOT NULL DEFAULT false,
description TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
INSERT INTO feature_flags (name, enabled, description) VALUES
('HLS_STREAMING', true, 'Enable HLS streaming'),
('ROLE_MANAGEMENT', true, 'Enable role management'),
('PLAYLIST_SHARE', true, 'Enable playlist sharing'),
('PLAYLIST_RECOMMENDATIONS', true, 'Enable playlist recommendations')
ON CONFLICT (name) DO NOTHING;