Callbacks service docs updated

This commit is contained in:
Stephan D
2026-03-02 16:27:33 +01:00
parent 17e08ff26f
commit 2be76aa519
77 changed files with 803 additions and 764 deletions

View File

@@ -12,4 +12,7 @@ type DB interface {
auth.ProtectedDB[*model.Callback]
SetArchived(ctx context.Context, accountRef, organizationRef, callbackRef bson.ObjectID, archived, cascade bool) error
List(ctx context.Context, accountRef, organizationRef, _ bson.ObjectID, cursor *model.ViewCursor) ([]model.Callback, error)
GetSigningSecretRef(ctx context.Context, accountRef, callbackRef bson.ObjectID) (string, error)
SetSigningSecretRef(ctx context.Context, accountRef, callbackRef bson.ObjectID, secretRef string) error
ClearSigningSecretRef(ctx context.Context, accountRef, callbackRef bson.ObjectID) error
}

View File

@@ -0,0 +1,8 @@
package callbacksdb
import "github.com/tech/sendico/pkg/model"
type callbackInternal struct {
model.Callback `bson:",inline" json:",inline"`
SecretRef string `bson:"secret_ref" json:"-"`
}

View File

@@ -2,14 +2,12 @@ package callbacksdb
import (
"context"
"errors"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/callbacks"
"github.com/tech/sendico/pkg/db/policy"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
@@ -29,10 +27,6 @@ func Create(
pdb policy.DB,
db *mongo.Database,
) (*CallbacksDB, error) {
if err := ensureBuiltInPolicy(ctx, logger, pdb); err != nil {
return nil, err
}
p, err := auth.CreateDBImp[*model.Callback](ctx, logger, pdb, enforcer, mservice.Callbacks, db)
if err != nil {
return nil, err
@@ -43,7 +37,7 @@ func Create(
Name: "uq_callbacks_client_url",
Keys: []ri.Key{
{Field: storable.OrganizationRefField, Sort: ri.Asc},
{Field: "client_id", Sort: ri.Asc},
{Field: "organization_ref", Sort: ri.Asc},
{Field: "url", Sort: ri.Asc},
},
Unique: true,
@@ -82,31 +76,4 @@ func Create(
}, nil
}
func ensureBuiltInPolicy(ctx context.Context, logger mlogger.Logger, pdb policy.DB) error {
var existing model.PolicyDescription
if err := pdb.GetBuiltInPolicy(ctx, mservice.Callbacks, &existing); err == nil {
return nil
} else if !errors.Is(err, merrors.ErrNoData) {
return err
}
description := "Callbacks subscription management"
resourceTypes := []mservice.Type{mservice.Callbacks}
policyDescription := &model.PolicyDescription{
Describable: model.Describable{
Name: "Callbacks",
Description: &description,
},
ResourceTypes: &resourceTypes,
}
if err := pdb.Create(ctx, policyDescription); err != nil && !errors.Is(err, merrors.ErrDataConflict) {
if logger != nil {
logger.Warn("Failed to create built-in callbacks policy", zap.Error(err))
}
return err
}
return pdb.GetBuiltInPolicy(ctx, mservice.Callbacks, &existing)
}
var _ callbacks.DB = (*CallbacksDB)(nil)

View File

@@ -0,0 +1,60 @@
package callbacksdb
import (
"context"
"strings"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
func (db *CallbacksDB) GetSigningSecretRef(ctx context.Context, accountRef, callbackRef bson.ObjectID) (string, error) {
if callbackRef.IsZero() {
return "", merrors.InvalidArgument("callback reference is required", "callbackRef")
}
// Enforce read permissions through the public callback object first.
var callback model.Callback
if err := db.Get(ctx, accountRef, callbackRef, &callback); err != nil {
return "", err
}
internal := &callbackInternal{}
if err := db.DBImp.Repository.Get(ctx, callbackRef, internal); err != nil {
return "", err
}
return strings.TrimSpace(internal.SecretRef), nil
}
func (db *CallbacksDB) SetSigningSecretRef(ctx context.Context, accountRef, callbackRef bson.ObjectID, secretRef string) error {
if callbackRef.IsZero() {
return merrors.InvalidArgument("callback reference is required", "callbackRef")
}
value := strings.TrimSpace(secretRef)
if value == "" {
return merrors.InvalidArgument("secret reference is required", "secretRef")
}
return db.Patch(
ctx,
accountRef,
callbackRef,
repository.Patch().Set(repository.Field("secretRef"), value),
)
}
func (db *CallbacksDB) ClearSigningSecretRef(ctx context.Context, accountRef, callbackRef bson.ObjectID) error {
if callbackRef.IsZero() {
return merrors.InvalidArgument("callback reference is required", "callbackRef")
}
return db.Patch(
ctx,
accountRef,
callbackRef,
repository.Patch().Unset(repository.Field("secretRef")),
)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
mutil "github.com/tech/sendico/pkg/mutil/db"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
@@ -54,7 +55,6 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.Any("scope_filter", scopeFilter.BuildQuery()),
)
// 1) Fast path for magic-link tokens: hash is deterministic and globally unique.
@@ -69,7 +69,6 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.Any("magic_filter", magicFilter.BuildQuery()),
)
var direct model.VerificationToken
err := db.DBImp.FindOne(ctx, magicFilter, &direct)
@@ -152,7 +151,7 @@ func (db *verificationDB) Consume(
db.Logger.Debug("Verification consume OTP candidate evaluated",
zap.Int("candidate_index", i),
zap.Int("candidate_total", len(tokens)),
zap.String("candidate_id", t.ID.Hex()),
mzap.ObjRef("candidate_ref", t.ID),
zap.Bool("candidate_has_salt", t.Salt != nil),
zap.Bool("candidate_used", t.UsedAt != nil),
zap.Time("candidate_expires_at", t.ExpiresAt),
@@ -181,7 +180,6 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.Any("active_filter", activeFilter.BuildQuery()),
)
incremented, patchErr := db.DBImp.PatchMany(
@@ -271,8 +269,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
zap.Any("consume_filter", consumeFilter.BuildQuery()),
mzap.StorableRef(token),
)
updated, err := db.DBImp.PatchMany(
@@ -285,7 +282,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Error(err),
)
return nil, err
@@ -294,7 +291,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Int("updated_count", updated),
)
@@ -322,7 +319,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Error(incrementErr),
)
} else {
@@ -330,7 +327,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Int("updated_count", incremented),
zap.String("transaction_note", "this update occurs inside transaction and may roll back depending on returned error"),
)
@@ -343,7 +340,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Error(err),
)
return nil, merrors.Internal("failed to re-check token state")
@@ -353,7 +350,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
}, tokenStateFields(&fresh)...,
)...,
)
@@ -363,7 +360,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
)
return nil, verification.ErorrTokenAlreadyUsed()
}
@@ -372,7 +369,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Time("now_utc", now),
zap.Time("token_expires_at", fresh.ExpiresAt),
)
@@ -383,7 +380,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Int("token_attempts", fresh.Attempts),
zap.Int("token_max_retries", *fresh.MaxRetries),
)
@@ -394,7 +391,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
)
return nil, verification.ErorrTokenNotFound()
},
@@ -457,8 +454,8 @@ func tokenIsDigitsOnly(value string) bool {
func tokenStateFields(token *model.VerificationToken) []zap.Field {
fields := []zap.Field{
zap.String("token_id", token.ID.Hex()),
zap.String("token_account_ref", token.AccountRef.Hex()),
mzap.StorableRef(token),
mzap.ObjRef("token_account_ref", token.AccountRef),
zap.String("token_purpose", string(token.Purpose)),
zap.Bool("token_has_target", strings.TrimSpace(token.Target) != ""),
zap.Bool("token_has_idempotency_key", token.IdempotencyKey != nil),

View File

@@ -29,14 +29,6 @@ func syntheticIdempotencyKey() string {
return "auto:" + bson.NewObjectID().Hex()
}
func verificationContextFilter(request *verification.Request) builder.Query {
return repository.Query().And(
repository.Filter("accountRef", request.AccountRef),
repository.Filter("purpose", request.Purpose),
repository.Filter("target", request.Target),
)
}
func activeContextFilter(request *verification.Request, now time.Time) builder.Query {
return repository.Query().And(
repository.Filter("accountRef", request.AccountRef),