bff for callbacks
This commit is contained in:
15
api/pkg/db/callbacks/callbacks.go
Normal file
15
api/pkg/db/callbacks/callbacks.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package callbacks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db/account"
|
||||
"github.com/tech/sendico/pkg/db/callbacks"
|
||||
"github.com/tech/sendico/pkg/db/chainassets"
|
||||
"github.com/tech/sendico/pkg/db/chainwalletroutes"
|
||||
mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo"
|
||||
@@ -29,6 +30,7 @@ type Factory interface {
|
||||
NewOrganizationDB() (organization.DB, error)
|
||||
NewInvitationsDB() (invitation.DB, error)
|
||||
NewRecipientsDB() (recipient.DB, error)
|
||||
NewCallbacksDB() (callbacks.DB, error)
|
||||
NewVerificationsDB() (verification.DB, error)
|
||||
|
||||
NewRolesDB() (role.DB, error)
|
||||
|
||||
30
api/pkg/db/internal/mongo/callbacksdb/archived.go
Normal file
30
api/pkg/db/internal/mongo/callbacksdb/archived.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package callbacksdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (db *CallbacksDB) SetArchived(
|
||||
ctx context.Context,
|
||||
accountRef,
|
||||
organizationRef,
|
||||
callbackRef bson.ObjectID,
|
||||
isArchived,
|
||||
cascade bool,
|
||||
) error {
|
||||
if err := db.ArchivableDB.SetArchived(ctx, accountRef, callbackRef, isArchived); err != nil {
|
||||
db.DBImp.Logger.Warn("Failed to change callback archive status", zap.Error(err),
|
||||
mzap.AccRef(accountRef),
|
||||
mzap.ObjRef("organization_ref", organizationRef),
|
||||
mzap.ObjRef("callback_ref", callbackRef),
|
||||
zap.Bool("archived", isArchived),
|
||||
zap.Bool("cascade", cascade),
|
||||
)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
112
api/pkg/db/internal/mongo/callbacksdb/db.go
Normal file
112
api/pkg/db/internal/mongo/callbacksdb/db.go
Normal file
@@ -0,0 +1,112 @@
|
||||
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"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CallbacksDB struct {
|
||||
auth.ProtectedDBImp[*model.Callback]
|
||||
auth.ArchivableDB[*model.Callback]
|
||||
}
|
||||
|
||||
func Create(
|
||||
ctx context.Context,
|
||||
logger mlogger.Logger,
|
||||
enforcer auth.Enforcer,
|
||||
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
|
||||
}
|
||||
|
||||
for _, definition := range []*ri.Definition{
|
||||
{
|
||||
Name: "uq_callbacks_client_url",
|
||||
Keys: []ri.Key{
|
||||
{Field: storable.OrganizationRefField, Sort: ri.Asc},
|
||||
{Field: "client_id", Sort: ri.Asc},
|
||||
{Field: "url", Sort: ri.Asc},
|
||||
},
|
||||
Unique: true,
|
||||
},
|
||||
{
|
||||
Name: "idx_callbacks_lookup",
|
||||
Keys: []ri.Key{
|
||||
{Field: storable.OrganizationRefField, Sort: ri.Asc},
|
||||
{Field: "status", Sort: ri.Asc},
|
||||
{Field: "event_types", Sort: ri.Asc},
|
||||
},
|
||||
},
|
||||
} {
|
||||
if err := p.DBImp.Repository.CreateIndex(definition); err != nil {
|
||||
p.DBImp.Logger.Warn("Failed to create callbacks index", zap.String("index", definition.Name), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
createEmpty := func() *model.Callback {
|
||||
return &model.Callback{}
|
||||
}
|
||||
getArchivable := func(callback *model.Callback) model.Archivable {
|
||||
return &callback.ArchivableBase
|
||||
}
|
||||
|
||||
return &CallbacksDB{
|
||||
ProtectedDBImp: *p,
|
||||
ArchivableDB: auth.NewArchivableDB(
|
||||
p.DBImp,
|
||||
p.DBImp.Logger,
|
||||
enforcer,
|
||||
createEmpty,
|
||||
getArchivable,
|
||||
),
|
||||
}, 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)
|
||||
36
api/pkg/db/internal/mongo/callbacksdb/list.go
Normal file
36
api/pkg/db/internal/mongo/callbacksdb/list.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package callbacksdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
mauth "github.com/tech/sendico/pkg/mutil/db/auth"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func (db *CallbacksDB) List(
|
||||
ctx context.Context,
|
||||
accountRef,
|
||||
organizationRef,
|
||||
_ bson.ObjectID,
|
||||
cursor *model.ViewCursor,
|
||||
) ([]model.Callback, error) {
|
||||
res, err := mauth.GetProtectedObjects[model.Callback](
|
||||
ctx,
|
||||
db.DBImp.Logger,
|
||||
accountRef,
|
||||
organizationRef,
|
||||
model.ActionRead,
|
||||
repository.OrgFilter(organizationRef),
|
||||
cursor,
|
||||
db.Enforcer,
|
||||
db.DBImp.Repository,
|
||||
)
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
return []model.Callback{}, nil
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
@@ -10,9 +10,11 @@ import (
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db/account"
|
||||
"github.com/tech/sendico/pkg/db/callbacks"
|
||||
"github.com/tech/sendico/pkg/db/chainassets"
|
||||
"github.com/tech/sendico/pkg/db/chainwalletroutes"
|
||||
"github.com/tech/sendico/pkg/db/internal/mongo/accountdb"
|
||||
"github.com/tech/sendico/pkg/db/internal/mongo/callbacksdb"
|
||||
"github.com/tech/sendico/pkg/db/internal/mongo/chainassetsdb"
|
||||
"github.com/tech/sendico/pkg/db/internal/mongo/chainwalletroutesdb"
|
||||
"github.com/tech/sendico/pkg/db/internal/mongo/invitationdb"
|
||||
@@ -218,6 +220,10 @@ func (db *DB) NewRecipientsDB() (recipient.DB, error) {
|
||||
return newProtectedDB(db, create)
|
||||
}
|
||||
|
||||
func (db *DB) NewCallbacksDB() (callbacks.DB, error) {
|
||||
return newProtectedDB(db, callbacksdb.Create)
|
||||
}
|
||||
|
||||
func (db *DB) NewRefreshTokensDB() (refreshtokens.DB, error) {
|
||||
return refreshtokensdb.Create(db.logger, db.db())
|
||||
}
|
||||
|
||||
41
api/pkg/model/callback.go
Normal file
41
api/pkg/model/callback.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package model
|
||||
|
||||
import "github.com/tech/sendico/pkg/mservice"
|
||||
|
||||
type CallbackStatus string
|
||||
|
||||
const (
|
||||
CallbackStatusActive CallbackStatus = "active"
|
||||
CallbackStatusDisabled CallbackStatus = "disabled"
|
||||
)
|
||||
|
||||
type CallbackSigningMode string
|
||||
|
||||
const (
|
||||
CallbackSigningModeNone CallbackSigningMode = "none"
|
||||
CallbackSigningModeHMACSHA256 CallbackSigningMode = "hmac_sha256"
|
||||
)
|
||||
|
||||
type CallbackRetryPolicy struct {
|
||||
MinDelayMS int `bson:"min_ms" json:"minDelayMs"`
|
||||
MaxDelayMS int `bson:"max_ms" json:"maxDelayMs"`
|
||||
SigningMode CallbackSigningMode `bson:"signing_mode" json:"signingMode"`
|
||||
SecretRef string `bson:"secret_ref,omitempty" json:"secretRef,omitempty"`
|
||||
Headers map[string]string `bson:"headers,omitempty" json:"headers,omitempty"`
|
||||
MaxAttempts int `bson:"max_attempts" json:"maxAttempts"`
|
||||
RequestTimeoutMS int `bson:"request_timeout_ms" json:"requestTimeoutMs"`
|
||||
}
|
||||
|
||||
type Callback struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
ClientID string `bson:"client_id" json:"clientId"`
|
||||
Status CallbackStatus `bson:"status" json:"status"`
|
||||
URL string `bson:"url" json:"url"`
|
||||
EventTypes []string `bson:"event_types" json:"eventTypes"`
|
||||
RetryPolicy CallbackRetryPolicy `bson:"retry_policy" json:"retryPolicy"`
|
||||
}
|
||||
|
||||
func (*Callback) Collection() string {
|
||||
return mservice.Callbacks
|
||||
}
|
||||
@@ -35,6 +35,7 @@ const (
|
||||
ChainWalletBalances Type = "chain_wallet_balances" // Represents managed chain wallet balances
|
||||
ChainTransfers Type = "chain_transfers" // Represents chain transfers
|
||||
ChainDeposits Type = "chain_deposits" // Represents chain deposits
|
||||
Callbacks Type = "callbacks" // Represents webhook callback subscriptions
|
||||
Notifications Type = "notifications" // Represents notifications sent to users
|
||||
Organizations Type = "organizations" // Represents organizations in the system
|
||||
Payments Type = "payments" // Represents payments service
|
||||
@@ -58,7 +59,7 @@ const (
|
||||
func StringToSType(s string) (Type, error) {
|
||||
switch Type(s) {
|
||||
case Accounts, Verification, Amplitude, Site, Changes, Clients, ChainGateway, ChainWallets, WalletRoutes, ChainWalletBalances,
|
||||
ChainTransfers, ChainDeposits, MntxGateway, PaymentGateway, FXOracle, FeePlans, BillingDocuments, FilterProjects, Invitations, Invoices, Logo, Ledger,
|
||||
ChainTransfers, ChainDeposits, Callbacks, MntxGateway, PaymentGateway, FXOracle, FeePlans, BillingDocuments, FilterProjects, Invitations, Invoices, Logo, Ledger,
|
||||
LedgerAccounts, LedgerBalances, LedgerEntries, LedgerOutbox, LedgerParties, LedgerPlines, Notifications,
|
||||
Organizations, Payments, PaymentRoutes, PaymentOrchestrator, PaymentMethods, Permissions, Policies, PolicyAssignements,
|
||||
Recipients, RefreshTokens, Roles, Storage, Tenants, Workflows, Discovery:
|
||||
|
||||
@@ -8,10 +8,12 @@ import (
|
||||
|
||||
// Config describes Vault KV v2 connection settings.
|
||||
type Config struct {
|
||||
Address string `mapstructure:"address" yaml:"address"`
|
||||
TokenEnv string `mapstructure:"token_env" yaml:"token_env"`
|
||||
Namespace string `mapstructure:"namespace" yaml:"namespace"`
|
||||
MountPath string `mapstructure:"mount_path" yaml:"mount_path"`
|
||||
Address string `mapstructure:"address" yaml:"address"`
|
||||
TokenEnv string `mapstructure:"token_env" yaml:"token_env"`
|
||||
TokenFileEnv string `mapstructure:"token_file_env" yaml:"token_file_env"`
|
||||
TokenFile string `mapstructure:"token_file" yaml:"token_file"`
|
||||
Namespace string `mapstructure:"namespace" yaml:"namespace"`
|
||||
MountPath string `mapstructure:"mount_path" yaml:"mount_path"`
|
||||
}
|
||||
|
||||
// Client defines KV operations used by services.
|
||||
|
||||
@@ -36,16 +36,14 @@ func newService(opts Options) (Client, error) {
|
||||
return nil, merrors.InvalidArgument(component + ": address is required")
|
||||
}
|
||||
|
||||
tokenEnv := strings.TrimSpace(opts.Config.TokenEnv)
|
||||
if tokenEnv == "" {
|
||||
logger.Error("Vault token env missing")
|
||||
return nil, merrors.InvalidArgument(component + ": token_env is required")
|
||||
token, tokenSource, err := resolveToken(opts.Config)
|
||||
if err != nil {
|
||||
logger.Error("Vault token configuration is invalid", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(os.Getenv(tokenEnv))
|
||||
if token == "" {
|
||||
logger.Error("Vault token missing; expected Vault Agent to export token", zap.String("env", tokenEnv))
|
||||
return nil, merrors.InvalidArgument(component + ": token env " + tokenEnv + " is not set (expected Vault Agent sink to populate it)")
|
||||
logger.Error("Vault token missing", zap.String("source", tokenSource))
|
||||
return nil, merrors.InvalidArgument(component + ": vault token is empty")
|
||||
}
|
||||
|
||||
mountPath := strings.Trim(strings.TrimSpace(opts.Config.MountPath), "/")
|
||||
@@ -148,4 +146,36 @@ func normalizePath(secretPath string) (string, error) {
|
||||
return normalizedPath, nil
|
||||
}
|
||||
|
||||
func resolveToken(config Config) (string, string, error) {
|
||||
tokenEnv := strings.TrimSpace(config.TokenEnv)
|
||||
if tokenEnv != "" {
|
||||
if token := strings.TrimSpace(os.Getenv(tokenEnv)); token != "" {
|
||||
return token, "token_env:" + tokenEnv, nil
|
||||
}
|
||||
}
|
||||
|
||||
tokenFilePath := strings.TrimSpace(config.TokenFile)
|
||||
if tokenFileEnv := strings.TrimSpace(config.TokenFileEnv); tokenFileEnv != "" {
|
||||
if resolved := strings.TrimSpace(os.Getenv(tokenFileEnv)); resolved != "" {
|
||||
tokenFilePath = resolved
|
||||
}
|
||||
}
|
||||
if tokenFilePath != "" {
|
||||
raw, err := os.ReadFile(tokenFilePath)
|
||||
if err != nil {
|
||||
return "", "", merrors.Internal("vault kv: failed to read token file " + tokenFilePath + ": " + err.Error())
|
||||
}
|
||||
return strings.TrimSpace(string(raw)), "token_file:" + tokenFilePath, nil
|
||||
}
|
||||
|
||||
if tokenEnv != "" {
|
||||
return "", "token_env:" + tokenEnv, merrors.InvalidArgument("vault kv: token env " + tokenEnv + " is empty")
|
||||
}
|
||||
if strings.TrimSpace(config.TokenFileEnv) != "" {
|
||||
return "", "token_file_env:" + strings.TrimSpace(config.TokenFileEnv), merrors.InvalidArgument("vault kv: token file env is empty")
|
||||
}
|
||||
|
||||
return "", "", merrors.InvalidArgument("vault kv: either token_env or token_file/token_file_env must be configured")
|
||||
}
|
||||
|
||||
var _ Client = (*service)(nil)
|
||||
|
||||
@@ -10,11 +10,13 @@ import (
|
||||
|
||||
// Config describes how to connect to Vault for managed wallet keys.
|
||||
type Config struct {
|
||||
Address string `mapstructure:"address" yaml:"address"`
|
||||
TokenEnv string `mapstructure:"token_env" yaml:"token_env"`
|
||||
Namespace string `mapstructure:"namespace" yaml:"namespace"`
|
||||
MountPath string `mapstructure:"mount_path" yaml:"mount_path"`
|
||||
KeyPrefix string `mapstructure:"key_prefix" yaml:"key_prefix"`
|
||||
Address string `mapstructure:"address" yaml:"address"`
|
||||
TokenEnv string `mapstructure:"token_env" yaml:"token_env"`
|
||||
TokenFileEnv string `mapstructure:"token_file_env" yaml:"token_file_env"`
|
||||
TokenFile string `mapstructure:"token_file" yaml:"token_file"`
|
||||
Namespace string `mapstructure:"namespace" yaml:"namespace"`
|
||||
MountPath string `mapstructure:"mount_path" yaml:"mount_path"`
|
||||
KeyPrefix string `mapstructure:"key_prefix" yaml:"key_prefix"`
|
||||
}
|
||||
|
||||
// ManagedWalletKey captures metadata returned after key provisioning.
|
||||
|
||||
@@ -38,10 +38,12 @@ func newService(opts Options) (Service, error) {
|
||||
store, err := kv.New(kv.Options{
|
||||
Logger: logger,
|
||||
Config: kv.Config{
|
||||
Address: opts.Config.Address,
|
||||
TokenEnv: opts.Config.TokenEnv,
|
||||
Namespace: opts.Config.Namespace,
|
||||
MountPath: opts.Config.MountPath,
|
||||
Address: opts.Config.Address,
|
||||
TokenEnv: opts.Config.TokenEnv,
|
||||
TokenFileEnv: opts.Config.TokenFileEnv,
|
||||
TokenFile: opts.Config.TokenFile,
|
||||
Namespace: opts.Config.Namespace,
|
||||
MountPath: opts.Config.MountPath,
|
||||
},
|
||||
Component: component,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user