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:
@@ -18,7 +18,7 @@ var (
|
||||
ErrInsufficientPerms = infraerrors.Forbidden("INSUFFICIENT_PERMISSIONS", "insufficient permissions")
|
||||
)
|
||||
|
||||
const maxNotifyExtraEmails = 5
|
||||
const maxNotifyEmails = 3 // Total limit: primary (email="") + up to 2 extra
|
||||
|
||||
// UserListFilters contains all filter options for listing users
|
||||
type UserListFilters struct {
|
||||
@@ -338,17 +338,21 @@ func (s *UserService) VerifyAndAddNotifyEmail(ctx context.Context, userID int64,
|
||||
|
||||
// Check if already exists
|
||||
for _, e := range user.BalanceNotifyExtraEmails {
|
||||
if strings.EqualFold(e, email) {
|
||||
if strings.EqualFold(e.Email, email) {
|
||||
return nil // Already added
|
||||
}
|
||||
}
|
||||
|
||||
// Check limit
|
||||
if len(user.BalanceNotifyExtraEmails) >= maxNotifyExtraEmails {
|
||||
return infraerrors.BadRequest("TOO_MANY_NOTIFY_EMAILS", fmt.Sprintf("maximum %d extra notification emails allowed", maxNotifyExtraEmails))
|
||||
// Check limit (total includes primary email="" placeholder + extra emails)
|
||||
if len(user.BalanceNotifyExtraEmails) >= maxNotifyEmails {
|
||||
return infraerrors.BadRequest("TOO_MANY_NOTIFY_EMAILS", fmt.Sprintf("maximum %d notification emails allowed", maxNotifyEmails))
|
||||
}
|
||||
|
||||
user.BalanceNotifyExtraEmails = append(user.BalanceNotifyExtraEmails, email)
|
||||
user.BalanceNotifyExtraEmails = append(user.BalanceNotifyExtraEmails, NotifyEmailEntry{
|
||||
Email: email,
|
||||
Disabled: false,
|
||||
Verified: true,
|
||||
})
|
||||
return s.userRepo.Update(ctx, user)
|
||||
}
|
||||
|
||||
@@ -359,9 +363,9 @@ func (s *UserService) RemoveNotifyEmail(ctx context.Context, userID int64, email
|
||||
return err
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(user.BalanceNotifyExtraEmails))
|
||||
filtered := make([]NotifyEmailEntry, 0, len(user.BalanceNotifyExtraEmails))
|
||||
for _, e := range user.BalanceNotifyExtraEmails {
|
||||
if !strings.EqualFold(e, email) {
|
||||
if !strings.EqualFold(e.Email, email) {
|
||||
filtered = append(filtered, e)
|
||||
}
|
||||
}
|
||||
@@ -369,6 +373,28 @@ func (s *UserService) RemoveNotifyEmail(ctx context.Context, userID int64, email
|
||||
return s.userRepo.Update(ctx, user)
|
||||
}
|
||||
|
||||
// ToggleNotifyEmail toggles the disabled state of a notification email entry.
|
||||
func (s *UserService) ToggleNotifyEmail(ctx context.Context, userID int64, email string, disabled bool) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found := false
|
||||
for i, e := range user.BalanceNotifyExtraEmails {
|
||||
if strings.EqualFold(e.Email, email) {
|
||||
user.BalanceNotifyExtraEmails[i].Disabled = disabled
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return infraerrors.BadRequest("EMAIL_NOT_FOUND", "notification email not found")
|
||||
}
|
||||
|
||||
return s.userRepo.Update(ctx, user)
|
||||
}
|
||||
|
||||
// buildNotifyVerifyEmailBody builds the HTML email body for notify email verification.
|
||||
func buildNotifyVerifyEmailBody(code, siteName string) string {
|
||||
return fmt.Sprintf(`
|
||||
|
||||
Reference in New Issue
Block a user