184 lines
5.4 KiB
Go
184 lines
5.4 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.String("candidate_stored_hash_prefix", hashPreview(token.VerifyTokenHash)),
|
|
zap.String("candidate_computed_hash_prefix", hashPreview(hash)),
|
|
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"),
|
|
)...,
|
|
)
|
|
}
|