This commit is contained in:
parent
950a6b7f1d
commit
310ad0136e
@ -1076,6 +1076,7 @@ func DisableExternalEmail(ctx *context.Context, w http.ResponseWriter, req *http
|
||||
}
|
||||
if runtime.verification != nil && runtime.verification.Store != nil {
|
||||
runtime.verification.Store.InvalidateResource(input.ID, verification.PurposeExternalEmailBind)
|
||||
runtime.verification.Store.InvalidateResource(input.ID, verification.PurposeExternalEmailChange)
|
||||
runtime.verification.Store.InvalidateAccount(ctx.UserAccount)
|
||||
}
|
||||
if runtime.recovery != nil {
|
||||
|
||||
@ -61,7 +61,7 @@ func (r *Repository) BeginReplacement(request BindRequest) (*models.ExternalEmai
|
||||
if sameAddress.UserID != request.UserID || sameAddress.Status == models.ExternalEmailStatusVerified {
|
||||
return nil, ErrBindingExists
|
||||
}
|
||||
if sameAddress.EmailType != request.Type {
|
||||
if sameAddress.Status != models.ExternalEmailStatusDisabled && sameAddress.EmailType != request.Type {
|
||||
return nil, ErrBindingExists
|
||||
}
|
||||
if sameAddress.Status == models.ExternalEmailStatusPending {
|
||||
@ -71,13 +71,19 @@ func (r *Repository) BeginReplacement(request BindRequest) (*models.ExternalEmai
|
||||
|
||||
now := r.now().UTC()
|
||||
if hasSameAddress && sameAddress.Status == models.ExternalEmailStatusDisabled {
|
||||
sameAddress.EmailType = request.Type
|
||||
sameAddress.Status = models.ExternalEmailStatusPending
|
||||
sameAddress.VerifiedAt = nil
|
||||
sameAddress.CredentialVersion++
|
||||
sameAddress.UpdatedAt = now
|
||||
if _, err := r.engine.ID(sameAddress.ID).Cols("status", "verified_at", "credential_version", "updated_at").Update(&sameAddress); err != nil {
|
||||
updated, err := r.engine.ID(sameAddress.ID).Where("status = ?", models.ExternalEmailStatusDisabled).
|
||||
Cols("email_type", "status", "verified_at", "credential_version", "updated_at").Update(&sameAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reuse disabled replacement external email: %w", err)
|
||||
}
|
||||
if updated != 1 {
|
||||
return nil, ErrBindingExists
|
||||
}
|
||||
return &sameAddress, nil
|
||||
}
|
||||
|
||||
@ -145,11 +151,11 @@ func (r *Repository) Bind(request BindRequest) (*models.ExternalEmail, error) {
|
||||
return nil, err
|
||||
}
|
||||
var existing models.ExternalEmail
|
||||
has, err := r.engine.Where("user_id = ? AND email_fingerprint = ?", request.UserID, fingerprint).Get(&existing)
|
||||
has, err := r.engine.Where("email_fingerprint = ?", fingerprint).Get(&existing)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check external email binding: %w", err)
|
||||
}
|
||||
if has {
|
||||
if has && (existing.UserID != request.UserID || existing.Status != models.ExternalEmailStatusDisabled) {
|
||||
return nil, ErrBindingExists
|
||||
}
|
||||
var count int64
|
||||
@ -164,6 +170,23 @@ func (r *Repository) Bind(request BindRequest) (*models.ExternalEmail, error) {
|
||||
return nil, ErrBackupLimit
|
||||
}
|
||||
now := r.now().UTC()
|
||||
if has {
|
||||
// 停用记录可以重新绑定,但必须重新验证,不能沿用之前的验证状态。
|
||||
existing.EmailType = request.Type
|
||||
existing.Status = models.ExternalEmailStatusPending
|
||||
existing.VerifiedAt = nil
|
||||
existing.CredentialVersion++
|
||||
existing.UpdatedAt = now
|
||||
updated, err := r.engine.ID(existing.ID).Where("status = ?", models.ExternalEmailStatusDisabled).
|
||||
Cols("email_type", "status", "verified_at", "credential_version", "updated_at").Update(&existing)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reuse disabled external email binding: %w", err)
|
||||
}
|
||||
if updated != 1 {
|
||||
return nil, ErrBindingExists
|
||||
}
|
||||
return &existing, nil
|
||||
}
|
||||
item := &models.ExternalEmail{
|
||||
UserID: request.UserID,
|
||||
EmailCiphertext: ciphertext,
|
||||
|
||||
@ -165,3 +165,83 @@ func TestRepositoryReplacesExternalEmailOnlyAfterVerification(t *testing.T) {
|
||||
t.Fatalf("unexpected replacement statuses: %+v", statuses)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryRebindsDisabledEmailWithFreshVerification(t *testing.T) {
|
||||
for _, replacement := range []bool{false, true} {
|
||||
name := "bind"
|
||||
if replacement {
|
||||
name = "replacement"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
engine, err := xorm.NewEngine("sqlite", filepath.Join(t.TempDir(), "external.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer engine.Close()
|
||||
if err := engine.Sync2(&models.ExternalEmail{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
vault := NewVaultWithKeys([]byte("01234567890123456789012345678901"), []byte("abcdefabcdefabcdefabcdefabcdefab"))
|
||||
repo, err := NewRepository(engine, vault)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
item, err := repo.Bind(BindRequest{UserID: 1, Email: "owner@example.com", Type: models.ExternalEmailTypeBackup})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repo.Verify(1, item.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bind := repo.Bind
|
||||
verify := repo.Verify
|
||||
if replacement {
|
||||
bind = repo.BeginReplacement
|
||||
verify = repo.VerifyReplacement
|
||||
}
|
||||
request := BindRequest{UserID: 1, Email: "OWNER@example.com", Type: models.ExternalEmailTypePrimary}
|
||||
if _, err := bind(request); err != ErrBindingExists {
|
||||
t.Fatalf("已验证邮箱不应直接更改用途:%v", err)
|
||||
}
|
||||
if err := repo.Disable(1, item.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
other := request
|
||||
other.UserID = 2
|
||||
if _, err := bind(other); err != ErrBindingExists {
|
||||
t.Fatalf("不能复用其他用户的停用记录:%v", err)
|
||||
}
|
||||
pending, err := bind(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pending.ID != item.ID || pending.Status != models.ExternalEmailStatusPending || pending.VerifiedAt != nil || pending.EmailType != models.ExternalEmailTypePrimary || pending.CredentialVersion <= item.CredentialVersion {
|
||||
t.Fatalf("重新绑定必须保留记录并重新验证:%+v", pending)
|
||||
}
|
||||
if _, err := repo.FindVerifiedPrimaryByUser(1); err != ErrBindingNotFound {
|
||||
t.Fatalf("未验证邮箱不能参与登录:%v", err)
|
||||
}
|
||||
stored, err := repo.List(1)
|
||||
if err != nil || len(stored) != 1 || stored[0].VerifiedAt != nil || stored[0].Status != models.ExternalEmailStatusPending {
|
||||
t.Fatalf("旧验证状态未清除:%+v,%v", stored, err)
|
||||
}
|
||||
if err := verify(1, pending.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := repo.FindVerifiedPrimaryByUser(1); err != nil {
|
||||
t.Fatalf("重新验证后应可用于登录:%v", err)
|
||||
}
|
||||
if !replacement {
|
||||
if err := repo.Disable(1, item.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := repo.Bind(BindRequest{UserID: 1, Email: "other@example.com", Type: models.ExternalEmailTypePrimary}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := bind(request); err != ErrPrimaryLimit {
|
||||
t.Fatalf("重新绑定不能绕过主要邮箱数量限制:%v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user