From e605c734ad20670700bcee5bba2d7b067ad052ac Mon Sep 17 00:00:00 2001 From: Stephan D Date: Thu, 12 Feb 2026 20:46:11 +0100 Subject: [PATCH] fixed verificaiton error --- .../internal/mongo/verificationimp/create.go | 45 +++++++++++++------ .../verificationimp/verification_test.go | 15 ++++++- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/api/pkg/db/internal/mongo/verificationimp/create.go b/api/pkg/db/internal/mongo/verificationimp/create.go index f627cf26..13f15948 100644 --- a/api/pkg/db/internal/mongo/verificationimp/create.go +++ b/api/pkg/db/internal/mongo/verificationimp/create.go @@ -29,6 +29,35 @@ func syntheticIdempotencyKey() string { return "auto:" + bson.NewObjectID().Hex() } +func verificationContextFilter(request *verification.Request) builder.Query { + return repository.Query().And( + repository.Filter("accountRef", request.AccountRef), + repository.Filter("purpose", request.Purpose), + repository.Filter("target", request.Target), + ) +} + +func activeContextFilter(request *verification.Request, now time.Time) builder.Query { + return repository.Query().And( + repository.Filter("accountRef", request.AccountRef), + repository.Filter("purpose", request.Purpose), + repository.Filter("target", request.Target), + repository.Filter("usedAt", nil), + repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now), + ) +} + +func cooldownActiveContextFilter(request *verification.Request, now, cutoff time.Time) builder.Query { + return repository.Query().And( + repository.Filter("accountRef", request.AccountRef), + repository.Filter("purpose", request.Purpose), + repository.Filter("target", request.Target), + repository.Filter("usedAt", nil), + repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now), + repository.Query().Comparison(repository.Field("createdAt"), builder.Gt, cutoff), + ) +} + func idempotencyFilter( request *verification.Request, idempotencyKey string, @@ -140,13 +169,7 @@ func (db *verificationDB) Create( _, err = db.tf.CreateTransaction().Execute(ctx, func(tx context.Context) (any, error) { now := time.Now().UTC() - baseFilter := repository.Query().And( - repository.Filter("accountRef", request.AccountRef), - repository.Filter("purpose", request.Purpose), - repository.Filter("target", request.Target), - repository.Filter("usedAt", nil), - repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now), - ) + activeFilter := activeContextFilter(request, now) // Optional idempotency key support for safe retries. if hasIdempotency { @@ -177,12 +200,8 @@ func (db *verificationDB) Create( if request.Cooldown != nil { cutoff := now.Add(-*request.Cooldown) - cooldownFilter := baseFilter.And( - repository.Query().Comparison(repository.Field("createdAt"), builder.Gt, cutoff), - ) - var recent model.VerificationToken - err := db.DBImp.FindOne(tx, cooldownFilter, &recent) + err := db.DBImp.FindOne(tx, cooldownActiveContextFilter(request, now, cutoff), &recent) switch { case err == nil: return nil, verification.ErrorCooldownActive() @@ -195,7 +214,7 @@ func (db *verificationDB) Create( // 2) Invalidate active tokens for this context if _, err := db.DBImp.PatchMany( tx, - baseFilter, + activeFilter, repository.Patch().Set(repository.Field("usedAt"), now), ); err != nil { return nil, err diff --git a/api/pkg/db/internal/mongo/verificationimp/verification_test.go b/api/pkg/db/internal/mongo/verificationimp/verification_test.go index 364c87c9..9220730a 100644 --- a/api/pkg/db/internal/mongo/verificationimp/verification_test.go +++ b/api/pkg/db/internal/mongo/verificationimp/verification_test.go @@ -849,15 +849,26 @@ func TestCreate_CooldownExpiresAllowsCreation(t *testing.T) { accountRef := bson.NewObjectID() // First creation without cooldown. - _, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour)) + firstRaw, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour)) require.NoError(t, err) time.Sleep(2 * time.Millisecond) // Re-create with short cooldown — the prior token is old enough to be invalidated. r2 := req(accountRef, model.PurposePasswordReset, "", time.Hour).WithCooldown(time.Millisecond) - _, err = db.Create(ctx, r2) + secondRaw, err := db.Create(ctx, r2) require.NoError(t, err) + assert.NotEqual(t, firstRaw, secondRaw) + + // Old token should be rotated out after successful re-issue. + _, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, firstRaw) + require.Error(t, err) + assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed)) + + // New token remains valid. + tok, err := db.Consume(ctx, accountRef, model.PurposePasswordReset, secondRaw) + require.NoError(t, err) + assert.Equal(t, accountRef, tok.AccountRef) } func TestCreate_CooldownNilIgnored(t *testing.T) { -- 2.49.1