feat(notify): convert email lists to NotifyEmailEntry struct with toggle support
- Change balance_notify_extra_emails and account_quota_notify_emails
from []string to []NotifyEmailEntry{email, disabled, verified}
- Add per-email enable/disable toggle for both user and admin notifications
- Add PUT /user/notify-email/toggle API endpoint
- Fix critical bug: API key auth cache snapshot missing balance notify
fields (Email, Username, BalanceNotifyEnabled, etc.), causing
notifications to never fire on cached request paths
- Bump cache snapshot version 3→4 to invalidate stale entries
- Add SQL migration 104 to convert old format data
- Backward compatible: parseNotifyEmails auto-detects old/new format
- User balance notify: max 3 emails (primary + 2 extra)
- Admin quota notify: unlimited emails, each with toggle
This commit is contained in:
@@ -178,7 +178,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
|
||||
BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
|
||||
BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
|
||||
AccountQuotaNotifyEmails: settings.AccountQuotaNotifyEmails,
|
||||
AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(settings.AccountQuotaNotifyEmails),
|
||||
PaymentEnabled: paymentCfg.Enabled,
|
||||
PaymentMinAmount: paymentCfg.MinAmount,
|
||||
PaymentMaxAmount: paymentCfg.MaxAmount,
|
||||
@@ -311,7 +311,7 @@ type UpdateSettingsRequest struct {
|
||||
// Balance low notification
|
||||
BalanceLowNotifyEnabled *bool `json:"balance_low_notify_enabled"`
|
||||
BalanceLowNotifyThreshold *float64 `json:"balance_low_notify_threshold"`
|
||||
AccountQuotaNotifyEmails *[]string `json:"account_quota_notify_emails"`
|
||||
AccountQuotaNotifyEmails *[]dto.NotifyEmailEntry `json:"account_quota_notify_emails"`
|
||||
|
||||
// Payment configuration (integrated into settings, full replace)
|
||||
PaymentEnabled *bool `json:"payment_enabled"`
|
||||
@@ -902,9 +902,9 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
return previousSettings.BalanceLowNotifyThreshold
|
||||
}(),
|
||||
AccountQuotaNotifyEmails: func() []string {
|
||||
AccountQuotaNotifyEmails: func() []service.NotifyEmailEntry {
|
||||
if req.AccountQuotaNotifyEmails != nil {
|
||||
return *req.AccountQuotaNotifyEmails
|
||||
return dto.NotifyEmailEntriesToService(*req.AccountQuotaNotifyEmails)
|
||||
}
|
||||
return previousSettings.AccountQuotaNotifyEmails
|
||||
}(),
|
||||
@@ -1056,7 +1056,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
EnableCCHSigning: updatedSettings.EnableCCHSigning,
|
||||
BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled,
|
||||
BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold,
|
||||
AccountQuotaNotifyEmails: updatedSettings.AccountQuotaNotifyEmails,
|
||||
AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(updatedSettings.AccountQuotaNotifyEmails),
|
||||
PaymentEnabled: updatedPaymentCfg.Enabled,
|
||||
PaymentMinAmount: updatedPaymentCfg.MinAmount,
|
||||
PaymentMaxAmount: updatedPaymentCfg.MaxAmount,
|
||||
|
||||
@@ -26,7 +26,7 @@ func UserFromServiceShallow(u *service.User) *User {
|
||||
BalanceNotifyEnabled: u.BalanceNotifyEnabled,
|
||||
BalanceNotifyThresholdType: u.BalanceNotifyThresholdType,
|
||||
BalanceNotifyThreshold: u.BalanceNotifyThreshold,
|
||||
BalanceNotifyExtraEmails: u.BalanceNotifyExtraEmails,
|
||||
BalanceNotifyExtraEmails: NotifyEmailEntriesFromService(u.BalanceNotifyExtraEmails),
|
||||
TotalRecharged: u.TotalRecharged,
|
||||
}
|
||||
}
|
||||
|
||||
43
backend/internal/handler/dto/notify_email_entry.go
Normal file
43
backend/internal/handler/dto/notify_email_entry.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package dto
|
||||
|
||||
import "github.com/Wei-Shaw/sub2api/internal/service"
|
||||
|
||||
// NotifyEmailEntry represents a notification email with enable/disable and verification state.
|
||||
// Email="" is a placeholder for the "primary email" (user's registration email or first admin email).
|
||||
type NotifyEmailEntry struct {
|
||||
Email string `json:"email"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
// NotifyEmailEntriesFromService converts service entries to DTO entries.
|
||||
func NotifyEmailEntriesFromService(entries []service.NotifyEmailEntry) []NotifyEmailEntry {
|
||||
if entries == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]NotifyEmailEntry, len(entries))
|
||||
for i, e := range entries {
|
||||
result[i] = NotifyEmailEntry{
|
||||
Email: e.Email,
|
||||
Disabled: e.Disabled,
|
||||
Verified: e.Verified,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// NotifyEmailEntriesToService converts DTO entries to service entries.
|
||||
func NotifyEmailEntriesToService(entries []NotifyEmailEntry) []service.NotifyEmailEntry {
|
||||
if entries == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]service.NotifyEmailEntry, len(entries))
|
||||
for i, e := range entries {
|
||||
result[i] = service.NotifyEmailEntry{
|
||||
Email: e.Email,
|
||||
Disabled: e.Disabled,
|
||||
Verified: e.Verified,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -152,7 +152,7 @@ type SystemSettings struct {
|
||||
// Balance low notification
|
||||
BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"`
|
||||
BalanceLowNotifyThreshold float64 `json:"balance_low_notify_threshold"`
|
||||
AccountQuotaNotifyEmails []string `json:"account_quota_notify_emails"`
|
||||
AccountQuotaNotifyEmails []NotifyEmailEntry `json:"account_quota_notify_emails"`
|
||||
}
|
||||
|
||||
type DefaultSubscriptionSetting struct {
|
||||
|
||||
@@ -22,7 +22,7 @@ type User struct {
|
||||
BalanceNotifyEnabled bool `json:"balance_notify_enabled"`
|
||||
BalanceNotifyThresholdType string `json:"balance_notify_threshold_type"`
|
||||
BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"`
|
||||
BalanceNotifyExtraEmails []string `json:"balance_notify_extra_emails"`
|
||||
BalanceNotifyExtraEmails []NotifyEmailEntry `json:"balance_notify_extra_emails"`
|
||||
TotalRecharged float64 `json:"total_recharged"`
|
||||
|
||||
APIKeys []APIKey `json:"api_keys,omitempty"`
|
||||
|
||||
@@ -214,3 +214,39 @@ func (h *UserHandler) RemoveNotifyEmail(c *gin.Context) {
|
||||
|
||||
response.Success(c, dto.UserFromService(updatedUser))
|
||||
}
|
||||
|
||||
// ToggleNotifyEmailRequest represents the request to toggle a notify email's disabled state
|
||||
type ToggleNotifyEmailRequest struct {
|
||||
Email string `json:"email"` // empty string for primary email placeholder
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
// ToggleNotifyEmail toggles the disabled state of a notification email
|
||||
// PUT /api/v1/user/notify-email/toggle
|
||||
func (h *UserHandler) ToggleNotifyEmail(c *gin.Context) {
|
||||
subject, ok := middleware2.GetAuthSubjectFromContext(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
var req ToggleNotifyEmailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err := h.userService.ToggleNotifyEmail(c.Request.Context(), subject.UserID, req.Email, req.Disabled)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedUser, err := h.userService.GetByID(c.Request.Context(), subject.UserID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.UserFromService(updatedUser))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user