bff for callbacks
This commit is contained in:
@@ -4,6 +4,7 @@ matrix:
|
||||
BFF_DOCKERFILE: ci/prod/compose/bff.dockerfile
|
||||
BFF_MONGO_SECRET_PATH: sendico/db
|
||||
BFF_API_SECRET_PATH: sendico/api/endpoint
|
||||
BFF_VAULT_SECRET_PATH: sendico/edge/bff/vault
|
||||
BFF_ENV: prod
|
||||
|
||||
when:
|
||||
|
||||
@@ -109,6 +109,19 @@ api:
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
callbacks:
|
||||
default_event_types:
|
||||
- payment.status.updated
|
||||
default_status: active
|
||||
secret_path_prefix: sendico/callbacks
|
||||
secret_field: value
|
||||
secret_length_bytes: 32
|
||||
vault:
|
||||
address: "http://dev-vault:8200"
|
||||
token_env: VAULT_TOKEN
|
||||
token_file_env: VAULT_TOKEN_FILE
|
||||
namespace: ""
|
||||
mount_path: kv
|
||||
|
||||
app:
|
||||
|
||||
|
||||
@@ -111,6 +111,19 @@ api:
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
callbacks:
|
||||
default_event_types:
|
||||
- payment.status.updated
|
||||
default_status: active
|
||||
secret_path_prefix: sendico/callbacks
|
||||
secret_field: value
|
||||
secret_length_bytes: 32
|
||||
vault:
|
||||
address: "https://vault.sendico.io"
|
||||
token_env: VAULT_TOKEN
|
||||
token_file_env: VAULT_TOKEN_FILE
|
||||
namespace: ""
|
||||
mount_path: kv
|
||||
|
||||
app:
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ require (
|
||||
github.com/go-chi/metrics v0.1.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tech/sendico/gateway/tron v0.0.0-00010101000000-000000000000
|
||||
@@ -83,11 +84,22 @@ require (
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-chi/chi v1.5.5 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||
github.com/hashicorp/vault/api v1.22.0 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/dsig v1.0.0 // indirect
|
||||
@@ -100,6 +112,7 @@ require (
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
@@ -116,10 +129,10 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
@@ -145,5 +158,6 @@ require (
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
)
|
||||
|
||||
@@ -85,6 +85,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
||||
@@ -98,6 +100,8 @@ github.com/go-chi/jwtauth/v5 v5.4.0 h1:Ieh0xMJsFvqylqJ02/mQHKzbbKO9DYNBh4DPKCwTw
|
||||
github.com/go-chi/jwtauth/v5 v5.4.0/go.mod h1:w6yjqUUXz1b8+oiJel64Sz1KJwduQM6qUA5QNzO5+bQ=
|
||||
github.com/go-chi/metrics v0.1.1 h1:CXhbnkAVVjb0k73EBRQ6Z2YdWFnbXZgNtg1Mboguibk=
|
||||
github.com/go-chi/metrics v0.1.1/go.mod h1:mcGTM1pPalP7WCtb+akNYFO/lwNwBBLCuedepqjoPn4=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -106,6 +110,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
@@ -123,6 +129,29 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||
github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0=
|
||||
github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
@@ -159,6 +188,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
@@ -208,6 +239,8 @@ github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEy
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/vault/kv"
|
||||
mwa "github.com/tech/sendico/server/interface/middleware"
|
||||
fsc "github.com/tech/sendico/server/interface/services/fileservice/config"
|
||||
)
|
||||
@@ -13,6 +14,7 @@ type Config struct {
|
||||
PaymentOrchestrator *PaymentOrchestratorConfig `yaml:"payment_orchestrator"`
|
||||
PaymentQuotation *PaymentOrchestratorConfig `yaml:"payment_quotation"`
|
||||
PaymentMethods *PaymentOrchestratorConfig `yaml:"payment_methods"`
|
||||
Callbacks *CallbacksConfig `yaml:"callbacks"`
|
||||
}
|
||||
|
||||
type ChainGatewayConfig struct {
|
||||
@@ -45,3 +47,12 @@ type PaymentOrchestratorConfig struct {
|
||||
CallTimeoutSeconds int `yaml:"call_timeout_seconds"`
|
||||
Insecure bool `yaml:"insecure"`
|
||||
}
|
||||
|
||||
type CallbacksConfig struct {
|
||||
DefaultEventTypes []string `yaml:"default_event_types"`
|
||||
DefaultStatus string `yaml:"default_status"`
|
||||
SecretPathPrefix string `yaml:"secret_path_prefix"`
|
||||
SecretField string `yaml:"secret_field"`
|
||||
SecretLengthBytes int `yaml:"secret_length_bytes"`
|
||||
Vault kv.Config `yaml:"vault"`
|
||||
}
|
||||
|
||||
11
api/edge/bff/interface/services/callbacks/callbacks.go
Normal file
11
api/edge/bff/interface/services/callbacks/callbacks.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package callbacks
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/callbacksimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return callbacksimp.CreateAPI(a)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/interface/services/account"
|
||||
"github.com/tech/sendico/server/interface/services/callbacks"
|
||||
"github.com/tech/sendico/server/interface/services/invitation"
|
||||
"github.com/tech/sendico/server/interface/services/ledger"
|
||||
"github.com/tech/sendico/server/interface/services/logo"
|
||||
@@ -91,6 +92,7 @@ func (a *APIImp) installServices() error {
|
||||
srvf = append(srvf, wallet.Create)
|
||||
srvf = append(srvf, ledger.Create)
|
||||
srvf = append(srvf, recipient.Create)
|
||||
srvf = append(srvf, callbacks.Create)
|
||||
srvf = append(srvf, paymethod.Create)
|
||||
srvf = append(srvf, payment.Create)
|
||||
|
||||
|
||||
337
api/edge/bff/internal/server/callbacksimp/handlers.go
Normal file
337
api/edge/bff/internal/server/callbacksimp/handlers.go
Normal file
@@ -0,0 +1,337 @@
|
||||
package callbacksimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type callbackWriteResponse struct {
|
||||
AccessToken sresponse.TokenData `json:"accessToken"`
|
||||
Callbacks []model.Callback `json:"callbacks"`
|
||||
GeneratedSigningSecret string `json:"generatedSigningSecret,omitempty"`
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) create(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
|
||||
organizationRef, err := a.Oph.GetRef(r)
|
||||
if err != nil {
|
||||
a.Logger.Warn("Failed to parse organization reference", zap.Error(err), mutil.PLog(a.Oph, r))
|
||||
return response.BadReference(a.Logger, a.Name(), a.Oph.Name(), a.Oph.GetID(r), err)
|
||||
}
|
||||
|
||||
var callback model.Callback
|
||||
if err := json.NewDecoder(r.Body).Decode(&callback); err != nil {
|
||||
a.Logger.Warn("Failed to decode callback payload", zap.Error(err))
|
||||
return response.BadPayload(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
generatedSecret, err := a.normalizeAndPrepare(r.Context(), &callback, organizationRef, true)
|
||||
if err != nil {
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
if err := a.DB.Create(r.Context(), *account.GetID(), organizationRef, &callback); err != nil {
|
||||
a.Logger.Warn("Failed to create callback", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
return a.callbackResponse(&callback, accessToken, generatedSecret, true)
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) update(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
|
||||
var input model.Callback
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
a.Logger.Warn("Failed to decode callback payload", zap.Error(err))
|
||||
return response.BadPayload(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
callbackRef := *input.GetID()
|
||||
if callbackRef.IsZero() {
|
||||
return response.Auto(a.Logger, a.Name(), merrors.InvalidArgument("callback id is required", "id"))
|
||||
}
|
||||
|
||||
var existing model.Callback
|
||||
if err := a.db.Get(r.Context(), *account.GetID(), callbackRef, &existing); err != nil {
|
||||
a.Logger.Warn("Failed to fetch callback before update", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
mergeCallbackMutable(&existing, &input)
|
||||
generatedSecret, err := a.normalizeAndPrepare(r.Context(), &existing, existing.OrganizationRef, true)
|
||||
if err != nil {
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
if err := a.DB.Update(r.Context(), *account.GetID(), &existing); err != nil {
|
||||
a.Logger.Warn("Failed to update callback", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
return a.callbackResponse(&existing, accessToken, generatedSecret, false)
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) rotateSecret(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
|
||||
callbackRef, err := a.Cph.GetRef(r)
|
||||
if err != nil {
|
||||
a.Logger.Warn("Failed to parse callback reference", zap.Error(err), mutil.PLog(a.Cph, r))
|
||||
return response.BadReference(a.Logger, a.Name(), a.Cph.Name(), a.Cph.GetID(r), err)
|
||||
}
|
||||
|
||||
var callback model.Callback
|
||||
if err := a.db.Get(r.Context(), *account.GetID(), callbackRef, &callback); err != nil {
|
||||
a.Logger.Warn("Failed to fetch callback for secret rotation", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
if callback.RetryPolicy.SigningMode != model.CallbackSigningModeHMACSHA256 {
|
||||
return response.BadRequest(a.Logger, a.Name(), "invalid_signing_mode", "rotate-secret is available only for hmac_sha256 callbacks")
|
||||
}
|
||||
|
||||
secretRef, generatedSecret, err := a.secrets.Provision(r.Context(), callback.OrganizationRef, callbackRef)
|
||||
if err != nil {
|
||||
a.Logger.Warn("Failed to rotate callback signing secret", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
callback.RetryPolicy.SecretRef = secretRef
|
||||
|
||||
if err := a.DB.Update(r.Context(), *account.GetID(), &callback); err != nil {
|
||||
a.Logger.Warn("Failed to persist rotated callback secret reference", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
return a.callbackResponse(&callback, accessToken, generatedSecret, false)
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) normalizeAndPrepare(
|
||||
ctx context.Context,
|
||||
callback *model.Callback,
|
||||
organizationRef bson.ObjectID,
|
||||
allowSecretGeneration bool,
|
||||
) (string, error) {
|
||||
if callback == nil {
|
||||
return "", merrors.InvalidArgument("callback payload is required")
|
||||
}
|
||||
if organizationRef.IsZero() {
|
||||
return "", merrors.InvalidArgument("organization reference is required", "organizationRef")
|
||||
}
|
||||
|
||||
callback.SetOrganizationRef(organizationRef)
|
||||
callback.Name = strings.TrimSpace(callback.Name)
|
||||
callback.Description = trimDescription(callback.Description)
|
||||
callback.ClientID = strings.TrimSpace(callback.ClientID)
|
||||
if callback.ClientID == "" {
|
||||
callback.ClientID = organizationRef.Hex()
|
||||
}
|
||||
|
||||
callback.URL = strings.TrimSpace(callback.URL)
|
||||
if callback.URL == "" {
|
||||
return "", merrors.InvalidArgument("url is required", "url")
|
||||
}
|
||||
if err := validateCallbackURL(callback.URL); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if callback.Name == "" {
|
||||
callback.Name = callback.URL
|
||||
}
|
||||
|
||||
status, err := normalizeStatus(callback.Status, a.config.DefaultStatus)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
callback.Status = status
|
||||
callback.EventTypes = normalizeEventTypes(callback.EventTypes, a.config.DefaultEventTypes)
|
||||
|
||||
callback.RetryPolicy.MinDelayMS = defaultInt(callback.RetryPolicy.MinDelayMS, defaultRetryMinDelayMS)
|
||||
callback.RetryPolicy.MaxDelayMS = defaultInt(callback.RetryPolicy.MaxDelayMS, defaultRetryMaxDelayMS)
|
||||
if callback.RetryPolicy.MaxDelayMS < callback.RetryPolicy.MinDelayMS {
|
||||
callback.RetryPolicy.MaxDelayMS = callback.RetryPolicy.MinDelayMS
|
||||
}
|
||||
callback.RetryPolicy.MaxAttempts = defaultInt(callback.RetryPolicy.MaxAttempts, defaultRetryMaxAttempts)
|
||||
callback.RetryPolicy.RequestTimeoutMS = defaultInt(callback.RetryPolicy.RequestTimeoutMS, defaultRetryRequestTimeoutMS)
|
||||
callback.RetryPolicy.Headers = normalizeHeaders(callback.RetryPolicy.Headers)
|
||||
|
||||
mode, err := normalizeSigningMode(callback.RetryPolicy.SigningMode)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
callback.RetryPolicy.SigningMode = mode
|
||||
|
||||
callback.RetryPolicy.SecretRef = strings.TrimSpace(callback.RetryPolicy.SecretRef)
|
||||
switch callback.RetryPolicy.SigningMode {
|
||||
case model.CallbackSigningModeNone:
|
||||
callback.RetryPolicy.SecretRef = ""
|
||||
return "", nil
|
||||
case model.CallbackSigningModeHMACSHA256:
|
||||
if callback.RetryPolicy.SecretRef != "" {
|
||||
return "", nil
|
||||
}
|
||||
if !allowSecretGeneration {
|
||||
return "", merrors.InvalidArgument("secretRef is required for hmac_sha256 callbacks", "retryPolicy.secretRef")
|
||||
}
|
||||
if callback.GetID().IsZero() {
|
||||
callback.SetID(bson.NewObjectID())
|
||||
}
|
||||
secretRef, generatedSecret, err := a.secrets.Provision(ctx, organizationRef, *callback.GetID())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
callback.RetryPolicy.SecretRef = secretRef
|
||||
return generatedSecret, nil
|
||||
default:
|
||||
return "", merrors.InvalidArgument("unsupported signing mode", "retryPolicy.signingMode")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) callbackResponse(
|
||||
callback *model.Callback,
|
||||
accessToken *sresponse.TokenData,
|
||||
generatedSecret string,
|
||||
created bool,
|
||||
) http.HandlerFunc {
|
||||
if callback == nil || accessToken == nil {
|
||||
return response.Internal(a.Logger, a.Name(), merrors.Internal("failed to build callback response"))
|
||||
}
|
||||
|
||||
resp := callbackWriteResponse{
|
||||
AccessToken: *accessToken,
|
||||
Callbacks: []model.Callback{*callback},
|
||||
GeneratedSigningSecret: generatedSecret,
|
||||
}
|
||||
if created {
|
||||
return response.Created(a.Logger, resp)
|
||||
}
|
||||
return response.Ok(a.Logger, resp)
|
||||
}
|
||||
|
||||
func normalizeStatus(raw, fallback model.CallbackStatus) (model.CallbackStatus, error) {
|
||||
candidate := strings.ToLower(strings.TrimSpace(string(raw)))
|
||||
if candidate == "" {
|
||||
candidate = strings.ToLower(strings.TrimSpace(string(fallback)))
|
||||
}
|
||||
|
||||
switch candidate {
|
||||
case "", "active", "enabled":
|
||||
return model.CallbackStatusActive, nil
|
||||
case "disabled", "inactive":
|
||||
return model.CallbackStatusDisabled, nil
|
||||
default:
|
||||
return "", merrors.InvalidArgument("unsupported callback status", "status")
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeSigningMode(raw model.CallbackSigningMode) (model.CallbackSigningMode, error) {
|
||||
mode := strings.ToLower(strings.TrimSpace(string(raw)))
|
||||
switch mode {
|
||||
case "", "none":
|
||||
return model.CallbackSigningModeNone, nil
|
||||
case "hmac_sha256", "hmac-sha256", "hmac":
|
||||
return model.CallbackSigningModeHMACSHA256, nil
|
||||
default:
|
||||
return "", merrors.InvalidArgument("unsupported callback signing mode", "retryPolicy.signingMode")
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeEventTypes(eventTypes []string, defaults []string) []string {
|
||||
if len(eventTypes) == 0 {
|
||||
return normalizeEventTypes(defaults, nil)
|
||||
}
|
||||
seen := make(map[string]struct{}, len(eventTypes))
|
||||
out := make([]string, 0, len(eventTypes))
|
||||
for _, eventType := range eventTypes {
|
||||
value := strings.TrimSpace(eventType)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[value]; exists {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
out = append(out, value)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
if len(defaults) > 0 {
|
||||
return normalizeEventTypes(defaults, nil)
|
||||
}
|
||||
return []string{model.PaymentStatusUpdatedType}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeHeaders(headers map[string]string) map[string]string {
|
||||
if len(headers) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(headers))
|
||||
for key, value := range headers {
|
||||
k := strings.TrimSpace(key)
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
out[k] = strings.TrimSpace(value)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeCallbackMutable(dst, src *model.Callback) {
|
||||
dst.Describable = src.Describable
|
||||
dst.ClientID = src.ClientID
|
||||
dst.Status = src.Status
|
||||
dst.URL = src.URL
|
||||
dst.EventTypes = append([]string(nil), src.EventTypes...)
|
||||
dst.RetryPolicy = model.CallbackRetryPolicy{
|
||||
MinDelayMS: src.RetryPolicy.MinDelayMS,
|
||||
MaxDelayMS: src.RetryPolicy.MaxDelayMS,
|
||||
SigningMode: src.RetryPolicy.SigningMode,
|
||||
SecretRef: src.RetryPolicy.SecretRef,
|
||||
Headers: normalizeHeaders(src.RetryPolicy.Headers),
|
||||
MaxAttempts: src.RetryPolicy.MaxAttempts,
|
||||
RequestTimeoutMS: src.RetryPolicy.RequestTimeoutMS,
|
||||
}
|
||||
}
|
||||
|
||||
func defaultInt(value, fallback int) int {
|
||||
if value > 0 {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func trimDescription(in *string) *string {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
value := strings.TrimSpace(*in)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
func validateCallbackURL(raw string) error {
|
||||
parsed, err := url.ParseRequestURI(raw)
|
||||
if err != nil {
|
||||
return merrors.InvalidArgument("url is invalid", "url")
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
|
||||
case "https", "http":
|
||||
default:
|
||||
return merrors.InvalidArgument("url scheme must be http or https", "url")
|
||||
}
|
||||
if strings.TrimSpace(parsed.Host) == "" {
|
||||
return merrors.InvalidArgument("url host is required", "url")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
178
api/edge/bff/internal/server/callbacksimp/secrets.go
Normal file
178
api/edge/bff/internal/server/callbacksimp/secrets.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package callbacksimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/vault/kv"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type signingSecretManager interface {
|
||||
Provision(ctx context.Context, organizationRef, callbackRef bson.ObjectID) (secretRef string, generatedSecret string, err error)
|
||||
}
|
||||
|
||||
type vaultSigningSecretManager struct {
|
||||
logger mlogger.Logger
|
||||
store kv.Client
|
||||
pathPrefix string
|
||||
field string
|
||||
secretLength int
|
||||
}
|
||||
|
||||
const (
|
||||
metricsResultSuccess = "success"
|
||||
metricsResultError = "error"
|
||||
)
|
||||
|
||||
var (
|
||||
signingSecretMetricsOnce sync.Once
|
||||
signingSecretStatus *prometheus.CounterVec
|
||||
signingSecretLatency *prometheus.HistogramVec
|
||||
)
|
||||
|
||||
func ensureSigningSecretMetrics() {
|
||||
signingSecretMetricsOnce.Do(func() {
|
||||
signingSecretStatus = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "sendico",
|
||||
Subsystem: "bff_callbacks",
|
||||
Name: "signing_secret_provision_total",
|
||||
Help: "Total callback signing secret provisioning attempts.",
|
||||
}, []string{"result"})
|
||||
signingSecretLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "sendico",
|
||||
Subsystem: "bff_callbacks",
|
||||
Name: "signing_secret_provision_duration_seconds",
|
||||
Help: "Duration of callback signing secret provisioning attempts.",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
}, []string{"result"})
|
||||
})
|
||||
}
|
||||
|
||||
func newSigningSecretManager(logger mlogger.Logger, cfg callbacksConfig) (signingSecretManager, error) {
|
||||
if err := cfg.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
|
||||
manager := &vaultSigningSecretManager{
|
||||
logger: logger.Named("callbacks_secrets"),
|
||||
pathPrefix: strings.Trim(strings.TrimSpace(cfg.SecretPathPrefix), "/"),
|
||||
field: strings.TrimSpace(cfg.SecretField),
|
||||
secretLength: cfg.SecretLengthBytes,
|
||||
}
|
||||
if manager.pathPrefix == "" {
|
||||
manager.pathPrefix = defaultSigningSecretPathPrefix
|
||||
}
|
||||
if manager.field == "" {
|
||||
manager.field = defaultSigningSecretField
|
||||
}
|
||||
|
||||
if isVaultConfigEmpty(cfg.Vault) {
|
||||
manager.logger.Warn("Callbacks Vault config is not set; secret generation requires explicit secretRef in payloads")
|
||||
ensureSigningSecretMetrics()
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
store, err := kv.New(kv.Options{
|
||||
Logger: manager.logger,
|
||||
Config: kv.Config{
|
||||
Address: strings.TrimSpace(cfg.Vault.Address),
|
||||
TokenEnv: strings.TrimSpace(cfg.Vault.TokenEnv),
|
||||
TokenFileEnv: strings.TrimSpace(cfg.Vault.TokenFileEnv),
|
||||
TokenFile: strings.TrimSpace(cfg.Vault.TokenFile),
|
||||
Namespace: strings.TrimSpace(cfg.Vault.Namespace),
|
||||
MountPath: strings.TrimSpace(cfg.Vault.MountPath),
|
||||
},
|
||||
Component: "bff callbacks signing secret manager",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manager.store = store
|
||||
ensureSigningSecretMetrics()
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
func (m *vaultSigningSecretManager) Provision(
|
||||
ctx context.Context,
|
||||
organizationRef,
|
||||
callbackRef bson.ObjectID,
|
||||
) (string, string, error) {
|
||||
start := time.Now()
|
||||
result := metricsResultSuccess
|
||||
defer func() {
|
||||
signingSecretStatus.WithLabelValues(result).Inc()
|
||||
signingSecretLatency.WithLabelValues(result).Observe(time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
if organizationRef.IsZero() {
|
||||
result = metricsResultError
|
||||
return "", "", merrors.InvalidArgument("organization reference is required", "organizationRef")
|
||||
}
|
||||
if callbackRef.IsZero() {
|
||||
result = metricsResultError
|
||||
return "", "", merrors.InvalidArgument("callback reference is required", "callbackRef")
|
||||
}
|
||||
if m.store == nil {
|
||||
result = metricsResultError
|
||||
return "", "", merrors.InvalidArgument("callbacks vault config is required to generate signing secrets", "api.callbacks.vault")
|
||||
}
|
||||
|
||||
secret, err := generateSigningSecret(m.secretLength)
|
||||
if err != nil {
|
||||
result = metricsResultError
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
secretPath := path.Join(m.pathPrefix, organizationRef.Hex(), callbackRef.Hex())
|
||||
payload := map[string]interface{}{
|
||||
m.field: secret,
|
||||
"organization_ref": organizationRef.Hex(),
|
||||
"callback_ref": callbackRef.Hex(),
|
||||
"updated_at": time.Now().UTC().Format(time.RFC3339Nano),
|
||||
}
|
||||
if err := m.store.Put(ctx, secretPath, payload); err != nil {
|
||||
result = metricsResultError
|
||||
m.logger.Warn("Failed to store callback signing secret", zap.String("path", secretPath), zap.Error(err))
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
secretRef := "vault:" + secretPath + "#" + m.field
|
||||
m.logger.Info("Callback signing secret stored", zap.String("secret_ref", secretRef), zap.String("callback_ref", callbackRef.Hex()))
|
||||
|
||||
return secretRef, secret, nil
|
||||
}
|
||||
|
||||
func isVaultConfigEmpty(cfg VaultConfig) bool {
|
||||
return strings.TrimSpace(cfg.Address) == "" &&
|
||||
strings.TrimSpace(cfg.TokenEnv) == "" &&
|
||||
strings.TrimSpace(cfg.TokenFileEnv) == "" &&
|
||||
strings.TrimSpace(cfg.TokenFile) == "" &&
|
||||
strings.TrimSpace(cfg.MountPath) == "" &&
|
||||
strings.TrimSpace(cfg.Namespace) == ""
|
||||
}
|
||||
|
||||
func generateSigningSecret(length int) (string, error) {
|
||||
if length <= 0 {
|
||||
return "", merrors.InvalidArgument("secret length must be greater than zero", "secret_length")
|
||||
}
|
||||
raw := make([]byte, length)
|
||||
if _, err := rand.Read(raw); err != nil {
|
||||
return "", merrors.Internal("failed to generate signing secret: " + err.Error())
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(raw), nil
|
||||
}
|
||||
139
api/edge/bff/internal/server/callbacksimp/service.go
Normal file
139
api/edge/bff/internal/server/callbacksimp/service.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package callbacksimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/db/callbacks"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
eapi "github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/papitemplate"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CallbacksAPI struct {
|
||||
papitemplate.ProtectedAPI[model.Callback]
|
||||
db callbacks.DB
|
||||
secrets signingSecretManager
|
||||
config callbacksConfig
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) Name() mservice.Type {
|
||||
return mservice.Callbacks
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) Finish(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateAPI(apiCtx eapi.API) (*CallbacksAPI, error) {
|
||||
dbFactory := func() (papitemplate.ProtectedDB[model.Callback], error) {
|
||||
return apiCtx.DBFactory().NewCallbacksDB()
|
||||
}
|
||||
|
||||
res := &CallbacksAPI{
|
||||
config: newCallbacksConfig(apiCtx.Config().Callbacks),
|
||||
}
|
||||
|
||||
p, err := papitemplate.CreateAPI(apiCtx, dbFactory, mservice.Organizations, mservice.Callbacks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.ProtectedAPI = *p.
|
||||
WithNoCreateNotification().
|
||||
WithNoUpdateNotification().
|
||||
WithNoDeleteNotification().
|
||||
WithCreateHandler(res.create).
|
||||
WithUpdateHandler(res.update).
|
||||
Build()
|
||||
|
||||
if res.db, err = apiCtx.DBFactory().NewCallbacksDB(); err != nil {
|
||||
res.Logger.Warn("Failed to create callbacks database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.secrets, err = newSigningSecretManager(res.Logger, res.config); err != nil {
|
||||
res.Logger.Warn("Failed to initialize callbacks signing secret manager", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiCtx.Register().AccountHandler(res.Name(), res.Cph.AddRef("/rotate-secret"), api.Post, res.rotateSecret)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
const (
|
||||
defaultCallbackStatus = model.CallbackStatusActive
|
||||
defaultRetryMaxAttempts = 8
|
||||
defaultRetryMinDelayMS = 1000
|
||||
defaultRetryMaxDelayMS = 300000
|
||||
defaultRetryRequestTimeoutMS = 10000
|
||||
defaultSigningSecretLengthBytes = 32
|
||||
defaultSigningSecretField = "value"
|
||||
defaultSigningSecretPathPrefix = "sendico/callbacks"
|
||||
)
|
||||
|
||||
type callbacksConfig struct {
|
||||
DefaultEventTypes []string
|
||||
DefaultStatus model.CallbackStatus
|
||||
SecretPathPrefix string
|
||||
SecretField string
|
||||
SecretLengthBytes int
|
||||
Vault VaultConfig
|
||||
}
|
||||
|
||||
type VaultConfig struct {
|
||||
Address string
|
||||
TokenEnv string
|
||||
TokenFileEnv string
|
||||
TokenFile string
|
||||
Namespace string
|
||||
MountPath string
|
||||
}
|
||||
|
||||
func newCallbacksConfig(source *eapi.CallbacksConfig) callbacksConfig {
|
||||
cfg := callbacksConfig{
|
||||
DefaultEventTypes: []string{model.PaymentStatusUpdatedType},
|
||||
DefaultStatus: defaultCallbackStatus,
|
||||
SecretPathPrefix: defaultSigningSecretPathPrefix,
|
||||
SecretField: defaultSigningSecretField,
|
||||
SecretLengthBytes: defaultSigningSecretLengthBytes,
|
||||
}
|
||||
if source == nil {
|
||||
return cfg
|
||||
}
|
||||
|
||||
if source.SecretPathPrefix != "" {
|
||||
cfg.SecretPathPrefix = source.SecretPathPrefix
|
||||
}
|
||||
if source.SecretField != "" {
|
||||
cfg.SecretField = source.SecretField
|
||||
}
|
||||
if source.SecretLengthBytes > 0 {
|
||||
cfg.SecretLengthBytes = source.SecretLengthBytes
|
||||
}
|
||||
if len(source.DefaultEventTypes) > 0 {
|
||||
cfg.DefaultEventTypes = source.DefaultEventTypes
|
||||
}
|
||||
if source.DefaultStatus != "" {
|
||||
cfg.DefaultStatus = model.CallbackStatus(source.DefaultStatus)
|
||||
}
|
||||
cfg.Vault = VaultConfig{
|
||||
Address: source.Vault.Address,
|
||||
TokenEnv: source.Vault.TokenEnv,
|
||||
TokenFileEnv: source.Vault.TokenFileEnv,
|
||||
TokenFile: source.Vault.TokenFile,
|
||||
Namespace: source.Vault.Namespace,
|
||||
MountPath: source.Vault.MountPath,
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (c callbacksConfig) validate() error {
|
||||
if c.SecretLengthBytes <= 0 {
|
||||
return merrors.InvalidArgument("callbacks signing secret length must be greater than zero", "api.callbacks.secret_length_bytes")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
mutil "github.com/tech/sendico/pkg/mutil/db"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
@@ -22,7 +23,7 @@ import (
|
||||
const (
|
||||
inboxCollection string = "inbox"
|
||||
tasksCollection string = "tasks"
|
||||
endpointsCollection string = "endpoints"
|
||||
endpointsCollection string = mservice.Callbacks
|
||||
)
|
||||
|
||||
type mongoRepository struct {
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
@@ -65,7 +65,7 @@ Examples:
|
||||
|
||||
Infrastructure (MongoDB, NATS) uses plain `.env.dev` credentials.
|
||||
|
||||
Callbacks, Chain, and TRON run Vault Agent sidecars with AppRole.
|
||||
Callbacks, BFF, Chain, and TRON run Vault Agent sidecars with AppRole.
|
||||
Set the corresponding `*_VAULT_ROLE_ID` and `*_VAULT_SECRET_ID` values in `.env.dev`.
|
||||
|
||||
## Network
|
||||
|
||||
21
ci/dev/vault-agent/bff.hcl
Normal file
21
ci/dev/vault-agent/bff.hcl
Normal file
@@ -0,0 +1,21 @@
|
||||
vault {
|
||||
address = "http://dev-vault:8200"
|
||||
}
|
||||
|
||||
auto_auth {
|
||||
method "approle" {
|
||||
mount_path = "auth/approle"
|
||||
config = {
|
||||
role_id_file_path = "/run/vault/role_id"
|
||||
secret_id_file_path = "/run/vault/secret_id"
|
||||
}
|
||||
}
|
||||
|
||||
sink "file" {
|
||||
config = {
|
||||
path = "/run/vault/token"
|
||||
mode = 0600
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +181,7 @@ BFF_DIR=bff
|
||||
BFF_COMPOSE_PROJECT=sendico-bff
|
||||
BFF_SERVICE_NAME=sendico_bff
|
||||
BFF_HTTP_PORT=8080
|
||||
BFF_VAULT_SECRET_PATH=sendico/edge/bff/vault
|
||||
|
||||
# Callbacks service
|
||||
CALLBACKS_DIR=callbacks
|
||||
|
||||
@@ -5,6 +5,14 @@ x-common-env: &common-env
|
||||
- ../env/.env.runtime
|
||||
- ../env/.env.version
|
||||
|
||||
volumes:
|
||||
bff-vault-run:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: tmpfs
|
||||
device: tmpfs
|
||||
o: size=8m,uid=0,gid=0,mode=0700
|
||||
|
||||
networks:
|
||||
sendico-net:
|
||||
external: true
|
||||
@@ -51,8 +59,15 @@ services:
|
||||
PERMISSION_COLLECTION: ${PERMISSION_COLLECTION}
|
||||
PERMISSION_TIMEOUT: ${PERMISSION_TIMEOUT}
|
||||
PERMISSION_IS_FILTERED: ${PERMISSION_IS_FILTERED}
|
||||
VAULT_ADDR: ${VAULT_ADDR}
|
||||
VAULT_TOKEN_FILE: /run/vault/token
|
||||
ports:
|
||||
- "0.0.0.0:${BFF_HTTP_PORT}:8081"
|
||||
volumes:
|
||||
- bff-vault-run:/run/vault:ro
|
||||
depends_on:
|
||||
sendico_bff_vault_agent:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL","wget -qO- http://localhost:8081/api/v1/health | grep -q '\"status\":\"ok\"'"]
|
||||
interval: 30s
|
||||
@@ -61,3 +76,32 @@ services:
|
||||
start_period: 60s
|
||||
networks:
|
||||
- sendico-net
|
||||
|
||||
sendico_bff_vault_agent:
|
||||
<<: *common-env
|
||||
container_name: sendico-bff-vault-agent
|
||||
restart: unless-stopped
|
||||
image: hashicorp/vault:latest
|
||||
pull_policy: always
|
||||
cap_add: ["IPC_LOCK"]
|
||||
environment:
|
||||
VAULT_ADDR: ${VAULT_ADDR}
|
||||
BFF_VAULT_ROLE_ID: ${BFF_VAULT_ROLE_ID}
|
||||
BFF_VAULT_SECRET_ID: ${BFF_VAULT_SECRET_ID}
|
||||
command: >
|
||||
sh -lc 'set -euo pipefail; umask 077;
|
||||
: "${BFF_VAULT_ROLE_ID:?}"; : "${BFF_VAULT_SECRET_ID:?}";
|
||||
printf "%s" "$BFF_VAULT_ROLE_ID" > /run/vault/role_id;
|
||||
printf "%s" "$BFF_VAULT_SECRET_ID" > /run/vault/secret_id;
|
||||
unset BFF_VAULT_ROLE_ID BFF_VAULT_SECRET_ID;
|
||||
exec vault agent -config=/etc/vault/agent/bff.hcl'
|
||||
volumes:
|
||||
- ./vault-agent/bff.hcl:/etc/vault/agent/bff.hcl:ro
|
||||
- bff-vault-run:/run/vault
|
||||
healthcheck:
|
||||
test: ["CMD","test","-s","/run/vault/token"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 6
|
||||
networks:
|
||||
- sendico-net
|
||||
|
||||
21
ci/prod/compose/vault-agent/bff.hcl
Normal file
21
ci/prod/compose/vault-agent/bff.hcl
Normal file
@@ -0,0 +1,21 @@
|
||||
vault {
|
||||
address = "https://vault.sendico.io"
|
||||
}
|
||||
|
||||
auto_auth {
|
||||
method "approle" {
|
||||
mount_path = "auth/approle"
|
||||
config = {
|
||||
role_id_file_path = "/run/vault/role_id"
|
||||
secret_id_file_path = "/run/vault/secret_id"
|
||||
}
|
||||
}
|
||||
|
||||
sink "file" {
|
||||
config = {
|
||||
path = "/run/vault/token"
|
||||
mode = 0600
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ REQUIRED_SECRETS=(
|
||||
MONGO_USER
|
||||
MONGO_PASSWORD
|
||||
API_ENDPOINT_SECRET
|
||||
BFF_VAULT_ROLE_ID
|
||||
BFF_VAULT_SECRET_ID
|
||||
NATS_USER
|
||||
NATS_PASSWORD
|
||||
NATS_URL
|
||||
@@ -43,6 +45,8 @@ b64enc() {
|
||||
MONGO_USER_B64="$(b64enc "${MONGO_USER}")"
|
||||
MONGO_PASSWORD_B64="$(b64enc "${MONGO_PASSWORD}")"
|
||||
API_ENDPOINT_SECRET_B64="$(b64enc "${API_ENDPOINT_SECRET}")"
|
||||
BFF_VAULT_ROLE_ID_B64="$(b64enc "${BFF_VAULT_ROLE_ID}")"
|
||||
BFF_VAULT_SECRET_ID_B64="$(b64enc "${BFF_VAULT_SECRET_ID}")"
|
||||
NATS_USER_B64="$(b64enc "${NATS_USER}")"
|
||||
NATS_PASSWORD_B64="$(b64enc "${NATS_PASSWORD}")"
|
||||
NATS_URL_B64="$(b64enc "${NATS_URL}")"
|
||||
@@ -77,6 +81,8 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" \
|
||||
MONGO_USER_B64="$MONGO_USER_B64" \
|
||||
MONGO_PASSWORD_B64="$MONGO_PASSWORD_B64" \
|
||||
API_ENDPOINT_SECRET_B64="$API_ENDPOINT_SECRET_B64" \
|
||||
BFF_VAULT_ROLE_ID_B64="$BFF_VAULT_ROLE_ID_B64" \
|
||||
BFF_VAULT_SECRET_ID_B64="$BFF_VAULT_SECRET_ID_B64" \
|
||||
NATS_USER_B64="$NATS_USER_B64" \
|
||||
NATS_PASSWORD_B64="$NATS_PASSWORD_B64" \
|
||||
NATS_URL_B64="$NATS_URL_B64" \
|
||||
@@ -124,11 +130,14 @@ decode_b64() {
|
||||
MONGO_USER="$(decode_b64 "$MONGO_USER_B64")"
|
||||
MONGO_PASSWORD="$(decode_b64 "$MONGO_PASSWORD_B64")"
|
||||
API_ENDPOINT_SECRET="$(decode_b64 "$API_ENDPOINT_SECRET_B64")"
|
||||
BFF_VAULT_ROLE_ID="$(decode_b64 "$BFF_VAULT_ROLE_ID_B64")"
|
||||
BFF_VAULT_SECRET_ID="$(decode_b64 "$BFF_VAULT_SECRET_ID_B64")"
|
||||
NATS_USER="$(decode_b64 "$NATS_USER_B64")"
|
||||
NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")"
|
||||
NATS_URL="$(decode_b64 "$NATS_URL_B64")"
|
||||
|
||||
export MONGO_USER MONGO_PASSWORD API_ENDPOINT_SECRET
|
||||
export BFF_VAULT_ROLE_ID BFF_VAULT_SECRET_ID
|
||||
export NATS_USER NATS_PASSWORD NATS_URL
|
||||
COMPOSE_PROJECT_NAME="$COMPOSE_PROJECT"
|
||||
export COMPOSE_PROJECT_NAME
|
||||
|
||||
@@ -50,11 +50,18 @@ load_env_file ./.env.version
|
||||
|
||||
BFF_MONGO_SECRET_PATH="${BFF_MONGO_SECRET_PATH:?missing BFF_MONGO_SECRET_PATH}"
|
||||
BFF_API_SECRET_PATH="${BFF_API_SECRET_PATH:?missing BFF_API_SECRET_PATH}"
|
||||
BFF_VAULT_SECRET_PATH="${BFF_VAULT_SECRET_PATH:?missing BFF_VAULT_SECRET_PATH}"
|
||||
|
||||
export MONGO_USER="$(./ci/vlt kv_get kv "${BFF_MONGO_SECRET_PATH}" user)"
|
||||
export MONGO_PASSWORD="$(./ci/vlt kv_get kv "${BFF_MONGO_SECRET_PATH}" password)"
|
||||
|
||||
export API_ENDPOINT_SECRET="$(./ci/vlt kv_get kv "${BFF_API_SECRET_PATH}" secret)"
|
||||
export BFF_VAULT_ROLE_ID="$(./ci/vlt kv_get kv "${BFF_VAULT_SECRET_PATH}" role_id)"
|
||||
export BFF_VAULT_SECRET_ID="$(./ci/vlt kv_get kv "${BFF_VAULT_SECRET_PATH}" secret_id)"
|
||||
if [ -z "${BFF_VAULT_ROLE_ID}" ] || [ -z "${BFF_VAULT_SECRET_ID}" ]; then
|
||||
echo "[bff-deploy] vault approle creds are empty for path ${BFF_VAULT_SECRET_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
load_nats_env
|
||||
|
||||
|
||||
@@ -35,6 +35,12 @@ volumes:
|
||||
type: tmpfs
|
||||
device: tmpfs
|
||||
o: size=8m,uid=0,gid=0,mode=0700
|
||||
dev-bff-vault-run:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: tmpfs
|
||||
device: tmpfs
|
||||
o: size=8m,uid=0,gid=0,mode=0700
|
||||
|
||||
# ============================================================================
|
||||
# INFRASTRUCTURE SERVICES
|
||||
@@ -923,6 +929,39 @@ services:
|
||||
VAULT_ADDR: ${VAULT_ADDR}
|
||||
VAULT_TOKEN_FILE: /run/vault/token
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# BFF Vault Agent (sidecar for AppRole authentication)
|
||||
# --------------------------------------------------------------------------
|
||||
dev-bff-vault-agent:
|
||||
<<: *common-env
|
||||
image: hashicorp/vault:latest
|
||||
container_name: dev-bff-vault-agent
|
||||
restart: unless-stopped
|
||||
cap_add: ["IPC_LOCK"]
|
||||
environment:
|
||||
VAULT_ADDR: ${VAULT_ADDR}
|
||||
BFF_VAULT_ROLE_ID: ${BFF_VAULT_ROLE_ID}
|
||||
BFF_VAULT_SECRET_ID: ${BFF_VAULT_SECRET_ID}
|
||||
command: >
|
||||
sh -c 'set -eu; umask 077;
|
||||
: "$$BFF_VAULT_ROLE_ID"; : "$$BFF_VAULT_SECRET_ID";
|
||||
echo "$$BFF_VAULT_ROLE_ID" > /run/vault/role_id;
|
||||
echo "$$BFF_VAULT_SECRET_ID" > /run/vault/secret_id;
|
||||
unset BFF_VAULT_ROLE_ID BFF_VAULT_SECRET_ID;
|
||||
exec vault agent -config=/etc/vault/agent/bff.hcl'
|
||||
volumes:
|
||||
- ./ci/dev/vault-agent/bff.hcl:/etc/vault/agent/bff.hcl:ro
|
||||
- dev-bff-vault-run:/run/vault
|
||||
depends_on:
|
||||
dev-vault: { condition: service_healthy }
|
||||
healthcheck:
|
||||
test: ["CMD", "test", "-s", "/run/vault/token"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 6
|
||||
networks:
|
||||
- sendico-dev
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# BFF (Backend for Frontend / Server) Service
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -942,9 +981,11 @@ services:
|
||||
dev-payments-quotation: { condition: service_started }
|
||||
dev-payments-methods: { condition: service_started }
|
||||
dev-chain-gateway: { condition: service_started }
|
||||
dev-bff-vault-agent: { condition: service_healthy }
|
||||
volumes:
|
||||
- ./api/edge/bff:/src/api/edge/bff
|
||||
- ./api/edge/bff/config.dev.yml:/app/config.yml:ro
|
||||
- dev-bff-vault-run:/run/vault:ro
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
@@ -977,6 +1018,8 @@ services:
|
||||
API_PROTOCOL: http
|
||||
SERVICE_HOST: localhost
|
||||
API_ENDPOINT: /api/v1
|
||||
VAULT_ADDR: ${VAULT_ADDR}
|
||||
VAULT_TOKEN_FILE: /run/vault/token
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Frontend (Flutter Web)
|
||||
|
||||
@@ -23,6 +23,8 @@ tags:
|
||||
description: Recipient CRUD and archive flows
|
||||
- name: Payment Methods
|
||||
description: Payment method CRUD and archive flows
|
||||
- name: Callbacks
|
||||
description: Webhook callback subscription CRUD and signing secret rotation
|
||||
- name: Payments
|
||||
description: Quotation and payment orchestration
|
||||
|
||||
@@ -70,6 +72,19 @@ paths:
|
||||
/payment_methods/archive/{organizations_ref}/{payment_methods_ref}:
|
||||
$ref: ./api/payment_methods/archive.yaml
|
||||
|
||||
/callbacks/list/{org_ref}/{organizations_ref}:
|
||||
$ref: ./api/callbacks/list.yaml
|
||||
/callbacks/{org_ref}:
|
||||
$ref: ./api/callbacks/create.yaml
|
||||
/callbacks/{callbacks_ref}:
|
||||
$ref: ./api/callbacks/object.yaml
|
||||
/callbacks:
|
||||
$ref: ./api/callbacks/update.yaml
|
||||
/callbacks/archive/{org_ref}/{callbacks_ref}:
|
||||
$ref: ./api/callbacks/archive.yaml
|
||||
/callbacks/rotate-secret/{callbacks_ref}:
|
||||
$ref: ./api/callbacks/rotate_secret.yaml
|
||||
|
||||
/payments/quote/{organizations_ref}:
|
||||
$ref: ./api/payments/quote.yaml
|
||||
/payments/multiquote/{organizations_ref}:
|
||||
|
||||
38
interface/api/callbacks/archive.yaml
Normal file
38
interface/api/callbacks/archive.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
get:
|
||||
tags: [Callbacks]
|
||||
summary: Archive/unarchive callback subscription
|
||||
description: Sets callback archive state by `callbacks_ref` and required `archived` query parameter.
|
||||
operationId: callbacksArchive
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: ../parameters/org_ref.yaml#/components/parameters/OrgRef
|
||||
- $ref: ../parameters/callbacks_ref.yaml#/components/parameters/CallbacksRef
|
||||
- name: archived
|
||||
in: query
|
||||
required: true
|
||||
description: Target archive value to set on the callback.
|
||||
schema:
|
||||
type: boolean
|
||||
- $ref: ../parameters/cascade.yaml#/components/parameters/Cascade
|
||||
responses:
|
||||
'200':
|
||||
description: Archive state updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: ../response/response.yaml#/components/schemas/BaseResponse
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: ./response/callback.yaml#/components/schemas/CallbacksAuthData
|
||||
'400':
|
||||
$ref: ../response/operation.yaml#/components/responses/BadRequest
|
||||
'401':
|
||||
$ref: ../response/operation.yaml#/components/responses/Unauthorized
|
||||
'403':
|
||||
$ref: ../response/operation.yaml#/components/responses/Forbidden
|
||||
'500':
|
||||
$ref: ../response/operation.yaml#/components/responses/InternalServerError
|
||||
|
||||
9
interface/api/callbacks/bodies/callback.yaml
Normal file
9
interface/api/callbacks/bodies/callback.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
components:
|
||||
requestBodies:
|
||||
CallbackBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../request/callback.yaml#/components/schemas/CallbackRequest
|
||||
|
||||
34
interface/api/callbacks/create.yaml
Normal file
34
interface/api/callbacks/create.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
post:
|
||||
tags: [Callbacks]
|
||||
summary: Create callback subscription
|
||||
description: Creates callback subscription for the organization identified by `org_ref`.
|
||||
operationId: callbacksCreate
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: ../parameters/org_ref.yaml#/components/parameters/OrgRef
|
||||
requestBody:
|
||||
$ref: ./bodies/callback.yaml#/components/requestBodies/CallbackBody
|
||||
responses:
|
||||
'201':
|
||||
description: Callback created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: ../response/response.yaml#/components/schemas/BaseResponse
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: ./response/callback.yaml#/components/schemas/CallbacksAuthData
|
||||
'400':
|
||||
$ref: ../response/operation.yaml#/components/responses/BadRequest
|
||||
'401':
|
||||
$ref: ../response/operation.yaml#/components/responses/Unauthorized
|
||||
'403':
|
||||
$ref: ../response/operation.yaml#/components/responses/Forbidden
|
||||
'409':
|
||||
$ref: ../response/operation.yaml#/components/responses/Conflict
|
||||
'500':
|
||||
$ref: ../response/operation.yaml#/components/responses/InternalServerError
|
||||
|
||||
34
interface/api/callbacks/list.yaml
Normal file
34
interface/api/callbacks/list.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
get:
|
||||
tags: [Callbacks]
|
||||
summary: List callback subscriptions
|
||||
description: Lists callbacks for the given organization context.
|
||||
operationId: callbacksList
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: ../parameters/org_ref.yaml#/components/parameters/OrgRef
|
||||
- $ref: ../parameters/organizations_ref.yaml#/components/parameters/OrganizationsRef
|
||||
- $ref: ../parameters/limit.yaml#/components/parameters/Limit
|
||||
- $ref: ../parameters/offset.yaml#/components/parameters/Offset
|
||||
- $ref: ../parameters/archived.yaml#/components/parameters/Archived
|
||||
responses:
|
||||
'200':
|
||||
description: Callback list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: ../response/response.yaml#/components/schemas/BaseResponse
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: ./response/callback.yaml#/components/schemas/CallbacksAuthData
|
||||
'400':
|
||||
$ref: ../response/operation.yaml#/components/responses/BadRequest
|
||||
'401':
|
||||
$ref: ../response/operation.yaml#/components/responses/Unauthorized
|
||||
'403':
|
||||
$ref: ../response/operation.yaml#/components/responses/Forbidden
|
||||
'500':
|
||||
$ref: ../response/operation.yaml#/components/responses/InternalServerError
|
||||
|
||||
65
interface/api/callbacks/object.yaml
Normal file
65
interface/api/callbacks/object.yaml
Normal file
@@ -0,0 +1,65 @@
|
||||
get:
|
||||
tags: [Callbacks]
|
||||
summary: Get callback subscription
|
||||
description: Returns callback subscription by `callbacks_ref`.
|
||||
operationId: callbacksGet
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: ../parameters/callbacks_ref.yaml#/components/parameters/CallbacksRef
|
||||
responses:
|
||||
'200':
|
||||
description: Callback data
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: ../response/response.yaml#/components/schemas/BaseResponse
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: ./response/callback.yaml#/components/schemas/CallbacksAuthData
|
||||
'400':
|
||||
$ref: ../response/operation.yaml#/components/responses/BadRequest
|
||||
'401':
|
||||
$ref: ../response/operation.yaml#/components/responses/Unauthorized
|
||||
'403':
|
||||
$ref: ../response/operation.yaml#/components/responses/Forbidden
|
||||
'404':
|
||||
$ref: ../response/operation.yaml#/components/responses/NotFound
|
||||
'500':
|
||||
$ref: ../response/operation.yaml#/components/responses/InternalServerError
|
||||
|
||||
delete:
|
||||
tags: [Callbacks]
|
||||
summary: Delete callback subscription
|
||||
description: Deletes callback by reference.
|
||||
operationId: callbacksDelete
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: ../parameters/callbacks_ref.yaml#/components/parameters/CallbacksRef
|
||||
- $ref: ../parameters/cascade.yaml#/components/parameters/Cascade
|
||||
responses:
|
||||
'200':
|
||||
description: Callback deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: ../response/response.yaml#/components/schemas/BaseResponse
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: ./response/callback.yaml#/components/schemas/CallbacksAuthData
|
||||
'400':
|
||||
$ref: ../response/operation.yaml#/components/responses/BadRequest
|
||||
'401':
|
||||
$ref: ../response/operation.yaml#/components/responses/Unauthorized
|
||||
'403':
|
||||
$ref: ../response/operation.yaml#/components/responses/Forbidden
|
||||
'404':
|
||||
$ref: ../response/operation.yaml#/components/responses/NotFound
|
||||
'500':
|
||||
$ref: ../response/operation.yaml#/components/responses/InternalServerError
|
||||
|
||||
5
interface/api/callbacks/request/callback.yaml
Normal file
5
interface/api/callbacks/request/callback.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
components:
|
||||
schemas:
|
||||
CallbackRequest:
|
||||
$ref: ../../../models/callback/callback.yaml#/components/schemas/Callback
|
||||
|
||||
19
interface/api/callbacks/response/callback.yaml
Normal file
19
interface/api/callbacks/response/callback.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
components:
|
||||
schemas:
|
||||
CallbacksAuthData:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- accessToken
|
||||
- callbacks
|
||||
properties:
|
||||
accessToken:
|
||||
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
|
||||
callbacks:
|
||||
type: array
|
||||
items:
|
||||
$ref: ../../../models/callback/callback.yaml#/components/schemas/Callback
|
||||
generatedSigningSecret:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
32
interface/api/callbacks/rotate_secret.yaml
Normal file
32
interface/api/callbacks/rotate_secret.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
post:
|
||||
tags: [Callbacks]
|
||||
summary: Rotate callback signing secret
|
||||
description: Generates and stores a new HMAC secret for the callback in Vault and returns it once.
|
||||
operationId: callbacksRotateSecret
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: ../parameters/callbacks_ref.yaml#/components/parameters/CallbacksRef
|
||||
responses:
|
||||
'200':
|
||||
description: Callback secret rotated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: ../response/response.yaml#/components/schemas/BaseResponse
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: ./response/callback.yaml#/components/schemas/CallbacksAuthData
|
||||
'400':
|
||||
$ref: ../response/operation.yaml#/components/responses/BadRequest
|
||||
'401':
|
||||
$ref: ../response/operation.yaml#/components/responses/Unauthorized
|
||||
'403':
|
||||
$ref: ../response/operation.yaml#/components/responses/Forbidden
|
||||
'404':
|
||||
$ref: ../response/operation.yaml#/components/responses/NotFound
|
||||
'500':
|
||||
$ref: ../response/operation.yaml#/components/responses/InternalServerError
|
||||
|
||||
32
interface/api/callbacks/update.yaml
Normal file
32
interface/api/callbacks/update.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
put:
|
||||
tags: [Callbacks]
|
||||
summary: Update callback subscription
|
||||
description: Updates callback subscription fields.
|
||||
operationId: callbacksUpdate
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: ./bodies/callback.yaml#/components/requestBodies/CallbackBody
|
||||
responses:
|
||||
'200':
|
||||
description: Callback updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: ../response/response.yaml#/components/schemas/BaseResponse
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: ./response/callback.yaml#/components/schemas/CallbacksAuthData
|
||||
'400':
|
||||
$ref: ../response/operation.yaml#/components/responses/BadRequest
|
||||
'401':
|
||||
$ref: ../response/operation.yaml#/components/responses/Unauthorized
|
||||
'403':
|
||||
$ref: ../response/operation.yaml#/components/responses/Forbidden
|
||||
'404':
|
||||
$ref: ../response/operation.yaml#/components/responses/NotFound
|
||||
'500':
|
||||
$ref: ../response/operation.yaml#/components/responses/InternalServerError
|
||||
|
||||
10
interface/api/parameters/callbacks_ref.yaml
Normal file
10
interface/api/parameters/callbacks_ref.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
components:
|
||||
parameters:
|
||||
CallbacksRef:
|
||||
name: callbacks_ref
|
||||
in: path
|
||||
required: true
|
||||
description: Callback subscription reference (Mongo ObjectId, 24 hex chars).
|
||||
schema:
|
||||
$ref: ../../models/objectid.yaml#/components/schemas/ObjectId
|
||||
|
||||
67
interface/models/callback/callback.yaml
Normal file
67
interface/models/callback/callback.yaml
Normal file
@@ -0,0 +1,67 @@
|
||||
components:
|
||||
schemas:
|
||||
CallbackRetryPolicy:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- minDelayMs
|
||||
- maxDelayMs
|
||||
- signingMode
|
||||
- maxAttempts
|
||||
- requestTimeoutMs
|
||||
properties:
|
||||
minDelayMs:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maxDelayMs:
|
||||
type: integer
|
||||
minimum: 1
|
||||
signingMode:
|
||||
type: string
|
||||
enum:
|
||||
- none
|
||||
- hmac_sha256
|
||||
secretRef:
|
||||
type: string
|
||||
nullable: true
|
||||
headers:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
maxAttempts:
|
||||
type: integer
|
||||
minimum: 1
|
||||
requestTimeoutMs:
|
||||
type: integer
|
||||
minimum: 1
|
||||
|
||||
Callback:
|
||||
allOf:
|
||||
- $ref: ../permission_bound.yaml#/components/schemas/PermissionBound
|
||||
- $ref: ../common/describable.yaml#/components/schemas/Describable
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- clientId
|
||||
- status
|
||||
- url
|
||||
- eventTypes
|
||||
- retryPolicy
|
||||
properties:
|
||||
clientId:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- active
|
||||
- disabled
|
||||
url:
|
||||
type: string
|
||||
format: uri
|
||||
eventTypes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
retryPolicy:
|
||||
$ref: '#/components/schemas/CallbackRetryPolicy'
|
||||
|
||||
Reference in New Issue
Block a user