Linting #509
@@ -29,6 +29,35 @@ func syntheticIdempotencyKey() string {
|
|||||||
return "auto:" + bson.NewObjectID().Hex()
|
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(
|
func idempotencyFilter(
|
||||||
request *verification.Request,
|
request *verification.Request,
|
||||||
idempotencyKey string,
|
idempotencyKey string,
|
||||||
@@ -140,13 +169,7 @@ func (db *verificationDB) Create(
|
|||||||
_, err = db.tf.CreateTransaction().Execute(ctx, func(tx context.Context) (any, error) {
|
_, err = db.tf.CreateTransaction().Execute(ctx, func(tx context.Context) (any, error) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
baseFilter := repository.Query().And(
|
activeFilter := activeContextFilter(request, now)
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Optional idempotency key support for safe retries.
|
// Optional idempotency key support for safe retries.
|
||||||
if hasIdempotency {
|
if hasIdempotency {
|
||||||
@@ -177,12 +200,8 @@ func (db *verificationDB) Create(
|
|||||||
if request.Cooldown != nil {
|
if request.Cooldown != nil {
|
||||||
cutoff := now.Add(-*request.Cooldown)
|
cutoff := now.Add(-*request.Cooldown)
|
||||||
|
|
||||||
cooldownFilter := baseFilter.And(
|
|
||||||
repository.Query().Comparison(repository.Field("createdAt"), builder.Gt, cutoff),
|
|
||||||
)
|
|
||||||
|
|
||||||
var recent model.VerificationToken
|
var recent model.VerificationToken
|
||||||
err := db.DBImp.FindOne(tx, cooldownFilter, &recent)
|
err := db.DBImp.FindOne(tx, cooldownActiveContextFilter(request, now, cutoff), &recent)
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
return nil, verification.ErrorCooldownActive()
|
return nil, verification.ErrorCooldownActive()
|
||||||
@@ -195,7 +214,7 @@ func (db *verificationDB) Create(
|
|||||||
// 2) Invalidate active tokens for this context
|
// 2) Invalidate active tokens for this context
|
||||||
if _, err := db.DBImp.PatchMany(
|
if _, err := db.DBImp.PatchMany(
|
||||||
tx,
|
tx,
|
||||||
baseFilter,
|
activeFilter,
|
||||||
repository.Patch().Set(repository.Field("usedAt"), now),
|
repository.Patch().Set(repository.Field("usedAt"), now),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -849,15 +849,26 @@ func TestCreate_CooldownExpiresAllowsCreation(t *testing.T) {
|
|||||||
accountRef := bson.NewObjectID()
|
accountRef := bson.NewObjectID()
|
||||||
|
|
||||||
// First creation without cooldown.
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
time.Sleep(2 * time.Millisecond)
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
|
||||||
// Re-create with short cooldown — the prior token is old enough to be invalidated.
|
// Re-create with short cooldown — the prior token is old enough to be invalidated.
|
||||||
r2 := req(accountRef, model.PurposePasswordReset, "", time.Hour).WithCooldown(time.Millisecond)
|
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)
|
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) {
|
func TestCreate_CooldownNilIgnored(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user