package business import ( "context" "errors" "gorm.io/gorm" "gorm.io/gorm/clause" "kra/internal/biz/business" "time" ) type resetCodePO struct { UserID uint `gorm:"primaryKey;autoIncrement:false"` CodeHash string `gorm:"size:64"` ExpiresAt, SendAfter *time.Time UpdatedAt time.Time } func (resetCodePO) TableName() string { return "sys_business_password_codes" } type resetTokenPO struct { Hash string `gorm:"column:token_hash;primaryKey;size:64"` UserID uint `gorm:"index"` ExpiresAt time.Time `gorm:"index"` CreatedAt time.Time Failures int } func (resetTokenPO) TableName() string { return "sys_business_password_resets" } type passwordResetRepo struct{ data Provider } func NewPasswordResetRepo(data Provider) business.PasswordResetRepo { return &passwordResetRepo{data: data} } type resetTxKey struct{} func (r *passwordResetRepo) db(ctx context.Context) *gorm.DB { if tx, ok := ctx.Value(resetTxKey{}).(*gorm.DB); ok { return tx.WithContext(ctx) } return r.data.DB().WithContext(ctx) } func resetUser(po accountPO) *business.ResetUser { return &business.ResetUser{ID: po.ID, Username: po.Username, Phone: po.Phone, Enabled: po.Enable == 1, Status: po.BusinessStatus} } func (r *passwordResetRepo) UserByName(ctx context.Context, name string) (*business.ResetUser, error) { var po accountPO err := r.db(ctx).Select("id,username,phone,enable,business_status").Where("username = ?", name).First(&po).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { return nil, business.ErrStorage } return resetUser(po), nil } func (r *passwordResetRepo) UserByID(ctx context.Context, id uint) (*business.ResetUser, error) { var po accountPO err := r.db(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Select("id,username,phone,enable,business_status").First(&po, id).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { return nil, business.ErrStorage } return resetUser(po), nil } func (r *passwordResetRepo) WithCodeLock(ctx context.Context, id uint, fn func(context.Context) error) error { err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { po := resetCodePO{UserID: id} if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&po).Error; err != nil { return err } if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&po, id).Error; err != nil { return err } return fn(context.WithValue(ctx, resetTxKey{}, tx)) }) return storageError(err) } func (r *passwordResetRepo) Code(ctx context.Context, id uint) (*business.ResetCode, error) { var po resetCodePO if err := r.db(ctx).First(&po, id).Error; err != nil { return nil, storageError(err) } return &business.ResetCode{UserID: po.UserID, CodeHash: po.CodeHash, ExpiresAt: po.ExpiresAt, SendAfter: po.SendAfter}, nil } func (r *passwordResetRepo) SaveCode(ctx context.Context, c *business.ResetCode) error { return storageError(r.db(ctx).Model(&resetCodePO{}).Where("user_id = ?", c.UserID).Updates(map[string]any{"code_hash": c.CodeHash, "expires_at": c.ExpiresAt, "send_after": c.SendAfter}).Error) } func (r *passwordResetRepo) CreateToken(ctx context.Context, t *business.ResetToken) error { if err := r.db(ctx).Where("expires_at < ?", t.CreatedAt).Delete(&resetTokenPO{}).Error; err != nil { return storageError(err) } return storageError(r.db(ctx).Create(&resetTokenPO{Hash: t.Hash, UserID: t.UserID, ExpiresAt: t.ExpiresAt, CreatedAt: t.CreatedAt, Failures: t.Failures}).Error) } func (r *passwordResetRepo) WithTokenLock(ctx context.Context, key string, fn func(context.Context) error) error { return storageError(r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { return fn(context.WithValue(ctx, resetTxKey{}, tx)) })) } func (r *passwordResetRepo) Token(ctx context.Context, key string) (*business.ResetToken, error) { var po resetTokenPO err := r.db(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).First(&po, "token_hash = ?", key).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { return nil, storageError(err) } return &business.ResetToken{Hash: po.Hash, UserID: po.UserID, ExpiresAt: po.ExpiresAt, CreatedAt: po.CreatedAt, Failures: po.Failures}, nil } func (r *passwordResetRepo) SaveToken(ctx context.Context, t *business.ResetToken) error { return storageError(r.db(ctx).Model(&resetTokenPO{}).Where("token_hash = ?", t.Hash).Update("failures", t.Failures).Error) } func (r *passwordResetRepo) DeleteToken(ctx context.Context, key string) error { return storageError(r.db(ctx).Where("token_hash = ?", key).Delete(&resetTokenPO{}).Error) } func (r *passwordResetRepo) UpdatePassword(ctx context.Context, id uint, hash string) error { result := r.db(ctx).Model(&accountPO{}).Where("id = ?", id).Updates(map[string]any{"password": hash, "password_updated_at": time.Now(), "must_change_password": false}) if result.Error != nil { return storageError(result.Error) } if result.RowsAffected == 0 { return business.ErrResetToken } return nil }