Commit b32d1a2c authored by erio's avatar erio
Browse files

feat(notify): add balance low & account quota notification system

- User balance low notification: email alert when balance drops below
  configurable threshold (user email + verified extra emails)
- Account quota notification: broadcast email to admin-configured
  recipients when daily/weekly/total quota usage exceeds alert threshold
- Admin settings: global enable/disable, default threshold, quota
  notification email list (Email Settings tab)
- User profile: enable/disable, custom threshold, add/remove extra
  notification emails with verification code flow
- Account quota: per-dimension alert toggle and threshold in quota
  control card
- Trigger logic: first-crossing only (old >= threshold && new < threshold
  for balance; old < threshold && new >= threshold for quota), naturally
  prevents duplicate notifications without Redis dedup
parent 60b0fa81
......@@ -68,7 +68,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
promoService := service.NewPromoService(promoCodeRepository, userRepository, billingCacheService, client, apiKeyAuthCacheInvalidator)
subscriptionService := service.NewSubscriptionService(groupRepository, userSubscriptionRepository, billingCacheService, client, configConfig)
authService := service.NewAuthService(client, userRepository, redeemCodeRepository, refreshTokenCache, configConfig, settingService, emailService, turnstileService, emailQueueService, promoService, subscriptionService)
userService := service.NewUserService(userRepository, apiKeyAuthCacheInvalidator, billingCache)
userService := service.NewUserService(userRepository, settingRepository, apiKeyAuthCacheInvalidator, billingCache)
redeemCache := repository.NewRedeemCache(redisClient)
redeemService := service.NewRedeemService(redeemCodeRepository, userRepository, subscriptionService, redeemCache, billingCacheService, client, apiKeyAuthCacheInvalidator)
secretEncryptor, err := repository.NewAESEncryptor(configConfig)
......@@ -78,7 +78,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
totpCache := repository.NewTotpCache(redisClient)
totpService := service.NewTotpService(userRepository, secretEncryptor, totpCache, settingService, emailService, emailQueueService)
authHandler := handler.NewAuthHandler(configConfig, authService, userService, settingService, promoService, redeemService, totpService)
userHandler := handler.NewUserHandler(userService)
userHandler := handler.NewUserHandler(userService, emailService, emailCache)
apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService)
usageLogRepository := repository.NewUsageLogRepository(client, db)
usageService := service.NewUsageService(usageLogRepository, userRepository, client, apiKeyAuthCacheInvalidator)
......@@ -176,9 +176,10 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
channelRepository := repository.NewChannelRepository(db)
channelService := service.NewChannelService(channelRepository, apiKeyAuthCacheInvalidator)
modelPricingResolver := service.NewModelPricingResolver(channelService, billingService)
gatewayService := service.NewGatewayService(accountRepository, groupRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, identityService, httpUpstream, deferredService, claudeTokenProvider, sessionLimitCache, rpmCache, digestSessionStore, settingService, tlsFingerprintProfileService, channelService, modelPricingResolver)
balanceNotifyService := service.ProvideBalanceNotifyService(emailService, settingRepository)
gatewayService := service.NewGatewayService(accountRepository, groupRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, identityService, httpUpstream, deferredService, claudeTokenProvider, sessionLimitCache, rpmCache, digestSessionStore, settingService, tlsFingerprintProfileService, channelService, modelPricingResolver, balanceNotifyService)
openAITokenProvider := service.ProvideOpenAITokenProvider(accountRepository, geminiTokenCache, openAIOAuthService, oAuthRefreshAPI)
openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, httpUpstream, deferredService, openAITokenProvider, modelPricingResolver, channelService)
openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, httpUpstream, deferredService, openAITokenProvider, modelPricingResolver, channelService, balanceNotifyService)
geminiMessagesCompatService := service.NewGeminiMessagesCompatService(accountRepository, groupRepository, gatewayCache, schedulerSnapshotService, geminiTokenProvider, rateLimitService, httpUpstream, antigravityGatewayService, configConfig)
opsSystemLogSink := service.ProvideOpsSystemLogSink(opsRepository)
opsService := service.NewOpsService(opsRepository, settingRepository, configConfig, accountRepository, userRepository, concurrencyService, gatewayService, openAIGatewayService, geminiMessagesCompatService, antigravityGatewayService, opsSystemLogSink)
......
......@@ -1078,6 +1078,9 @@ var (
{Name: "totp_secret_encrypted", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}},
{Name: "totp_enabled", Type: field.TypeBool, Default: false},
{Name: "totp_enabled_at", Type: field.TypeTime, Nullable: true},
{Name: "balance_notify_enabled", Type: field.TypeBool, Default: true},
{Name: "balance_notify_threshold", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
{Name: "balance_notify_extra_emails", Type: field.TypeString, Default: "[]", SchemaType: map[string]string{"postgres": "text"}},
}
// UsersTable holds the schema information for the "users" table.
UsersTable = &schema.Table{
......
......@@ -28210,6 +28210,10 @@ type UserMutation struct {
totp_secret_encrypted *string
totp_enabled *bool
totp_enabled_at *time.Time
balance_notify_enabled *bool
balance_notify_threshold *float64
addbalance_notify_threshold *float64
balance_notify_extra_emails *string
clearedFields map[string]struct{}
api_keys map[int64]struct{}
removedapi_keys map[int64]struct{}
......@@ -28927,6 +28931,148 @@ func (m *UserMutation) ResetTotpEnabledAt() {
delete(m.clearedFields, user.FieldTotpEnabledAt)
}
 
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (m *UserMutation) SetBalanceNotifyEnabled(b bool) {
m.balance_notify_enabled = &b
}
// BalanceNotifyEnabled returns the value of the "balance_notify_enabled" field in the mutation.
func (m *UserMutation) BalanceNotifyEnabled() (r bool, exists bool) {
v := m.balance_notify_enabled
if v == nil {
return
}
return *v, true
}
// OldBalanceNotifyEnabled returns the old "balance_notify_enabled" field's value of the User entity.
// If the User object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldBalanceNotifyEnabled(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldBalanceNotifyEnabled is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldBalanceNotifyEnabled requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldBalanceNotifyEnabled: %w", err)
}
return oldValue.BalanceNotifyEnabled, nil
}
// ResetBalanceNotifyEnabled resets all changes to the "balance_notify_enabled" field.
func (m *UserMutation) ResetBalanceNotifyEnabled() {
m.balance_notify_enabled = nil
}
// SetBalanceNotifyThreshold sets the "balance_notify_threshold" field.
func (m *UserMutation) SetBalanceNotifyThreshold(f float64) {
m.balance_notify_threshold = &f
m.addbalance_notify_threshold = nil
}
// BalanceNotifyThreshold returns the value of the "balance_notify_threshold" field in the mutation.
func (m *UserMutation) BalanceNotifyThreshold() (r float64, exists bool) {
v := m.balance_notify_threshold
if v == nil {
return
}
return *v, true
}
// OldBalanceNotifyThreshold returns the old "balance_notify_threshold" field's value of the User entity.
// If the User object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldBalanceNotifyThreshold(ctx context.Context) (v *float64, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldBalanceNotifyThreshold is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldBalanceNotifyThreshold requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldBalanceNotifyThreshold: %w", err)
}
return oldValue.BalanceNotifyThreshold, nil
}
// AddBalanceNotifyThreshold adds f to the "balance_notify_threshold" field.
func (m *UserMutation) AddBalanceNotifyThreshold(f float64) {
if m.addbalance_notify_threshold != nil {
*m.addbalance_notify_threshold += f
} else {
m.addbalance_notify_threshold = &f
}
}
// AddedBalanceNotifyThreshold returns the value that was added to the "balance_notify_threshold" field in this mutation.
func (m *UserMutation) AddedBalanceNotifyThreshold() (r float64, exists bool) {
v := m.addbalance_notify_threshold
if v == nil {
return
}
return *v, true
}
// ClearBalanceNotifyThreshold clears the value of the "balance_notify_threshold" field.
func (m *UserMutation) ClearBalanceNotifyThreshold() {
m.balance_notify_threshold = nil
m.addbalance_notify_threshold = nil
m.clearedFields[user.FieldBalanceNotifyThreshold] = struct{}{}
}
// BalanceNotifyThresholdCleared returns if the "balance_notify_threshold" field was cleared in this mutation.
func (m *UserMutation) BalanceNotifyThresholdCleared() bool {
_, ok := m.clearedFields[user.FieldBalanceNotifyThreshold]
return ok
}
// ResetBalanceNotifyThreshold resets all changes to the "balance_notify_threshold" field.
func (m *UserMutation) ResetBalanceNotifyThreshold() {
m.balance_notify_threshold = nil
m.addbalance_notify_threshold = nil
delete(m.clearedFields, user.FieldBalanceNotifyThreshold)
}
// SetBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field.
func (m *UserMutation) SetBalanceNotifyExtraEmails(s string) {
m.balance_notify_extra_emails = &s
}
// BalanceNotifyExtraEmails returns the value of the "balance_notify_extra_emails" field in the mutation.
func (m *UserMutation) BalanceNotifyExtraEmails() (r string, exists bool) {
v := m.balance_notify_extra_emails
if v == nil {
return
}
return *v, true
}
// OldBalanceNotifyExtraEmails returns the old "balance_notify_extra_emails" field's value of the User entity.
// If the User object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldBalanceNotifyExtraEmails(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldBalanceNotifyExtraEmails is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldBalanceNotifyExtraEmails requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldBalanceNotifyExtraEmails: %w", err)
}
return oldValue.BalanceNotifyExtraEmails, nil
}
// ResetBalanceNotifyExtraEmails resets all changes to the "balance_notify_extra_emails" field.
func (m *UserMutation) ResetBalanceNotifyExtraEmails() {
m.balance_notify_extra_emails = nil
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by ids.
func (m *UserMutation) AddAPIKeyIDs(ids ...int64) {
if m.api_keys == nil {
......@@ -29501,7 +29647,7 @@ func (m *UserMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *UserMutation) Fields() []string {
fields := make([]string, 0, 14)
fields := make([]string, 0, 17)
if m.created_at != nil {
fields = append(fields, user.FieldCreatedAt)
}
......@@ -29544,6 +29690,15 @@ func (m *UserMutation) Fields() []string {
if m.totp_enabled_at != nil {
fields = append(fields, user.FieldTotpEnabledAt)
}
if m.balance_notify_enabled != nil {
fields = append(fields, user.FieldBalanceNotifyEnabled)
}
if m.balance_notify_threshold != nil {
fields = append(fields, user.FieldBalanceNotifyThreshold)
}
if m.balance_notify_extra_emails != nil {
fields = append(fields, user.FieldBalanceNotifyExtraEmails)
}
return fields
}
 
......@@ -29580,6 +29735,12 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) {
return m.TotpEnabled()
case user.FieldTotpEnabledAt:
return m.TotpEnabledAt()
case user.FieldBalanceNotifyEnabled:
return m.BalanceNotifyEnabled()
case user.FieldBalanceNotifyThreshold:
return m.BalanceNotifyThreshold()
case user.FieldBalanceNotifyExtraEmails:
return m.BalanceNotifyExtraEmails()
}
return nil, false
}
......@@ -29617,6 +29778,12 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er
return m.OldTotpEnabled(ctx)
case user.FieldTotpEnabledAt:
return m.OldTotpEnabledAt(ctx)
case user.FieldBalanceNotifyEnabled:
return m.OldBalanceNotifyEnabled(ctx)
case user.FieldBalanceNotifyThreshold:
return m.OldBalanceNotifyThreshold(ctx)
case user.FieldBalanceNotifyExtraEmails:
return m.OldBalanceNotifyExtraEmails(ctx)
}
return nil, fmt.Errorf("unknown User field %s", name)
}
......@@ -29724,6 +29891,27 @@ func (m *UserMutation) SetField(name string, value ent.Value) error {
}
m.SetTotpEnabledAt(v)
return nil
case user.FieldBalanceNotifyEnabled:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetBalanceNotifyEnabled(v)
return nil
case user.FieldBalanceNotifyThreshold:
v, ok := value.(float64)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetBalanceNotifyThreshold(v)
return nil
case user.FieldBalanceNotifyExtraEmails:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetBalanceNotifyExtraEmails(v)
return nil
}
return fmt.Errorf("unknown User field %s", name)
}
......@@ -29738,6 +29926,9 @@ func (m *UserMutation) AddedFields() []string {
if m.addconcurrency != nil {
fields = append(fields, user.FieldConcurrency)
}
if m.addbalance_notify_threshold != nil {
fields = append(fields, user.FieldBalanceNotifyThreshold)
}
return fields
}
 
