Files
Charon/backend/internal/api/handlers/feature_flags_handler.go
GitHub Actions 0492c1becb fix: implement user management UI
Complete user management frontend with resend invite, email validation,
and modal accessibility improvements.

Backend:

Add POST /api/v1/users/:id/resend-invite endpoint with authorization
Add 6 unit tests for resend invite handler
Fix feature flags default values
Frontend:

Add client-side email format validation with error display
Add resend invite button for pending users with Mail icon
Add Escape key keyboard navigation for modals
Fix PermissionsModal useState anti-pattern (now useEffect)
Add translations for de/es/fr/zh locales
Tests:

Enable 7 previously-skipped E2E tests (now 15 passing)
Fix Playwright locator strict mode violations
Update UsersPage test mocks for new API
Docs:

Document resend-invite API endpoint
Update CHANGELOG for Phase 6
2026-01-24 22:22:40 +00:00

118 lines
3.1 KiB
Go

package handlers
import (
"net/http"
"os"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/Wikid82/charon/backend/internal/models"
)
// FeatureFlagsHandler exposes simple DB-backed feature flags with env fallback.
type FeatureFlagsHandler struct {
DB *gorm.DB
}
func NewFeatureFlagsHandler(db *gorm.DB) *FeatureFlagsHandler {
return &FeatureFlagsHandler{DB: db}
}
// defaultFlags lists the canonical feature flags we expose.
var defaultFlags = []string{
"feature.cerberus.enabled",
"feature.uptime.enabled",
"feature.crowdsec.console_enrollment",
}
var defaultFlagValues = map[string]bool{
"feature.cerberus.enabled": false, // Cerberus OFF by default (per diagnostic fix)
"feature.uptime.enabled": true, // Uptime enabled by default
"feature.crowdsec.console_enrollment": false,
}
// GetFlags returns a map of feature flag -> bool. DB setting takes precedence
// and falls back to environment variables if present.
func (h *FeatureFlagsHandler) GetFlags(c *gin.Context) {
result := make(map[string]bool)
for _, key := range defaultFlags {
defaultVal := true
if v, ok := defaultFlagValues[key]; ok {
defaultVal = v
}
// Try DB
var s models.Setting
if err := h.DB.Where("key = ?", key).First(&s).Error; err == nil {
v := strings.ToLower(strings.TrimSpace(s.Value))
b := v == "1" || v == "true" || v == "yes"
result[key] = b
continue
}
// Fallback to env vars. Try FEATURE_... and also stripped service name e.g. CERBERUS_ENABLED
envKey := strings.ToUpper(strings.ReplaceAll(key, ".", "_"))
if ev, ok := os.LookupEnv(envKey); ok {
if bv, err := strconv.ParseBool(ev); err == nil {
result[key] = bv
continue
}
// accept 1/0
result[key] = ev == "1"
continue
}
// Try shorter variant after removing leading "feature."
if strings.HasPrefix(key, "feature.") {
short := strings.ToUpper(strings.ReplaceAll(strings.TrimPrefix(key, "feature."), ".", "_"))
if ev, ok := os.LookupEnv(short); ok {
if bv, err := strconv.ParseBool(ev); err == nil {
result[key] = bv
continue
}
result[key] = ev == "1"
continue
}
}
// Default based on declared flag value
result[key] = defaultVal
}
c.JSON(http.StatusOK, result)
}
// UpdateFlags accepts a JSON object map[string]bool and upserts settings.
func (h *FeatureFlagsHandler) UpdateFlags(c *gin.Context) {
var payload map[string]bool
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for k, v := range payload {
// Only allow keys in the default list to avoid arbitrary settings
allowed := false
for _, ak := range defaultFlags {
if ak == k {
allowed = true
break
}
}
if !allowed {
continue
}
s := models.Setting{Key: k, Value: strconv.FormatBool(v), Type: "bool", Category: "feature"}
if err := h.DB.Where(models.Setting{Key: k}).Assign(s).FirstOrCreate(&s).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save setting"})
return
}
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}