Files
kra-new/internal/data/business/password_reset.go
T

129 lines
5.0 KiB
Go

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
}