Files
sxchnge/backend/product/verification/store/idempotency.go

96 lines
2.5 KiB
Go

package verification
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"strings"
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"
)
func normalizedIdempotencyKey(value *string) (string, bool) {
if value == nil {
return "", false
}
key := strings.TrimSpace(*value)
if key == "" {
return "", false
}
return key, true
}
func syntheticIdempotencyKey() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", merrors.InternalWrap(err, "generate verification key")
}
return "auto:" + base64.RawURLEncoding.EncodeToString(b), nil
}
func idempotencyContextFilter(
request *Request,
idempotencyKey string,
) builder.Query {
return repository.Query().And(
repository.Filter("accountRef", request.AccountRef),
repository.Filter("purpose", request.Purpose),
targetFilter(request.Target),
repository.Filter("idempotencyKey", idempotencyKey),
)
}
func idempotencySeed(request *Request, idempotencyKey string) string {
return strings.Join([]string{
request.AccountRef.Hex(),
string(request.Purpose),
request.Target,
request.ContextRef,
request.Kind,
idempotencyKey,
}, "|")
}
func (db *verificationDB) lookupIdempotencyReplay(ctx context.Context, request *Request, idempotencyKey string) (*verificationmodel.VerificationToken, bool, error) {
tokens, err := mutil.GetObjects[verificationmodel.VerificationToken](
ctx,
db.Logger,
idempotencyContextFilter(request, idempotencyKey),
nil,
db.Repository,
)
switch {
case err == nil:
case errors.Is(err, merrors.ErrNoData):
return nil, false, nil
default:
return nil, false, err
}
var replayed *verificationmodel.VerificationToken
for i := range tokens {
existingContextRef := strings.TrimSpace(tokens[i].ContextRef)
// The lookup intentionally ignores contextRef. A reused idempotency key
// in another context is a client conflict, not a replay.
if existingContextRef != request.ContextRef {
return nil, false, ErrorIdempotencyConflict()
}
if tokens[i].UsedAt != nil {
return nil, false, ErorrTokenAlreadyUsed()
}
if tokens[i].MaxRetries != nil && tokens[i].Attempts >= *tokens[i].MaxRetries {
return nil, false, ErrorTokenAttemptsExceeded()
}
if replayed == nil {
token := tokens[i]
replayed = &token
}
}
return replayed, replayed != nil, nil
}