package verification import ( "context" "errors" verificationmodel "git.sendico.io/tech/sxchnge/product/verification/model" mutil "git.sendico.io/tech/sxchnge/shared/database/query" "git.sendico.io/tech/sxchnge/shared/database/repository" "git.sendico.io/tech/sxchnge/shared/database/repository/builder" "git.sendico.io/tech/sxchnge/shared/errors" "git.sendico.io/tech/sxchnge/shared/logging/fields" "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" ) type consumeMissError struct { cause error tokenIDs []bson.ObjectID } func newConsumeMissError(tokens []verificationmodel.VerificationToken) consumeMissError { tokenIDs := make([]bson.ObjectID, 0, len(tokens)) for i := range tokens { if tokens[i].ID != bson.NilObjectID { tokenIDs = append(tokenIDs, tokens[i].ID) } } return consumeMissError{ cause: ErorrTokenNotFound(), tokenIDs: tokenIDs, } } func (e consumeMissError) Error() string { return e.cause.Error() } func (e consumeMissError) Unwrap() error { return e.cause } func (db *verificationDB) lookupConsumeToken( ctx context.Context, scope consumeScope, ) (*verificationmodel.VerificationToken, error) { scopeFilter := scope.scopeFilter() db.Logger.Debug("Verification consume scope filter prepared", scope.logFields()...) token, err := db.lookupConsumeMagicToken(ctx, scope) if err != nil { return nil, err } if token != nil { return token, nil } if !scope.accountScoped { db.Logger.Debug("Verification consume rejected unscoped OTP scan", scope.logFields()...) return nil, ErorrTokenNotFound() } return db.lookupConsumeOTPToken(ctx, scope, scopeFilter) } func (db *verificationDB) lookupConsumeMagicToken( ctx context.Context, scope consumeScope, ) (*verificationmodel.VerificationToken, error) { magicFilter := repository.Query().And( append( scope.scopeFilters(), repository.Filter("verifyTokenHash", tokenHash(scope.rawToken)), )..., ) db.Logger.Debug("Verification consume magic path lookup", scope.logFields()...) var direct verificationmodel.VerificationToken err := db.FindOne(ctx, magicFilter, &direct) switch { case err == nil: db.Logger.Debug("Verification consume matched by magic hash path", scope.tokenLogFields(&direct)..., ) return &direct, nil case errors.Is(err, merrors.ErrNoData): db.Logger.Debug("Verification consume no direct magic match", scope.logFields()...) return nil, nil default: db.Logger.Warn("Verification consume magic path lookup failed", scope.logFields(zap.Error(err))..., ) return nil, err } } func (db *verificationDB) lookupConsumeOTPToken( ctx context.Context, scope consumeScope, scopeFilter builder.Query, ) (*verificationmodel.VerificationToken, error) { db.Logger.Debug("Verification consume OTP fallback lookup started", scope.logFields(zap.Any("scope_filter", scopeFilter.BuildQuery()))..., ) tokens, err := mutil.GetObjects[verificationmodel.VerificationToken]( ctx, db.Logger, scopeFilter, nil, db.Repository, ) if err != nil { if errors.Is(err, merrors.ErrNoData) { db.Logger.Debug("Verification consume OTP fallback found no scoped tokens", scope.logFields()...) return nil, ErorrTokenNotFound() } db.Logger.Warn("Verification consume OTP fallback query failed", scope.logFields(zap.Error(err))..., ) return nil, err } db.Logger.Debug("Verification consume OTP fallback loaded candidates", scope.logFields(zap.Int("candidate_count", len(tokens)))..., ) for i := range tokens { token := &tokens[i] hash := hasherFor(token).Hash(scope.rawToken, token) match := hash == token.VerifyTokenHash db.Logger.Debug("Verification consume OTP candidate evaluated", zap.Int("candidate_index", i), zap.Int("candidate_total", len(tokens)), mzap.ObjRef("candidate_ref", token.ID), zap.Bool("candidate_has_salt", token.Salt != nil), zap.Bool("candidate_used", token.UsedAt != nil), zap.Time("candidate_expires_at", token.ExpiresAt), zap.Int("candidate_attempts", token.Attempts), zap.Bool("candidate_has_max_retries", token.MaxRetries != nil), zap.Bool("candidate_match", match), ) if match { return token, nil } } return nil, newConsumeMissError(tokens) } func (db *verificationDB) incrementConsumeAttemptsForTokenIDs( ctx context.Context, scope consumeScope, tokenIDs []bson.ObjectID, ) { if len(tokenIDs) == 0 { return } db.Logger.Debug("Verification consume no OTP candidate matched, incrementing attempts for active scoped tokens", scope.logFields(zap.Int("candidate_count", len(tokenIDs)))..., ) filters := append( scope.scopeFilters(), repository.Query().Comparison(repository.IDField(), builder.In, tokenIDs), repository.Filter("usedAt", nil), repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, scope.now), ) incremented, err := db.PatchMany( ctx, repository.Query().And(filters...), repository.Patch().Inc(repository.Field("attempts"), 1), ) if err != nil { db.Logger.Warn("Verification consume failed to increment attempts for unmatched token", scope.logFields(zap.Error(err))..., ) return } db.Logger.Debug("Verification consume attempts increment for unmatched token executed", scope.logFields( zap.Int("updated_count", incremented), zap.String("transaction_note", "this update occurs after failed consume transaction so not_found does not roll it back"), )..., ) }