package verificationimp import ( "context" "errors" "time" "github.com/tech/sendico/pkg/db/repository" "github.com/tech/sendico/pkg/db/repository/builder" "github.com/tech/sendico/pkg/db/verification" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/model" "go.uber.org/zap" ) func (db *verificationDB) Consume( ct context.Context, rawToken string, ) (*model.VerificationToken, error) { hash := tokenHash(rawToken) now := time.Now().UTC() // 1) Пытаемся атомарно использовать токен filter := repository.Query().And( repository.Filter("verifyTokenHash", hash), repository.Filter("usedAt", nil), repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now), ) t, err := db.tf.CreateTransaction().Execute( ct, func(ctx context.Context) (any, error) { var existing model.VerificationToken if err := db.DBImp.FindOne(ctx, filter, &existing); err != nil { if errors.Is(err, merrors.ErrNoData) { // normal behaviour db.Logger.Debug("Token hash not found", zap.Error(err), zap.String("hash", hash)) return nil, wrap(verification.ErrTokenNotFound, err.Error()) } db.Logger.Warn("Failed to check token", zap.Error(err), zap.String("hash", hash)) return nil, err } if existing.UsedAt != nil { db.Logger.Debug("Token has already been used", zap.String("hash", hash), zap.Time("used_at", *existing.UsedAt)) return nil, wrap(verification.ErrTokenAlreadyUsed, "db: token already used") } if existing.ExpiresAt.Before(now) { db.Logger.Debug("Token has already expired", zap.String("hash", hash), zap.Time("expired_at", existing.ExpiresAt)) return nil, wrap(verification.ErrTokenExpired, "db: token expired") } existing.UsedAt = &now if err := db.DBImp.Update(ctx, &existing); err != nil { db.Logger.Warn("Failed to consume token", zap.Error(err), zap.String("hash", hash)) return nil, err } return &existing, nil }, ) if err != nil { return nil, err } res, ok := t.(*model.VerificationToken) if !ok { return nil, merrors.Internal("unexpexted token type") } return res, nil }