diff --git a/backend/internal/api/handlers/feature_flags_handler.go b/backend/internal/api/handlers/feature_flags_handler.go index b7dfd8f8..f874b210 100644 --- a/backend/internal/api/handlers/feature_flags_handler.go +++ b/backend/internal/api/handlers/feature_flags_handler.go @@ -30,6 +30,7 @@ var defaultFlags = []string{ "feature.crowdsec.console_enrollment", "feature.notifications.engine.notify_v1.enabled", "feature.notifications.service.discord.enabled", + "feature.notifications.service.email.enabled", "feature.notifications.service.gotify.enabled", "feature.notifications.service.webhook.enabled", "feature.notifications.security_provider_events.enabled", // Blocker 3: Add security_provider_events gate @@ -41,6 +42,7 @@ var defaultFlagValues = map[string]bool{ "feature.crowdsec.console_enrollment": false, "feature.notifications.engine.notify_v1.enabled": false, "feature.notifications.service.discord.enabled": false, + "feature.notifications.service.email.enabled": false, "feature.notifications.service.gotify.enabled": false, "feature.notifications.service.webhook.enabled": false, "feature.notifications.security_provider_events.enabled": false, // Blocker 3: Default disabled for this stage diff --git a/backend/internal/models/notification_config.go b/backend/internal/models/notification_config.go index 7bfed565..21c8518a 100644 --- a/backend/internal/models/notification_config.go +++ b/backend/internal/models/notification_config.go @@ -14,10 +14,10 @@ type NotificationConfig struct { MinLogLevel string `json:"min_log_level"` // error, warn, info, debug WebhookURL string `json:"webhook_url"` // Blocker 2 Fix: API surface uses security_* field names per spec (internal fields remain notify_*) - NotifyWAFBlocks bool `json:"security_waf_enabled"` - NotifyACLDenies bool `json:"security_acl_enabled"` - NotifyRateLimitHits bool `json:"security_rate_limit_enabled"` - NotifyCrowdSecDecisions bool `json:"security_crowdsec_enabled"` + NotifyWAFBlocks bool `json:"security_waf_enabled"` + NotifyACLDenies bool `json:"security_acl_enabled"` + NotifyRateLimitHits bool `json:"security_rate_limit_enabled"` + NotifyCrowdSecDecisions bool `json:"security_crowdsec_enabled"` // Legacy destination fields (compatibility, not stored in DB) DiscordWebhookURL string `gorm:"-" json:"discord_webhook_url,omitempty"` diff --git a/backend/internal/notifications/feature_flags.go b/backend/internal/notifications/feature_flags.go index f6792963..609fac7b 100644 --- a/backend/internal/notifications/feature_flags.go +++ b/backend/internal/notifications/feature_flags.go @@ -3,6 +3,7 @@ package notifications const ( FlagNotifyEngineEnabled = "feature.notifications.engine.notify_v1.enabled" FlagDiscordServiceEnabled = "feature.notifications.service.discord.enabled" + FlagEmailServiceEnabled = "feature.notifications.service.email.enabled" FlagGotifyServiceEnabled = "feature.notifications.service.gotify.enabled" FlagWebhookServiceEnabled = "feature.notifications.service.webhook.enabled" FlagSecurityProviderEventsEnabled = "feature.notifications.security_provider_events.enabled" diff --git a/backend/internal/services/notification_service.go b/backend/internal/services/notification_service.go index 724f4f9d..3eb4d9df 100644 --- a/backend/internal/services/notification_service.go +++ b/backend/internal/services/notification_service.go @@ -105,7 +105,7 @@ func supportsJSONTemplates(providerType string) bool { func isSupportedNotificationProviderType(providerType string) bool { switch strings.ToLower(strings.TrimSpace(providerType)) { - case "discord", "gotify", "webhook": + case "discord", "email", "gotify", "webhook": return true default: return false @@ -116,6 +116,8 @@ func (s *NotificationService) isDispatchEnabled(providerType string) bool { switch strings.ToLower(strings.TrimSpace(providerType)) { case "discord": return true + case "email": + return s.getFeatureFlagValue(notifications.FlagEmailServiceEnabled, false) case "gotify": return s.getFeatureFlagValue(notifications.FlagGotifyServiceEnabled, true) case "webhook": diff --git a/backend/internal/services/notification_service_test.go b/backend/internal/services/notification_service_test.go index 5e0303a2..9546ba45 100644 --- a/backend/internal/services/notification_service_test.go +++ b/backend/internal/services/notification_service_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/Wikid82/charon/backend/internal/models" + "github.com/Wikid82/charon/backend/internal/notifications" "github.com/Wikid82/charon/backend/internal/security" "github.com/Wikid82/charon/backend/internal/trace" "github.com/stretchr/testify/assert" @@ -2316,6 +2317,30 @@ func TestIsDispatchEnabled_WebhookDefaultTrue(t *testing.T) { assert.True(t, svc.isDispatchEnabled("webhook")) } +func TestFlagEmailServiceEnabled_ConstantValue(t *testing.T) { + assert.Equal(t, "feature.notifications.service.email.enabled", notifications.FlagEmailServiceEnabled) +} + +func TestIsSupportedNotificationProviderType_Email(t *testing.T) { + assert.True(t, isSupportedNotificationProviderType("email")) +} + +func TestIsDispatchEnabled_EmailDefaultFalse(t *testing.T) { + db := setupNotificationTestDB(t) + _ = db.AutoMigrate(&models.Setting{}) + svc := NewNotificationService(db) + + // No feature flag row — email defaults to false + assert.False(t, svc.isDispatchEnabled("email")) + + // Explicitly set flag to true — should now return true + require.NoError(t, db.Create(&models.Setting{ + Key: notifications.FlagEmailServiceEnabled, + Value: "true", + }).Error) + assert.True(t, svc.isDispatchEnabled("email")) +} + func TestTestProvider_GotifyWorksWithoutFeatureFlag(t *testing.T) { db := setupNotificationTestDB(t) _ = db.AutoMigrate(&models.Setting{}) diff --git a/tests/fixtures/settings.ts b/tests/fixtures/settings.ts index b8fadb5d..566e004f 100644 --- a/tests/fixtures/settings.ts +++ b/tests/fixtures/settings.ts @@ -272,6 +272,7 @@ export const certificateEmailSettings = { export interface FeatureFlags { cerberus_enabled: boolean; crowdsec_console_enrollment: boolean; + "feature.notifications.service.email.enabled": boolean; uptime_monitoring: boolean; } @@ -281,6 +282,7 @@ export interface FeatureFlags { export const defaultFeatureFlags: FeatureFlags = { cerberus_enabled: false, crowdsec_console_enrollment: false, + "feature.notifications.service.email.enabled": false, uptime_monitoring: false, }; @@ -290,6 +292,7 @@ export const defaultFeatureFlags: FeatureFlags = { export const allFeaturesEnabled: FeatureFlags = { cerberus_enabled: true, crowdsec_console_enrollment: true, + "feature.notifications.service.email.enabled": false, uptime_monitoring: true, };