......@@ -29750,6 +29941,8 @@ func (m *UserMutation) AddedField(name string) (ent.Value, bool) {
return m.AddedBalance()
case user.FieldConcurrency:
return m.AddedConcurrency()
case user.FieldBalanceNotifyThreshold:
return m.AddedBalanceNotifyThreshold()
}
return nil, false
}
......@@ -29773,6 +29966,13 @@ func (m *UserMutation) AddField(name string, value ent.Value) error {
}
m.AddConcurrency(v)
return nil
case user.FieldBalanceNotifyThreshold:
v, ok := value.(float64)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.AddBalanceNotifyThreshold(v)
return nil
}
return fmt.Errorf("unknown User numeric field %s", name)
}
......@@ -29790,6 +29990,9 @@ func (m *UserMutation) ClearedFields() []string {
if m.FieldCleared(user.FieldTotpEnabledAt) {
fields = append(fields, user.FieldTotpEnabledAt)
}
if m.FieldCleared(user.FieldBalanceNotifyThreshold) {
fields = append(fields, user.FieldBalanceNotifyThreshold)
}
return fields
}
 
......@@ -29813,6 +30016,9 @@ func (m *UserMutation) ClearField(name string) error {
case user.FieldTotpEnabledAt:
m.ClearTotpEnabledAt()
return nil
case user.FieldBalanceNotifyThreshold:
m.ClearBalanceNotifyThreshold()
return nil
}
return fmt.Errorf("unknown User nullable field %s", name)
}
......@@ -29863,6 +30069,15 @@ func (m *UserMutation) ResetField(name string) error {
case user.FieldTotpEnabledAt:
m.ResetTotpEnabledAt()
return nil
case user.FieldBalanceNotifyEnabled:
m.ResetBalanceNotifyEnabled()
return nil
case user.FieldBalanceNotifyThreshold:
m.ResetBalanceNotifyThreshold()
return nil
case user.FieldBalanceNotifyExtraEmails:
m.ResetBalanceNotifyExtraEmails()
return nil
}
return fmt.Errorf("unknown User field %s", name)
}
......@@ -1293,6 +1293,14 @@ func init() {
userDescTotpEnabled := userFields[9].Descriptor()
// user.DefaultTotpEnabled holds the default value on creation for the totp_enabled field.
user.DefaultTotpEnabled = userDescTotpEnabled.Default.(bool)
// userDescBalanceNotifyEnabled is the schema descriptor for balance_notify_enabled field.
userDescBalanceNotifyEnabled := userFields[11].Descriptor()
// user.DefaultBalanceNotifyEnabled holds the default value on creation for the balance_notify_enabled field.
user.DefaultBalanceNotifyEnabled = userDescBalanceNotifyEnabled.Default.(bool)
// userDescBalanceNotifyExtraEmails is the schema descriptor for balance_notify_extra_emails field.
userDescBalanceNotifyExtraEmails := userFields[13].Descriptor()
// user.DefaultBalanceNotifyExtraEmails holds the default value on creation for the balance_notify_extra_emails field.
user.DefaultBalanceNotifyExtraEmails = userDescBalanceNotifyExtraEmails.Default.(string)
userallowedgroupFields := schema.UserAllowedGroup{}.Fields()
_ = userallowedgroupFields
// userallowedgroupDescCreatedAt is the schema descriptor for created_at field.
......
......@@ -72,6 +72,17 @@ func (User) Fields() []ent.Field {
field.Time("totp_enabled_at").
Optional().
Nillable(),
// 余额不足通知
field.Bool("balance_notify_enabled").
Default(true),
field.Float("balance_notify_threshold").
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
Optional().
Nillable(),
field.String("balance_notify_extra_emails").
SchemaType(map[string]string{dialect.Postgres: "text"}).
Default("[]"),
}
}
......
......@@ -45,6 +45,12 @@ type User struct {
TotpEnabled bool `json:"totp_enabled,omitempty"`
// TotpEnabledAt holds the value of the "totp_enabled_at" field.
TotpEnabledAt *time.Time `json:"totp_enabled_at,omitempty"`
// BalanceNotifyEnabled holds the value of the "balance_notify_enabled" field.
BalanceNotifyEnabled bool `json:"balance_notify_enabled,omitempty"`
// BalanceNotifyThreshold holds the value of the "balance_notify_threshold" field.
BalanceNotifyThreshold *float64 `json:"balance_notify_threshold,omitempty"`
// BalanceNotifyExtraEmails holds the value of the "balance_notify_extra_emails" field.
BalanceNotifyExtraEmails string `json:"balance_notify_extra_emails,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the UserQuery when eager-loading is set.
Edges UserEdges `json:"edges"`
......@@ -184,13 +190,13 @@ func (*User) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case user.FieldTotpEnabled:
case user.FieldTotpEnabled, user.FieldBalanceNotifyEnabled:
values[i] = new(sql.NullBool)
case user.FieldBalance:
case user.FieldBalance, user.FieldBalanceNotifyThreshold:
values[i] = new(sql.NullFloat64)
case user.FieldID, user.FieldConcurrency:
values[i] = new(sql.NullInt64)
case user.FieldEmail, user.FieldPasswordHash, user.FieldRole, user.FieldStatus, user.FieldUsername, user.FieldNotes, user.FieldTotpSecretEncrypted:
case user.FieldEmail, user.FieldPasswordHash, user.FieldRole, user.FieldStatus, user.FieldUsername, user.FieldNotes, user.FieldTotpSecretEncrypted, user.FieldBalanceNotifyExtraEmails:
values[i] = new(sql.NullString)
case user.FieldCreatedAt, user.FieldUpdatedAt, user.FieldDeletedAt, user.FieldTotpEnabledAt:
values[i] = new(sql.NullTime)
......@@ -302,6 +308,25 @@ func (_m *User) assignValues(columns []string, values []any) error {
_m.TotpEnabledAt = new(time.Time)
*_m.TotpEnabledAt = value.Time
}
case user.FieldBalanceNotifyEnabled:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field balance_notify_enabled", values[i])
} else if value.Valid {
_m.BalanceNotifyEnabled = value.Bool
}
case user.FieldBalanceNotifyThreshold:
if value, ok := values[i].(*sql.NullFloat64); !ok {
return fmt.Errorf("unexpected type %T for field balance_notify_threshold", values[i])
} else if value.Valid {
_m.BalanceNotifyThreshold = new(float64)
*_m.BalanceNotifyThreshold = value.Float64
}
case user.FieldBalanceNotifyExtraEmails:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field balance_notify_extra_emails", values[i])
} else if value.Valid {
_m.BalanceNotifyExtraEmails = value.String
}
default:
_m.selectValues.Set(columns[i], values[i])
}
......@@ -440,6 +465,17 @@ func (_m *User) String() string {
builder.WriteString("totp_enabled_at=")
builder.WriteString(v.Format(time.ANSIC))
}
builder.WriteString(", ")
builder.WriteString("balance_notify_enabled=")
builder.WriteString(fmt.Sprintf("%v", _m.BalanceNotifyEnabled))
builder.WriteString(", ")
if v := _m.BalanceNotifyThreshold; v != nil {
builder.WriteString("balance_notify_threshold=")
builder.WriteString(fmt.Sprintf("%v", *v))
}
builder.WriteString(", ")
builder.WriteString("balance_notify_extra_emails=")
builder.WriteString(_m.BalanceNotifyExtraEmails)
builder.WriteByte(')')
return builder.String()
}
......
......@@ -43,6 +43,12 @@ const (
FieldTotpEnabled = "totp_enabled"
// FieldTotpEnabledAt holds the string denoting the totp_enabled_at field in the database.
FieldTotpEnabledAt = "totp_enabled_at"
// FieldBalanceNotifyEnabled holds the string denoting the balance_notify_enabled field in the database.
FieldBalanceNotifyEnabled = "balance_notify_enabled"
// FieldBalanceNotifyThreshold holds the string denoting the balance_notify_threshold field in the database.
FieldBalanceNotifyThreshold = "balance_notify_threshold"
// FieldBalanceNotifyExtraEmails holds the string denoting the balance_notify_extra_emails field in the database.
FieldBalanceNotifyExtraEmails = "balance_notify_extra_emails"
// EdgeAPIKeys holds the string denoting the api_keys edge name in mutations.
EdgeAPIKeys = "api_keys"
// EdgeRedeemCodes holds the string denoting the redeem_codes edge name in mutations.
......@@ -161,6 +167,9 @@ var Columns = []string{
FieldTotpSecretEncrypted,
FieldTotpEnabled,
FieldTotpEnabledAt,
FieldBalanceNotifyEnabled,
FieldBalanceNotifyThreshold,
FieldBalanceNotifyExtraEmails,
}
var (
......@@ -217,6 +226,10 @@ var (
DefaultNotes string
// DefaultTotpEnabled holds the default value on creation for the "totp_enabled" field.
DefaultTotpEnabled bool
// DefaultBalanceNotifyEnabled holds the default value on creation for the "balance_notify_enabled" field.
DefaultBalanceNotifyEnabled bool
// DefaultBalanceNotifyExtraEmails holds the default value on creation for the "balance_notify_extra_emails" field.
DefaultBalanceNotifyExtraEmails string
)
// OrderOption defines the ordering options for the User queries.
......@@ -297,6 +310,21 @@ func ByTotpEnabledAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldTotpEnabledAt, opts...).ToFunc()
}
// ByBalanceNotifyEnabled orders the results by the balance_notify_enabled field.
func ByBalanceNotifyEnabled(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldBalanceNotifyEnabled, opts...).ToFunc()
}
// ByBalanceNotifyThreshold orders the results by the balance_notify_threshold field.
func ByBalanceNotifyThreshold(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldBalanceNotifyThreshold, opts...).ToFunc()
}
// ByBalanceNotifyExtraEmails orders the results by the balance_notify_extra_emails field.
func ByBalanceNotifyExtraEmails(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldBalanceNotifyExtraEmails, opts...).ToFunc()
}
// ByAPIKeysCount orders the results by api_keys count.
func ByAPIKeysCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
......
......@@ -125,6 +125,21 @@ func TotpEnabledAt(v time.Time) predicate.User {
return predicate.User(sql.FieldEQ(FieldTotpEnabledAt, v))
}
// BalanceNotifyEnabled applies equality check predicate on the "balance_notify_enabled" field. It's identical to BalanceNotifyEnabledEQ.
func BalanceNotifyEnabled(v bool) predicate.User {
return predicate.User(sql.FieldEQ(FieldBalanceNotifyEnabled, v))
}
// BalanceNotifyThreshold applies equality check predicate on the "balance_notify_threshold" field. It's identical to BalanceNotifyThresholdEQ.
func BalanceNotifyThreshold(v float64) predicate.User {
return predicate.User(sql.FieldEQ(FieldBalanceNotifyThreshold, v))
}
// BalanceNotifyExtraEmails applies equality check predicate on the "balance_notify_extra_emails" field. It's identical to BalanceNotifyExtraEmailsEQ.
func BalanceNotifyExtraEmails(v string) predicate.User {
return predicate.User(sql.FieldEQ(FieldBalanceNotifyExtraEmails, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.User {
return predicate.User(sql.FieldEQ(FieldCreatedAt, v))
......@@ -860,6 +875,131 @@ func TotpEnabledAtNotNil() predicate.User {
return predicate.User(sql.FieldNotNull(FieldTotpEnabledAt))
}
// BalanceNotifyEnabledEQ applies the EQ predicate on the "balance_notify_enabled" field.
func BalanceNotifyEnabledEQ(v bool) predicate.User {
return predicate.User(sql.FieldEQ(FieldBalanceNotifyEnabled, v))
}
// BalanceNotifyEnabledNEQ applies the NEQ predicate on the "balance_notify_enabled" field.
func BalanceNotifyEnabledNEQ(v bool) predicate.User {
return predicate.User(sql.FieldNEQ(FieldBalanceNotifyEnabled, v))
}
// BalanceNotifyThresholdEQ applies the EQ predicate on the "balance_notify_threshold" field.
func BalanceNotifyThresholdEQ(v float64) predicate.User {
return predicate.User(sql.FieldEQ(FieldBalanceNotifyThreshold, v))
}
// BalanceNotifyThresholdNEQ applies the NEQ predicate on the "balance_notify_threshold" field.
func BalanceNotifyThresholdNEQ(v float64) predicate.User {
return predicate.User(sql.FieldNEQ(FieldBalanceNotifyThreshold, v))
}
// BalanceNotifyThresholdIn applies the In predicate on the "balance_notify_threshold" field.
func BalanceNotifyThresholdIn(vs ...float64) predicate.User {
return predicate.User(sql.FieldIn(FieldBalanceNotifyThreshold, vs...))
}
// BalanceNotifyThresholdNotIn applies the NotIn predicate on the "balance_notify_threshold" field.
func BalanceNotifyThresholdNotIn(vs ...float64) predicate.User {
return predicate.User(sql.FieldNotIn(FieldBalanceNotifyThreshold, vs...))
}
// BalanceNotifyThresholdGT applies the GT predicate on the "balance_notify_threshold" field.
func BalanceNotifyThresholdGT(v float64) predicate.User {
return predicate.User(sql.FieldGT(FieldBalanceNotifyThreshold, v))
}
// BalanceNotifyThresholdGTE applies the GTE predicate on the "balance_notify_threshold" field.
func BalanceNotifyThresholdGTE(v float64) predicate.User {
return predicate.User(sql.FieldGTE(FieldBalanceNotifyThreshold, v))
}
// BalanceNotifyThresholdLT applies the LT predicate on the "balance_notify_threshold" field.
func BalanceNotifyThresholdLT(v float64) predicate.User {
return predicate.User(sql.FieldLT(FieldBalanceNotifyThreshold, v))
}
// BalanceNotifyThresholdLTE applies the LTE predicate on the "balance_notify_threshold" field.
func BalanceNotifyThresholdLTE(v float64) predicate.User {
return predicate.User(sql.FieldLTE(FieldBalanceNotifyThreshold, v))
}
// BalanceNotifyThresholdIsNil applies the IsNil predicate on the "balance_notify_threshold" field.
func BalanceNotifyThresholdIsNil() predicate.User {
return predicate.User(sql.FieldIsNull(FieldBalanceNotifyThreshold))
}
// BalanceNotifyThresholdNotNil applies the NotNil predicate on the "balance_notify_threshold" field.
func BalanceNotifyThresholdNotNil() predicate.User {
return predicate.User(sql.FieldNotNull(FieldBalanceNotifyThreshold))
}
// BalanceNotifyExtraEmailsEQ applies the EQ predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsEQ(v string) predicate.User {
return predicate.User(sql.FieldEQ(FieldBalanceNotifyExtraEmails, v))
}
// BalanceNotifyExtraEmailsNEQ applies the NEQ predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsNEQ(v string) predicate.User {
return predicate.User(sql.FieldNEQ(FieldBalanceNotifyExtraEmails, v))
}
// BalanceNotifyExtraEmailsIn applies the In predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsIn(vs ...string) predicate.User {
return predicate.User(sql.FieldIn(FieldBalanceNotifyExtraEmails, vs...))
}
// BalanceNotifyExtraEmailsNotIn applies the NotIn predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsNotIn(vs ...string) predicate.User {
return predicate.User(sql.FieldNotIn(FieldBalanceNotifyExtraEmails, vs...))
}
// BalanceNotifyExtraEmailsGT applies the GT predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsGT(v string) predicate.User {
return predicate.User(sql.FieldGT(FieldBalanceNotifyExtraEmails, v))
}
// BalanceNotifyExtraEmailsGTE applies the GTE predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsGTE(v string) predicate.User {
return predicate.User(sql.FieldGTE(FieldBalanceNotifyExtraEmails, v))
}
// BalanceNotifyExtraEmailsLT applies the LT predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsLT(v string) predicate.User {
return predicate.User(sql.FieldLT(FieldBalanceNotifyExtraEmails, v))
}
// BalanceNotifyExtraEmailsLTE applies the LTE predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsLTE(v string) predicate.User {
return predicate.User(sql.FieldLTE(FieldBalanceNotifyExtraEmails, v))
}
// BalanceNotifyExtraEmailsContains applies the Contains predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsContains(v string) predicate.User {
return predicate.User(sql.FieldContains(FieldBalanceNotifyExtraEmails, v))
}
// BalanceNotifyExtraEmailsHasPrefix applies the HasPrefix predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsHasPrefix(v string) predicate.User {
return predicate.User(sql.FieldHasPrefix(FieldBalanceNotifyExtraEmails, v))
}
// BalanceNotifyExtraEmailsHasSuffix applies the HasSuffix predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsHasSuffix(v string) predicate.User {
return predicate.User(sql.FieldHasSuffix(FieldBalanceNotifyExtraEmails, v))
}
// BalanceNotifyExtraEmailsEqualFold applies the EqualFold predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsEqualFold(v string) predicate.User {
return predicate.User(sql.FieldEqualFold(FieldBalanceNotifyExtraEmails, v))
}
// BalanceNotifyExtraEmailsContainsFold applies the ContainsFold predicate on the "balance_notify_extra_emails" field.
func BalanceNotifyExtraEmailsContainsFold(v string) predicate.User {
return predicate.User(sql.FieldContainsFold(FieldBalanceNotifyExtraEmails, v))
}
// HasAPIKeys applies the HasEdge predicate on the "api_keys" edge.
func HasAPIKeys() predicate.User {
return predicate.User(func(s *sql.Selector) {
......
......@@ -211,6 +211,48 @@ func (_c *UserCreate) SetNillableTotpEnabledAt(v *time.Time) *UserCreate {
return _c
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (_c *UserCreate) SetBalanceNotifyEnabled(v bool) *UserCreate {
_c.mutation.SetBalanceNotifyEnabled(v)
return _c
}
// SetNillableBalanceNotifyEnabled sets the "balance_notify_enabled" field if the given value is not nil.
func (_c *UserCreate) SetNillableBalanceNotifyEnabled(v *bool) *UserCreate {
if v != nil {
_c.SetBalanceNotifyEnabled(*v)
}
return _c
}
// SetBalanceNotifyThreshold sets the "balance_notify_threshold" field.
func (_c *UserCreate) SetBalanceNotifyThreshold(v float64) *UserCreate {
_c.mutation.SetBalanceNotifyThreshold(v)
return _c
}
// SetNillableBalanceNotifyThreshold sets the "balance_notify_threshold" field if the given value is not nil.
func (_c *UserCreate) SetNillableBalanceNotifyThreshold(v *float64) *UserCreate {
if v != nil {
_c.SetBalanceNotifyThreshold(*v)
}
return _c
}
// SetBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field.
func (_c *UserCreate) SetBalanceNotifyExtraEmails(v string) *UserCreate {
_c.mutation.SetBalanceNotifyExtraEmails(v)
return _c
}
// SetNillableBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field if the given value is not nil.
func (_c *UserCreate) SetNillableBalanceNotifyExtraEmails(v *string) *UserCreate {
if v != nil {
_c.SetBalanceNotifyExtraEmails(*v)
}
return _c
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func (_c *UserCreate) AddAPIKeyIDs(ids ...int64) *UserCreate {
_c.mutation.AddAPIKeyIDs(ids...)
......@@ -440,6 +482,14 @@ func (_c *UserCreate) defaults() error {
v := user.DefaultTotpEnabled
_c.mutation.SetTotpEnabled(v)
}
if _, ok := _c.mutation.BalanceNotifyEnabled(); !ok {
v := user.DefaultBalanceNotifyEnabled
_c.mutation.SetBalanceNotifyEnabled(v)
}
if _, ok := _c.mutation.BalanceNotifyExtraEmails(); !ok {
v := user.DefaultBalanceNotifyExtraEmails
_c.mutation.SetBalanceNotifyExtraEmails(v)
}
return nil
}
......@@ -503,6 +553,12 @@ func (_c *UserCreate) check() error {
if _, ok := _c.mutation.TotpEnabled(); !ok {
return &ValidationError{Name: "totp_enabled", err: errors.New(`ent: missing required field "User.totp_enabled"`)}
}
if _, ok := _c.mutation.BalanceNotifyEnabled(); !ok {
return &ValidationError{Name: "balance_notify_enabled", err: errors.New(`ent: missing required field "User.balance_notify_enabled"`)}
}
if _, ok := _c.mutation.BalanceNotifyExtraEmails(); !ok {
return &ValidationError{Name: "balance_notify_extra_emails", err: errors.New(`ent: missing required field "User.balance_notify_extra_emails"`)}
}
return nil
}
......@@ -586,6 +642,18 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
_spec.SetField(user.FieldTotpEnabledAt, field.TypeTime, value)
_node.TotpEnabledAt = &value
}
if value, ok := _c.mutation.BalanceNotifyEnabled(); ok {
_spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value)
_node.BalanceNotifyEnabled = value
}
if value, ok := _c.mutation.BalanceNotifyThreshold(); ok {
_spec.SetField(user.FieldBalanceNotifyThreshold, field.TypeFloat64, value)
_node.BalanceNotifyThreshold = &value
}
if value, ok := _c.mutation.BalanceNotifyExtraEmails(); ok {
_spec.SetField(user.FieldBalanceNotifyExtraEmails, field.TypeString, value)
_node.BalanceNotifyExtraEmails = value
}
if nodes := _c.mutation.APIKeysIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
......@@ -988,6 +1056,54 @@ func (u *UserUpsert) ClearTotpEnabledAt() *UserUpsert {
return u
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (u *UserUpsert) SetBalanceNotifyEnabled(v bool) *UserUpsert {
u.Set(user.FieldBalanceNotifyEnabled, v)
return u
}
// UpdateBalanceNotifyEnabled sets the "balance_notify_enabled" field to the value that was provided on create.
func (u *UserUpsert) UpdateBalanceNotifyEnabled() *UserUpsert {
u.SetExcluded(user.FieldBalanceNotifyEnabled)
return u
}
// SetBalanceNotifyThreshold sets the "balance_notify_threshold" field.
func (u *UserUpsert) SetBalanceNotifyThreshold(v float64) *UserUpsert {
u.Set(user.FieldBalanceNotifyThreshold, v)
return u
}
// UpdateBalanceNotifyThreshold sets the "balance_notify_threshold" field to the value that was provided on create.
func (u *UserUpsert) UpdateBalanceNotifyThreshold() *UserUpsert {
u.SetExcluded(user.FieldBalanceNotifyThreshold)
return u
}
// AddBalanceNotifyThreshold adds v to the "balance_notify_threshold" field.
func (u *UserUpsert) AddBalanceNotifyThreshold(v float64) *UserUpsert {
u.Add(user.FieldBalanceNotifyThreshold, v)
return u
}
// ClearBalanceNotifyThreshold clears the value of the "balance_notify_threshold" field.
func (u *UserUpsert) ClearBalanceNotifyThreshold() *UserUpsert {
u.SetNull(user.FieldBalanceNotifyThreshold)
return u
}
// SetBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field.
func (u *UserUpsert) SetBalanceNotifyExtraEmails(v string) *UserUpsert {
u.Set(user.FieldBalanceNotifyExtraEmails, v)
return u
}
// UpdateBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field to the value that was provided on create.
func (u *UserUpsert) UpdateBalanceNotifyExtraEmails() *UserUpsert {
u.SetExcluded(user.FieldBalanceNotifyExtraEmails)
return u
}
// UpdateNewValues updates the mutable fields using the new values that were set on create.
// Using this option is equivalent to using:
//
......@@ -1250,6 +1366,62 @@ func (u *UserUpsertOne) ClearTotpEnabledAt() *UserUpsertOne {
})
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (u *UserUpsertOne) SetBalanceNotifyEnabled(v bool) *UserUpsertOne {
return u.Update(func(s *UserUpsert) {
s.SetBalanceNotifyEnabled(v)
})
}
// UpdateBalanceNotifyEnabled sets the "balance_notify_enabled" field to the value that was provided on create.
func (u *UserUpsertOne) UpdateBalanceNotifyEnabled() *UserUpsertOne {
return u.Update(func(s *UserUpsert) {
s.UpdateBalanceNotifyEnabled()
})
}
// SetBalanceNotifyThreshold sets the "balance_notify_threshold" field.
func (u *UserUpsertOne) SetBalanceNotifyThreshold(v float64) *UserUpsertOne {
return u.Update(func(s *UserUpsert) {
s.SetBalanceNotifyThreshold(v)
})
}
// AddBalanceNotifyThreshold adds v to the "balance_notify_threshold" field.
func (u *UserUpsertOne) AddBalanceNotifyThreshold(v float64) *UserUpsertOne {
return u.Update(func(s *UserUpsert) {
s.AddBalanceNotifyThreshold(v)
})
}
// UpdateBalanceNotifyThreshold sets the "balance_notify_threshold" field to the value that was provided on create.
func (u *UserUpsertOne) UpdateBalanceNotifyThreshold() *UserUpsertOne {
return u.Update(func(s *UserUpsert) {
s.UpdateBalanceNotifyThreshold()
})
}
// ClearBalanceNotifyThreshold clears the value of the "balance_notify_threshold" field.
func (u *UserUpsertOne) ClearBalanceNotifyThreshold() *UserUpsertOne {
return u.Update(func(s *UserUpsert) {
s.ClearBalanceNotifyThreshold()
})
}
// SetBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field.
func (u *UserUpsertOne) SetBalanceNotifyExtraEmails(v string) *UserUpsertOne {
return u.Update(func(s *UserUpsert) {
s.SetBalanceNotifyExtraEmails(v)
})
}
// UpdateBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field to the value that was provided on create.
func (u *UserUpsertOne) UpdateBalanceNotifyExtraEmails() *UserUpsertOne {
return u.Update(func(s *UserUpsert) {
s.UpdateBalanceNotifyExtraEmails()
})
}
// Exec executes the query.
func (u *UserUpsertOne) Exec(ctx context.Context) error {
if len(u.create.conflict) == 0 {
......@@ -1678,6 +1850,62 @@ func (u *UserUpsertBulk) ClearTotpEnabledAt() *UserUpsertBulk {
})
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (u *UserUpsertBulk) SetBalanceNotifyEnabled(v bool) *UserUpsertBulk {
return u.Update(func(s *UserUpsert) {
s.SetBalanceNotifyEnabled(v)
})
}
// UpdateBalanceNotifyEnabled sets the "balance_notify_enabled" field to the value that was provided on create.
func (u *UserUpsertBulk) UpdateBalanceNotifyEnabled() *UserUpsertBulk {
return u.Update(func(s *UserUpsert) {
s.UpdateBalanceNotifyEnabled()
})
}
// SetBalanceNotifyThreshold sets the "balance_notify_threshold" field.
func (u *UserUpsertBulk) SetBalanceNotifyThreshold(v float64) *UserUpsertBulk {
return u.Update(func(s *UserUpsert) {
s.SetBalanceNotifyThreshold(v)
})
}
// AddBalanceNotifyThreshold adds v to the "balance_notify_threshold" field.
func (u *UserUpsertBulk) AddBalanceNotifyThreshold(v float64) *UserUpsertBulk {
return u.Update(func(s *UserUpsert) {
s.AddBalanceNotifyThreshold(v)
})
}
// UpdateBalanceNotifyThreshold sets the "balance_notify_threshold" field to the value that was provided on create.
func (u *UserUpsertBulk) UpdateBalanceNotifyThreshold() *UserUpsertBulk {
return u.Update(func(s *UserUpsert) {
s.UpdateBalanceNotifyThreshold()
})
}
// ClearBalanceNotifyThreshold clears the value of the "balance_notify_threshold" field.
func (u *UserUpsertBulk) ClearBalanceNotifyThreshold() *UserUpsertBulk {
return u.Update(func(s *UserUpsert) {
s.ClearBalanceNotifyThreshold()
})
}
// SetBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field.
func (u *UserUpsertBulk) SetBalanceNotifyExtraEmails(v string) *UserUpsertBulk {
return u.Update(func(s *UserUpsert) {
s.SetBalanceNotifyExtraEmails(v)
})
}
// UpdateBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field to the value that was provided on create.
func (u *UserUpsertBulk) UpdateBalanceNotifyExtraEmails() *UserUpsertBulk {
return u.Update(func(s *UserUpsert) {
s.UpdateBalanceNotifyExtraEmails()
})
}
// Exec executes the query.
func (u *UserUpsertBulk) Exec(ctx context.Context) error {
if u.create.err != nil {
......
......@@ -243,6 +243,61 @@ func (_u *UserUpdate) ClearTotpEnabledAt() *UserUpdate {
return _u
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (_u *UserUpdate) SetBalanceNotifyEnabled(v bool) *UserUpdate {
_u.mutation.SetBalanceNotifyEnabled(v)
return _u
}
// SetNillableBalanceNotifyEnabled sets the "balance_notify_enabled" field if the given value is not nil.
func (_u *UserUpdate) SetNillableBalanceNotifyEnabled(v *bool) *UserUpdate {
if v != nil {
_u.SetBalanceNotifyEnabled(*v)
}
return _u
}
// SetBalanceNotifyThreshold sets the "balance_notify_threshold" field.
func (_u *UserUpdate) SetBalanceNotifyThreshold(v float64) *UserUpdate {
_u.mutation.ResetBalanceNotifyThreshold()
_u.mutation.SetBalanceNotifyThreshold(v)
return _u
}
// SetNillableBalanceNotifyThreshold sets the "balance_notify_threshold" field if the given value is not nil.
func (_u *UserUpdate) SetNillableBalanceNotifyThreshold(v *float64) *UserUpdate {
if v != nil {
_u.SetBalanceNotifyThreshold(*v)
}
return _u
}
// AddBalanceNotifyThreshold adds value to the "balance_notify_threshold" field.
func (_u *UserUpdate) AddBalanceNotifyThreshold(v float64) *UserUpdate {
_u.mutation.AddBalanceNotifyThreshold(v)
return _u
}
// ClearBalanceNotifyThreshold clears the value of the "balance_notify_threshold" field.
func (_u *UserUpdate) ClearBalanceNotifyThreshold() *UserUpdate {
_u.mutation.ClearBalanceNotifyThreshold()
return _u
}
// SetBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field.
func (_u *UserUpdate) SetBalanceNotifyExtraEmails(v string) *UserUpdate {
_u.mutation.SetBalanceNotifyExtraEmails(v)
return _u
}
// SetNillableBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field if the given value is not nil.
func (_u *UserUpdate) SetNillableBalanceNotifyExtraEmails(v *string) *UserUpdate {
if v != nil {
_u.SetBalanceNotifyExtraEmails(*v)
}
return _u
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func (_u *UserUpdate) AddAPIKeyIDs(ids ...int64) *UserUpdate {
_u.mutation.AddAPIKeyIDs(ids...)
......@@ -746,6 +801,21 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if _u.mutation.TotpEnabledAtCleared() {
_spec.ClearField(user.FieldTotpEnabledAt, field.TypeTime)
}
if value, ok := _u.mutation.BalanceNotifyEnabled(); ok {
_spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value)
}
if value, ok := _u.mutation.BalanceNotifyThreshold(); ok {
_spec.SetField(user.FieldBalanceNotifyThreshold, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedBalanceNotifyThreshold(); ok {
_spec.AddField(user.FieldBalanceNotifyThreshold, field.TypeFloat64, value)
}
if _u.mutation.BalanceNotifyThresholdCleared() {
_spec.ClearField(user.FieldBalanceNotifyThreshold, field.TypeFloat64)
}
if value, ok := _u.mutation.BalanceNotifyExtraEmails(); ok {
_spec.SetField(user.FieldBalanceNotifyExtraEmails, field.TypeString, value)
}
if _u.mutation.APIKeysCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
......@@ -1434,6 +1504,61 @@ func (_u *UserUpdateOne) ClearTotpEnabledAt() *UserUpdateOne {
return _u
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (_u *UserUpdateOne) SetBalanceNotifyEnabled(v bool) *UserUpdateOne {
_u.mutation.SetBalanceNotifyEnabled(v)
return _u
}
// SetNillableBalanceNotifyEnabled sets the "balance_notify_enabled" field if the given value is not nil.
func (_u *UserUpdateOne) SetNillableBalanceNotifyEnabled(v *bool) *UserUpdateOne {
if v != nil {
_u.SetBalanceNotifyEnabled(*v)
}
return _u
}
// SetBalanceNotifyThreshold sets the "balance_notify_threshold" field.
func (_u *UserUpdateOne) SetBalanceNotifyThreshold(v float64) *UserUpdateOne {
_u.mutation.ResetBalanceNotifyThreshold()
_u.mutation.SetBalanceNotifyThreshold(v)
return _u
}
// SetNillableBalanceNotifyThreshold sets the "balance_notify_threshold" field if the given value is not nil.
func (_u *UserUpdateOne) SetNillableBalanceNotifyThreshold(v *float64) *UserUpdateOne {
if v != nil {
_u.SetBalanceNotifyThreshold(*v)
}
return _u
}
// AddBalanceNotifyThreshold adds value to the "balance_notify_threshold" field.
func (_u *UserUpdateOne) AddBalanceNotifyThreshold(v float64) *UserUpdateOne {
_u.mutation.AddBalanceNotifyThreshold(v)
return _u
}
// ClearBalanceNotifyThreshold clears the value of the "balance_notify_threshold" field.
func (_u *UserUpdateOne) ClearBalanceNotifyThreshold() *UserUpdateOne {
_u.mutation.ClearBalanceNotifyThreshold()
return _u
}
// SetBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field.
func (_u *UserUpdateOne) SetBalanceNotifyExtraEmails(v string) *UserUpdateOne {
_u.mutation.SetBalanceNotifyExtraEmails(v)
return _u
}
// SetNillableBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field if the given value is not nil.
func (_u *UserUpdateOne) SetNillableBalanceNotifyExtraEmails(v *string) *UserUpdateOne {
if v != nil {
_u.SetBalanceNotifyExtraEmails(*v)
}
return _u
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func (_u *UserUpdateOne) AddAPIKeyIDs(ids ...int64) *UserUpdateOne {
_u.mutation.AddAPIKeyIDs(ids...)
......@@ -1967,6 +2092,21 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) {
if _u.mutation.TotpEnabledAtCleared() {
_spec.ClearField(user.FieldTotpEnabledAt, field.TypeTime)
}
if value, ok := _u.mutation.BalanceNotifyEnabled(); ok {
_spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value)
}
if value, ok := _u.mutation.BalanceNotifyThreshold(); ok {
_spec.SetField(user.FieldBalanceNotifyThreshold, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedBalanceNotifyThreshold(); ok {
_spec.AddField(user.FieldBalanceNotifyThreshold, field.TypeFloat64, value)
}
if _u.mutation.BalanceNotifyThresholdCleared() {
_spec.ClearField(user.FieldBalanceNotifyThreshold, field.TypeFloat64)
}
if value, ok := _u.mutation.BalanceNotifyExtraEmails(); ok {
_spec.SetField(user.FieldBalanceNotifyExtraEmails, field.TypeString, value)
}
if _u.mutation.APIKeysCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
......
......@@ -183,6 +183,8 @@ github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
......@@ -218,6 +220,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
......@@ -251,6 +255,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
......@@ -280,6 +286,8 @@ github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEv
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
......@@ -312,6 +320,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
......
......@@ -175,7 +175,9 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
EnableFingerprintUnification: settings.EnableFingerprintUnification,
EnableMetadataPassthrough: settings.EnableMetadataPassthrough,
EnableCCHSigning: settings.EnableCCHSigning,
WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
AccountQuotaNotifyEmails: settings.AccountQuotaNotifyEmails,
PaymentEnabled: paymentCfg.Enabled,
PaymentMinAmount: paymentCfg.MinAmount,
PaymentMaxAmount: paymentCfg.MaxAmount,
......@@ -305,6 +307,11 @@ type UpdateSettingsRequest struct {
EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
EnableCCHSigning *bool `json:"enable_cch_signing"`
// Balance low notification
BalanceLowNotifyEnabled *bool `json:"balance_low_notify_enabled"`
BalanceLowNotifyThreshold *float64 `json:"balance_low_notify_threshold"`
AccountQuotaNotifyEmails *[]string `json:"account_quota_notify_emails"`
// Payment configuration (integrated into settings, full replace)
PaymentEnabled *bool `json:"payment_enabled"`
PaymentMinAmount *float64 `json:"payment_min_amount"`
......@@ -882,6 +889,24 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
return previousSettings.EnableCCHSigning
}(),
BalanceLowNotifyEnabled: func() bool {
if req.BalanceLowNotifyEnabled != nil {
return *req.BalanceLowNotifyEnabled
}
return previousSettings.BalanceLowNotifyEnabled
}(),
BalanceLowNotifyThreshold: func() float64 {
if req.BalanceLowNotifyThreshold != nil {
return *req.BalanceLowNotifyThreshold
}
return previousSettings.BalanceLowNotifyThreshold
}(),
AccountQuotaNotifyEmails: func() []string {
if req.AccountQuotaNotifyEmails != nil {
return *req.AccountQuotaNotifyEmails
}
return previousSettings.AccountQuotaNotifyEmails
}(),
}
if err := h.settingService.UpdateSettings(c.Request.Context(), settings); err != nil {
......@@ -1028,6 +1053,9 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
EnableFingerprintUnification: updatedSettings.EnableFingerprintUnification,
EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough,
EnableCCHSigning: updatedSettings.EnableCCHSigning,
BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled,
BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold,
AccountQuotaNotifyEmails: updatedSettings.AccountQuotaNotifyEmails,
PaymentEnabled: updatedPaymentCfg.Enabled,
PaymentMinAmount: updatedPaymentCfg.MinAmount,
PaymentMaxAmount: updatedPaymentCfg.MaxAmount,
......@@ -1848,37 +1876,3 @@ func (h *SettingHandler) UpdateStreamTimeoutSettings(c *gin.Context) {
ThresholdWindowMinutes: updatedSettings.ThresholdWindowMinutes,
})
}
// GetWebSearchEmulationConfig 获取 Web Search 模拟配置
// GET /api/v1/admin/settings/web-search-emulation
func (h *SettingHandler) GetWebSearchEmulationConfig(c *gin.Context) {
cfg, err := h.settingService.GetWebSearchEmulationConfig(c.Request.Context())
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, service.SanitizeWebSearchConfig(cfg))
}
// UpdateWebSearchEmulationConfig 更新 Web Search 模拟配置
// PUT /api/v1/admin/settings/web-search-emulation
func (h *SettingHandler) UpdateWebSearchEmulationConfig(c *gin.Context) {
var cfg service.WebSearchEmulationConfig
if err := c.ShouldBindJSON(&cfg); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
if err := h.settingService.SaveWebSearchEmulationConfig(c.Request.Context(), &cfg); err != nil {
response.ErrorFrom(c, err)
return
}
// Re-read (with sanitized api keys) to return current state
updated, err := h.settingService.GetWebSearchEmulationConfig(c.Request.Context())
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, service.SanitizeWebSearchConfig(updated))
}
......@@ -13,16 +13,19 @@ func UserFromServiceShallow(u *service.User) *User {
return nil
}
return &User{
ID: u.ID,
Email: u.Email,
Username: u.Username,
Role: u.Role,
Balance: u.Balance,
Concurrency: u.Concurrency,
Status: u.Status,
AllowedGroups: u.AllowedGroups,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
ID: u.ID,
Email: u.Email,
Username: u.Username,
Role: u.Role,
Balance: u.Balance,
Concurrency: u.Concurrency,
Status: u.Status,
AllowedGroups: u.AllowedGroups,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
BalanceNotifyEnabled: u.BalanceNotifyEnabled,
BalanceNotifyThreshold: u.BalanceNotifyThreshold,
BalanceNotifyExtraEmails: u.BalanceNotifyExtraEmails,
}
}
......@@ -322,6 +325,26 @@ func AccountFromServiceShallow(a *service.Account) *Account {
out.QuotaWeeklyResetAt = &v
}
}
// 配额通知配置
if enabled := a.GetQuotaNotifyDailyEnabled(); enabled {
out.QuotaNotifyDailyEnabled = &enabled
}
if threshold := a.GetQuotaNotifyDailyThreshold(); threshold > 0 {
out.QuotaNotifyDailyThreshold = &threshold
}
if enabled := a.GetQuotaNotifyWeeklyEnabled(); enabled {
out.QuotaNotifyWeeklyEnabled = &enabled
}
if threshold := a.GetQuotaNotifyWeeklyThreshold(); threshold > 0 {
out.QuotaNotifyWeeklyThreshold = &threshold
}
if enabled := a.GetQuotaNotifyTotalEnabled(); enabled {
out.QuotaNotifyTotalEnabled = &enabled
}
if threshold := a.GetQuotaNotifyTotalThreshold(); threshold > 0 {
out.QuotaNotifyTotalThreshold = &threshold
}
}
return out
......
......@@ -148,6 +148,11 @@ type SystemSettings struct {
PaymentCancelRateLimitWindow int `json:"payment_cancel_rate_limit_window"`
PaymentCancelRateLimitUnit string `json:"payment_cancel_rate_limit_unit"`
PaymentCancelRateLimitMode string `json:"payment_cancel_rate_limit_window_mode"`
// Balance low notification
BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"`
BalanceLowNotifyThreshold float64 `json:"balance_low_notify_threshold"`
AccountQuotaNotifyEmails []string `json:"account_quota_notify_emails"`
}
type DefaultSubscriptionSetting struct {
......
......@@ -18,6 +18,11 @@ type User struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 余额不足通知
BalanceNotifyEnabled bool `json:"balance_notify_enabled"`
BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"`
BalanceNotifyExtraEmails []string `json:"balance_notify_extra_emails"`
APIKeys []APIKey `json:"api_keys,omitempty"`
Subscriptions []UserSubscription `json:"subscriptions,omitempty"`
}
......@@ -218,6 +223,14 @@ type Account struct {
QuotaDailyResetAt *string `json:"quota_daily_reset_at,omitempty"`
QuotaWeeklyResetAt *string `json:"quota_weekly_reset_at,omitempty"`
// 配额通知配置
QuotaNotifyDailyEnabled *bool `json:"quota_notify_daily_enabled,omitempty"`
QuotaNotifyDailyThreshold *float64 `json:"quota_notify_daily_threshold,omitempty"`
QuotaNotifyWeeklyEnabled *bool `json:"quota_notify_weekly_enabled,omitempty"`
QuotaNotifyWeeklyThreshold *float64 `json:"quota_notify_weekly_threshold,omitempty"`
QuotaNotifyTotalEnabled *bool `json:"quota_notify_total_enabled,omitempty"`
QuotaNotifyTotalThreshold *float64 `json:"quota_notify_total_threshold,omitempty"`
Proxy *Proxy `json:"proxy,omitempty"`
AccountGroups []AccountGroup `json:"account_groups,omitempty"`
......
......@@ -11,13 +11,17 @@ import (
// UserHandler handles user-related requests
type UserHandler struct {
userService *service.UserService
userService *service.UserService
emailService *service.EmailService
emailCache service.EmailCache
}
// NewUserHandler creates a new UserHandler
func NewUserHandler(userService *service.UserService) *UserHandler {
func NewUserHandler(userService *service.UserService, emailService *service.EmailService, emailCache service.EmailCache) *UserHandler {
return &UserHandler{
userService: userService,
userService: userService,
emailService: emailService,
emailCache: emailCache,
}
}
......@@ -29,7 +33,9 @@ type ChangePasswordRequest struct {
// UpdateProfileRequest represents the update profile request payload
type UpdateProfileRequest struct {
Username *string `json:"username"`
Username *string `json:"username"`
BalanceNotifyEnabled *bool `json:"balance_notify_enabled"`
BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"`
}
// GetProfile handles getting user profile
......@@ -94,7 +100,9 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
}
svcReq := service.UpdateProfileRequest{
Username: req.Username,
Username: req.Username,
BalanceNotifyEnabled: req.BalanceNotifyEnabled,
BalanceNotifyThreshold: req.BalanceNotifyThreshold,
}
updatedUser, err := h.userService.UpdateProfile(c.Request.Context(), subject.UserID, svcReq)
if err != nil {
......@@ -104,3 +112,98 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
response.Success(c, dto.UserFromService(updatedUser))
}
// SendNotifyEmailCodeRequest represents the request to send notify email verification code
type SendNotifyEmailCodeRequest struct {
Email string `json:"email" binding:"required,email"`
}
// SendNotifyEmailCode sends verification code to extra notification email
// POST /api/v1/user/notify-email/send-code
func (h *UserHandler) SendNotifyEmailCode(c *gin.Context) {
subject, ok := middleware2.GetAuthSubjectFromContext(c)
if !ok {
response.Unauthorized(c, "User not authenticated")
return
}
var req SendNotifyEmailCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
err := h.userService.SendNotifyEmailCode(c.Request.Context(), subject.UserID, req.Email, h.emailService, h.emailCache)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, gin.H{"message": "Verification code sent successfully"})
}
// VerifyNotifyEmailRequest represents the request to verify and add notify email
type VerifyNotifyEmailRequest struct {
Email string `json:"email" binding:"required,email"`
Code string `json:"code" binding:"required,len=6"`
}
// VerifyNotifyEmail verifies code and adds email to notification list
// POST /api/v1/user/notify-email/verify
func (h *UserHandler) VerifyNotifyEmail(c *gin.Context) {
subject, ok := middleware2.GetAuthSubjectFromContext(c)
if !ok {
response.Unauthorized(c, "User not authenticated")
return
}
var req VerifyNotifyEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
err := h.userService.VerifyAndAddNotifyEmail(c.Request.Context(), subject.UserID, req.Email, req.Code, h.emailCache)
if err != nil {
response.ErrorFrom(c, err)
return
}
// Return updated user
updatedUser, err := h.userService.GetByID(c.Request.Context(), subject.UserID)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, dto.UserFromService(updatedUser))
}
// RemoveNotifyEmailRequest represents the request to remove a notify email
type RemoveNotifyEmailRequest struct {
Email string `json:"email" binding:"required,email"`
}
// RemoveNotifyEmail removes email from notification list
// DELETE /api/v1/user/notify-email
func (h *UserHandler) RemoveNotifyEmail(c *gin.Context) {
subject, ok := middleware2.GetAuthSubjectFromContext(c)
if !ok {
response.Unauthorized(c, "User not authenticated")
return
}
var req RemoveNotifyEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
err := h.userService.RemoveNotifyEmail(c.Request.Context(), subject.UserID, req.Email)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, gin.H{"message": "Email removed successfully"})
}
......@@ -3,6 +3,7 @@ package repository
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
......@@ -639,22 +640,32 @@ func userEntityToService(u *dbent.User) *service.User {
if u == nil {
return nil
}
return &service.User{
ID: u.ID,
Email: u.Email,
Username: u.Username,
Notes: u.Notes,
PasswordHash: u.PasswordHash,
Role: u.Role,
Balance: u.Balance,
Concurrency: u.Concurrency,
Status: u.Status,
TotpSecretEncrypted: u.TotpSecretEncrypted,
TotpEnabled: u.TotpEnabled,
TotpEnabledAt: u.TotpEnabledAt,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
out := &service.User{
ID: u.ID,
Email: u.Email,
Username: u.Username,
Notes: u.Notes,
PasswordHash: u.PasswordHash,
Role: u.Role,
Balance: u.Balance,
Concurrency: u.Concurrency,
Status: u.Status,
TotpSecretEncrypted: u.TotpSecretEncrypted,
TotpEnabled: u.TotpEnabled,
TotpEnabledAt: u.TotpEnabledAt,
BalanceNotifyEnabled: u.BalanceNotifyEnabled,
BalanceNotifyThreshold: u.BalanceNotifyThreshold,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
// Parse extra emails JSON array
if u.BalanceNotifyExtraEmails != "" && u.BalanceNotifyExtraEmails != "[]" {
var emails []string
if err := json.Unmarshal([]byte(u.BalanceNotifyExtraEmails), &emails); err == nil {
out.BalanceNotifyExtraEmails = emails
}
}
return out
}
func groupEntityToService(g *dbent.Group) *service.Group {
......
......@@ -11,6 +11,7 @@ import (
const (
verifyCodeKeyPrefix = "verify_code:"
notifyVerifyKeyPrefix = "notify_verify:"
passwordResetKeyPrefix = "password_reset:"
passwordResetSentAtKeyPrefix = "password_reset_sent:"
)
......@@ -20,6 +21,11 @@ func verifyCodeKey(email string) string {
return verifyCodeKeyPrefix + email
}
// notifyVerifyKey generates the Redis key for notify email verification code.
func notifyVerifyKey(email string) string {
return notifyVerifyKeyPrefix + email
}
// passwordResetKey generates the Redis key for password reset token.
func passwordResetKey(email string) string {
return passwordResetKeyPrefix + email
......@@ -106,3 +112,32 @@ func (c *emailCache) SetPasswordResetEmailCooldown(ctx context.Context, email st
key := passwordResetSentAtKey(email)
return c.rdb.Set(ctx, key, "1", ttl).Err()
}
// Notify email verification code methods
func (c *emailCache) GetNotifyVerifyCode(ctx context.Context, email string) (*service.VerificationCodeData, error) {
key := notifyVerifyKey(email)
val, err := c.rdb.Get(ctx, key).Result()
if err != nil {
return nil, err
}
var data service.VerificationCodeData
if err := json.Unmarshal([]byte(val), &data); err != nil {
return nil, err
}
return &data, nil
}
func (c *emailCache) SetNotifyVerifyCode(ctx context.Context, email string, data *service.VerificationCodeData, ttl time.Duration) error {
key := notifyVerifyKey(email)
val, err := json.Marshal(data)
if err != nil {
return err
}
return c.rdb.Set(ctx, key, val, ttl).Err()
}
func (c *emailCache) DeleteNotifyVerifyCode(ctx context.Context, email string) error {
key := notifyVerifyKey(email)
return c.rdb.Del(ctx, key).Err()
}
......@@ -3,6 +3,7 @@ package repository
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"sort"
......@@ -137,7 +138,7 @@ func (r *userRepository) Update(ctx context.Context, userIn *service.User) error
txClient = r.client
}
updated, err := txClient.User.UpdateOneID(userIn.ID).
updateOp := txClient.User.UpdateOneID(userIn.ID).
SetEmail(userIn.Email).
SetUsername(userIn.Username).
SetNotes(userIn.Notes).
......@@ -146,7 +147,13 @@ func (r *userRepository) Update(ctx context.Context, userIn *service.User) error
SetBalance(userIn.Balance).
SetConcurrency(userIn.Concurrency).
SetStatus(userIn.Status).
Save(ctx)
SetBalanceNotifyEnabled(userIn.BalanceNotifyEnabled).
SetNillableBalanceNotifyThreshold(userIn.BalanceNotifyThreshold).
SetBalanceNotifyExtraEmails(marshalExtraEmails(userIn.BalanceNotifyExtraEmails))
if userIn.BalanceNotifyThreshold == nil {
updateOp = updateOp.ClearBalanceNotifyThreshold()
}
updated, err := updateOp.Save(ctx)
if err != nil {
return translatePersistenceError(err, service.ErrUserNotFound, service.ErrEmailExists)
}
......@@ -549,6 +556,18 @@ func applyUserEntityToService(dst *service.User, src *dbent.User) {
dst.UpdatedAt = src.UpdatedAt
}
// marshalExtraEmails serializes a string slice to JSON for storage.
func marshalExtraEmails(emails []string) string {
if len(emails) == 0 {
return "[]"
}
data, err := json.Marshal(emails)
if err != nil {
return "[]"
}
return string(data)
}
// UpdateTotpSecret 更新用户的 TOTP 加密密钥
func (r *userRepository) UpdateTotpSecret(ctx context.Context, userID int64, encryptedSecret *string) error {
client := clientFromContext(ctx, r.client)
......
......@@ -26,6 +26,14 @@ func RegisterUserRoutes(
user.PUT("/password", h.User.ChangePassword)
user.PUT("", h.User.UpdateProfile)
// 通知邮箱管理
notifyEmail := user.Group("/notify-email")
{
notifyEmail.POST("/send-code", h.User.SendNotifyEmailCode)
notifyEmail.POST("/verify", h.User.VerifyNotifyEmail)
notifyEmail.DELETE("", h.User.RemoveNotifyEmail)
}
// TOTP 双因素认证
totp := user.Group("/totp")
{
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment