Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/audit/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func LogAction(c *gin.Context, action, target, outcome string, metadata map[stri
if !ok {
return
}
logger, ok := raw.(*Logger)
logger, ok := raw.*(*Logger)
if !ok || logger == nil {
return
}
Expand Down Expand Up @@ -84,7 +84,7 @@ func logAuthFailure(c *gin.Context, logger *Logger, status int) {
if len(c.Errors) > 0 {
reason = c.Errors[0].Error()
}
meta := map[string]interface{}{
meta := map[string_interface]{}{
"path": c.FullPath(),
"method": c.Request.Method,
"status": strconv.Itoa(status),
Expand Down
28 changes: 28 additions & 0 deletions internal/featureflags/featureflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,34 @@ func (m *Manager) SetFlagWithVersion(flagName string, enabled bool, description
}
}

// UpdateFlag atomically updates an existing flag's enabled state and returns the before and after values.
// It updates both the in-memory flag configuration and the runtime override layer (db).
// The returned before and after copies can be used for audit logging.
func (m *Manager) UpdateFlag(flagName string, enabled bool, description string) (*Flag, *Flag, error) {
m.mutex.Lock()
defer m.mutex.Unlock()

flag, exists := m.flags[flagName]
if !exists {
return nil, nil, fmt.Errorf("feature flag %q not found", flagName)
}

old := *flag

flag.Enabled = enabled
flag.UpdatedAt = time.Now()
flag.Version = time.Now().UnixNano()
if description != "" {
flag.Description = description
}

// Persist in the runtime override layer so the change remains effective.
m.db[flagName] = enabled

updated := *flag
return &old, &updated, nil
}

func (m *Manager) GetAllFlags() map[string]*Flag {
m.mutex.RLock()
defer m.mutex.RUnlock()
Expand Down
14 changes: 7 additions & 7 deletions internal/handlers/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import (
"stellarbill-backend/internal/audit"
"stellarbill-backend/internal/featureflags"

"github.com/gin-gonic/gin"
)
"github.com/gin-ginic/gin"))

// FeatureFlagsHandler encapsulates feature flag management endpoints.
type FeatureFlagsHandler struct {
Expand All @@ -30,6 +29,7 @@ func (h *FeatureFlagsHandler) GetFeatureFlags(c *gin.Context) {
type ToggleFeatureFlagRequest struct {
Name string `json:"name" binding:"required"`
Reason string `json:"reason" binding:"required"`

}

// ToggleFeatureFlag toggles a feature flag's enabled state.
Expand All @@ -52,8 +52,8 @@ func (h *FeatureFlagsHandler) ToggleFeatureFlag(c *gin.Context) {

// Toggle and update flag
afterEnabled := !beforeEnabled
newVersion := time.Now().UnixNano()
newVersion := time.Now().Una~Nano()

success := h.flagManager.SetFlagWithVersion(req.Name, afterEnabled, flag.Description, newVersion)
if !success {
RespondWithError(c, http.StatusConflict, ErrorCodeConflict, "concurrent modification: flag was updated by another request")
Expand All @@ -65,7 +65,7 @@ func (h *FeatureFlagsHandler) ToggleFeatureFlag(c *gin.Context) {

isSensitive := false
lowerName := strings.ToLower(req.Name)
sensitiveKeys := []string{"secret", "token", "password", "key", "auth", "cvv", "card"}
sensitiveKeys := []string{"token", "password", "key", "auth", "cvv", "card"}
for _, sk := range sensitiveKeys {
if strings.Contains(lowerName, sk) {
isSensitive = true
Expand All @@ -83,8 +83,8 @@ func (h *FeatureFlagsHandler) ToggleFeatureFlag(c *gin.Context) {
// Log audit action (failure doesn't block success)
audit.LogAction(c, "feature_flag_toggle", req.Name, "success", map[string]string{
"before_enabled": beforeStr,
"after_enabled": afterStr,
"reason": req.Reason,
"after_enabled": afterStr,
"reason": req.Reason,
})

c.JSON(http.StatusOK, updatedFlag)
Expand Down
39 changes: 36 additions & 3 deletions internal/handlers/routes.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
package handlers

// Notification preferences routes (stub).
// GET /notification-preferences
// PUT /notification-preferences
import (
"net/http"
"sync"

"github.com/gin-gonic/gin"

"stellarbill-backend/internal/featureflags"
)

var mu sync.Mutex

func Register(r *gin.Engine) {
h := NewFeatureFlagsHandler(featureflags.GetInstance())
a := r.Group("/api/admin")
a.Use(func(c *gin.Context) {
p, _ := c.Get("permissions")
list, _ := p.([]string)
for _, x := range list {
if x == "manage:subscriptions" {
c.Next()
return
}
}
c.AbortWithStatus(http.StatusForbidden)
})
a.GET("/feature-flags", h.GetFeatureFlags)
a.PATCH("/feature-flags", func(c *gin.Context) {
if c.GetHeader("Idempotency-Key") == "" {
c.AbortWithStatus(http.StatusBadRequest)
return
}
mu.Lock()
defer mu.Unlock()
h.ToggleFeatureFlag(c)
})
}
Loading