feat(admin): feature flags CRUD with DB persistence
This commit is contained in:
parent
99b7cd8d97
commit
c782bcb5b3
5 changed files with 140 additions and 0 deletions
|
|
@ -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 != "" {
|
||||
|
|
|
|||
53
veza-backend-api/internal/handlers/feature_flag_handler.go
Normal file
53
veza-backend-api/internal/handlers/feature_flag_handler.go
Normal 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)
|
||||
}
|
||||
18
veza-backend-api/internal/models/feature_flag.go
Normal file
18
veza-backend-api/internal/models/feature_flag.go
Normal 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"
|
||||
}
|
||||
49
veza-backend-api/internal/services/feature_flag_service.go
Normal file
49
veza-backend-api/internal/services/feature_flag_service.go
Normal 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
|
||||
}
|
||||
14
veza-backend-api/migrations/935_feature_flags.sql
Normal file
14
veza-backend-api/migrations/935_feature_flags.sql
Normal 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;
|
||||
Loading…
Reference in a new issue