Files

182 lines
5.3 KiB
Go

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"),
)...,
)
}