bff for callbacks
This commit is contained in:
@@ -4,6 +4,7 @@ matrix:
|
|||||||
BFF_DOCKERFILE: ci/prod/compose/bff.dockerfile
|
BFF_DOCKERFILE: ci/prod/compose/bff.dockerfile
|
||||||
BFF_MONGO_SECRET_PATH: sendico/db
|
BFF_MONGO_SECRET_PATH: sendico/db
|
||||||
BFF_API_SECRET_PATH: sendico/api/endpoint
|
BFF_API_SECRET_PATH: sendico/api/endpoint
|
||||||
|
BFF_VAULT_SECRET_PATH: sendico/edge/bff/vault
|
||||||
BFF_ENV: prod
|
BFF_ENV: prod
|
||||||
|
|
||||||
when:
|
when:
|
||||||
|
|||||||
@@ -109,6 +109,19 @@ api:
|
|||||||
dial_timeout_seconds: 5
|
dial_timeout_seconds: 5
|
||||||
call_timeout_seconds: 5
|
call_timeout_seconds: 5
|
||||||
insecure: true
|
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:
|
app:
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,19 @@ api:
|
|||||||
dial_timeout_seconds: 5
|
dial_timeout_seconds: 5
|
||||||
call_timeout_seconds: 5
|
call_timeout_seconds: 5
|
||||||
insecure: true
|
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:
|
app:
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ require (
|
|||||||
github.com/go-chi/metrics v0.1.1
|
github.com/go-chi/metrics v0.1.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/mitchellh/mapstructure v1.5.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/shopspring/decimal v1.4.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tech/sendico/gateway/tron v0.0.0-00010101000000-000000000000
|
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/docker/go-units v0.5.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-chi/chi v1.5.5 // 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/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // 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/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||||
github.com/lestrrat-go/dsig v1.0.0 // 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/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // 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/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||||
github.com/moby/sys/sequential 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/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // 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/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.5 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.20.1 // 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/segmentio/asm v1.2.1 // indirect
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // 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/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/text v0.34.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
|
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-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 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
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/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 h1:CXhbnkAVVjb0k73EBRQ6Z2YdWFnbXZgNtg1Mboguibk=
|
||||||
github.com/go-chi/metrics v0.1.1/go.mod h1:mcGTM1pPalP7WCtb+akNYFO/lwNwBBLCuedepqjoPn4=
|
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.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 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
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.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 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
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 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
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/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 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
|
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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
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=
|
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-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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
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=
|
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/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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
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 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
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=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/tech/sendico/pkg/vault/kv"
|
||||||
mwa "github.com/tech/sendico/server/interface/middleware"
|
mwa "github.com/tech/sendico/server/interface/middleware"
|
||||||
fsc "github.com/tech/sendico/server/interface/services/fileservice/config"
|
fsc "github.com/tech/sendico/server/interface/services/fileservice/config"
|
||||||
)
|
)
|
||||||
@@ -13,6 +14,7 @@ type Config struct {
|
|||||||
PaymentOrchestrator *PaymentOrchestratorConfig `yaml:"payment_orchestrator"`
|
PaymentOrchestrator *PaymentOrchestratorConfig `yaml:"payment_orchestrator"`
|
||||||
PaymentQuotation *PaymentOrchestratorConfig `yaml:"payment_quotation"`
|
PaymentQuotation *PaymentOrchestratorConfig `yaml:"payment_quotation"`
|
||||||
PaymentMethods *PaymentOrchestratorConfig `yaml:"payment_methods"`
|
PaymentMethods *PaymentOrchestratorConfig `yaml:"payment_methods"`
|
||||||
|
Callbacks *CallbacksConfig `yaml:"callbacks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChainGatewayConfig struct {
|
type ChainGatewayConfig struct {
|
||||||
@@ -45,3 +47,12 @@ type PaymentOrchestratorConfig struct {
|
|||||||
CallTimeoutSeconds int `yaml:"call_timeout_seconds"`
|
CallTimeoutSeconds int `yaml:"call_timeout_seconds"`
|
||||||
Insecure bool `yaml:"insecure"`
|
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/pkg/mservice"
|
||||||
"github.com/tech/sendico/server/interface/api"
|
"github.com/tech/sendico/server/interface/api"
|
||||||
"github.com/tech/sendico/server/interface/services/account"
|
"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/invitation"
|
||||||
"github.com/tech/sendico/server/interface/services/ledger"
|
"github.com/tech/sendico/server/interface/services/ledger"
|
||||||
"github.com/tech/sendico/server/interface/services/logo"
|
"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, wallet.Create)
|
||||||
srvf = append(srvf, ledger.Create)
|
srvf = append(srvf, ledger.Create)
|
||||||
srvf = append(srvf, recipient.Create)
|
srvf = append(srvf, recipient.Create)
|
||||||
|
srvf = append(srvf, callbacks.Create)
|
||||||
srvf = append(srvf, paymethod.Create)
|
srvf = append(srvf, paymethod.Create)
|
||||||
srvf = append(srvf, payment.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/db/storable"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
mutil "github.com/tech/sendico/pkg/mutil/db"
|
mutil "github.com/tech/sendico/pkg/mutil/db"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
@@ -22,7 +23,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
inboxCollection string = "inbox"
|
inboxCollection string = "inbox"
|
||||||
tasksCollection string = "tasks"
|
tasksCollection string = "tasks"
|
||||||
endpointsCollection string = "endpoints"
|
endpointsCollection string = mservice.Callbacks
|
||||||
)
|
)
|
||||||
|
|
||||||
type mongoRepository struct {
|
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 (
|
import (
|
||||||
"github.com/tech/sendico/pkg/auth"
|
"github.com/tech/sendico/pkg/auth"
|
||||||
"github.com/tech/sendico/pkg/db/account"
|
"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/chainassets"
|
||||||
"github.com/tech/sendico/pkg/db/chainwalletroutes"
|
"github.com/tech/sendico/pkg/db/chainwalletroutes"
|
||||||
mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo"
|
mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo"
|
||||||
@@ -29,6 +30,7 @@ type Factory interface {
|
|||||||
NewOrganizationDB() (organization.DB, error)
|
NewOrganizationDB() (organization.DB, error)
|
||||||
NewInvitationsDB() (invitation.DB, error)
|
NewInvitationsDB() (invitation.DB, error)
|
||||||
NewRecipientsDB() (recipient.DB, error)
|
NewRecipientsDB() (recipient.DB, error)
|
||||||
|
NewCallbacksDB() (callbacks.DB, error)
|
||||||
NewVerificationsDB() (verification.DB, error)
|
NewVerificationsDB() (verification.DB, error)
|
||||||
|
|
||||||
NewRolesDB() (role.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/mitchellh/mapstructure"
|
||||||
"github.com/tech/sendico/pkg/auth"
|
"github.com/tech/sendico/pkg/auth"
|
||||||
"github.com/tech/sendico/pkg/db/account"
|
"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/chainassets"
|
||||||
"github.com/tech/sendico/pkg/db/chainwalletroutes"
|
"github.com/tech/sendico/pkg/db/chainwalletroutes"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/accountdb"
|
"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/chainassetsdb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/chainwalletroutesdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/chainwalletroutesdb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/invitationdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/invitationdb"
|
||||||
@@ -218,6 +220,10 @@ func (db *DB) NewRecipientsDB() (recipient.DB, error) {
|
|||||||
return newProtectedDB(db, create)
|
return newProtectedDB(db, create)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) NewCallbacksDB() (callbacks.DB, error) {
|
||||||
|
return newProtectedDB(db, callbacksdb.Create)
|
||||||
|
}
|
||||||
|
|
||||||
func (db *DB) NewRefreshTokensDB() (refreshtokens.DB, error) {
|
func (db *DB) NewRefreshTokensDB() (refreshtokens.DB, error) {
|
||||||
return refreshtokensdb.Create(db.logger, db.db())
|
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
|
ChainWalletBalances Type = "chain_wallet_balances" // Represents managed chain wallet balances
|
||||||
ChainTransfers Type = "chain_transfers" // Represents chain transfers
|
ChainTransfers Type = "chain_transfers" // Represents chain transfers
|
||||||
ChainDeposits Type = "chain_deposits" // Represents chain deposits
|
ChainDeposits Type = "chain_deposits" // Represents chain deposits
|
||||||
|
Callbacks Type = "callbacks" // Represents webhook callback subscriptions
|
||||||
Notifications Type = "notifications" // Represents notifications sent to users
|
Notifications Type = "notifications" // Represents notifications sent to users
|
||||||
Organizations Type = "organizations" // Represents organizations in the system
|
Organizations Type = "organizations" // Represents organizations in the system
|
||||||
Payments Type = "payments" // Represents payments service
|
Payments Type = "payments" // Represents payments service
|
||||||
@@ -58,7 +59,7 @@ const (
|
|||||||
func StringToSType(s string) (Type, error) {
|
func StringToSType(s string) (Type, error) {
|
||||||
switch Type(s) {
|
switch Type(s) {
|
||||||
case Accounts, Verification, Amplitude, Site, Changes, Clients, ChainGateway, ChainWallets, WalletRoutes, ChainWalletBalances,
|
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,
|
LedgerAccounts, LedgerBalances, LedgerEntries, LedgerOutbox, LedgerParties, LedgerPlines, Notifications,
|
||||||
Organizations, Payments, PaymentRoutes, PaymentOrchestrator, PaymentMethods, Permissions, Policies, PolicyAssignements,
|
Organizations, Payments, PaymentRoutes, PaymentOrchestrator, PaymentMethods, Permissions, Policies, PolicyAssignements,
|
||||||
Recipients, RefreshTokens, Roles, Storage, Tenants, Workflows, Discovery:
|
Recipients, RefreshTokens, Roles, Storage, Tenants, Workflows, Discovery:
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Address string `mapstructure:"address" yaml:"address"`
|
Address string `mapstructure:"address" yaml:"address"`
|
||||||
TokenEnv string `mapstructure:"token_env" yaml:"token_env"`
|
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"`
|
Namespace string `mapstructure:"namespace" yaml:"namespace"`
|
||||||
MountPath string `mapstructure:"mount_path" yaml:"mount_path"`
|
MountPath string `mapstructure:"mount_path" yaml:"mount_path"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,16 +36,14 @@ func newService(opts Options) (Client, error) {
|
|||||||
return nil, merrors.InvalidArgument(component + ": address is required")
|
return nil, merrors.InvalidArgument(component + ": address is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenEnv := strings.TrimSpace(opts.Config.TokenEnv)
|
token, tokenSource, err := resolveToken(opts.Config)
|
||||||
if tokenEnv == "" {
|
if err != nil {
|
||||||
logger.Error("Vault token env missing")
|
logger.Error("Vault token configuration is invalid", zap.Error(err))
|
||||||
return nil, merrors.InvalidArgument(component + ": token_env is required")
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
token := strings.TrimSpace(os.Getenv(tokenEnv))
|
|
||||||
if token == "" {
|
if token == "" {
|
||||||
logger.Error("Vault token missing; expected Vault Agent to export token", zap.String("env", tokenEnv))
|
logger.Error("Vault token missing", zap.String("source", tokenSource))
|
||||||
return nil, merrors.InvalidArgument(component + ": token env " + tokenEnv + " is not set (expected Vault Agent sink to populate it)")
|
return nil, merrors.InvalidArgument(component + ": vault token is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
mountPath := strings.Trim(strings.TrimSpace(opts.Config.MountPath), "/")
|
mountPath := strings.Trim(strings.TrimSpace(opts.Config.MountPath), "/")
|
||||||
@@ -148,4 +146,36 @@ func normalizePath(secretPath string) (string, error) {
|
|||||||
return normalizedPath, nil
|
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)
|
var _ Client = (*service)(nil)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Address string `mapstructure:"address" yaml:"address"`
|
Address string `mapstructure:"address" yaml:"address"`
|
||||||
TokenEnv string `mapstructure:"token_env" yaml:"token_env"`
|
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"`
|
Namespace string `mapstructure:"namespace" yaml:"namespace"`
|
||||||
MountPath string `mapstructure:"mount_path" yaml:"mount_path"`
|
MountPath string `mapstructure:"mount_path" yaml:"mount_path"`
|
||||||
KeyPrefix string `mapstructure:"key_prefix" yaml:"key_prefix"`
|
KeyPrefix string `mapstructure:"key_prefix" yaml:"key_prefix"`
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ func newService(opts Options) (Service, error) {
|
|||||||
Config: kv.Config{
|
Config: kv.Config{
|
||||||
Address: opts.Config.Address,
|
Address: opts.Config.Address,
|
||||||
TokenEnv: opts.Config.TokenEnv,
|
TokenEnv: opts.Config.TokenEnv,
|
||||||
|
TokenFileEnv: opts.Config.TokenFileEnv,
|
||||||
|
TokenFile: opts.Config.TokenFile,
|
||||||
Namespace: opts.Config.Namespace,
|
Namespace: opts.Config.Namespace,
|
||||||
MountPath: opts.Config.MountPath,
|
MountPath: opts.Config.MountPath,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ Examples:
|
|||||||
|
|
||||||
Infrastructure (MongoDB, NATS) uses plain `.env.dev` credentials.
|
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`.
|
Set the corresponding `*_VAULT_ROLE_ID` and `*_VAULT_SECRET_ID` values in `.env.dev`.
|
||||||
|
|
||||||
## Network
|
## 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_COMPOSE_PROJECT=sendico-bff
|
||||||
BFF_SERVICE_NAME=sendico_bff
|
BFF_SERVICE_NAME=sendico_bff
|
||||||
BFF_HTTP_PORT=8080
|
BFF_HTTP_PORT=8080
|
||||||
|
BFF_VAULT_SECRET_PATH=sendico/edge/bff/vault
|
||||||
|
|
||||||
# Callbacks service
|
# Callbacks service
|
||||||
CALLBACKS_DIR=callbacks
|
CALLBACKS_DIR=callbacks
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ x-common-env: &common-env
|
|||||||
- ../env/.env.runtime
|
- ../env/.env.runtime
|
||||||
- ../env/.env.version
|
- ../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:
|
networks:
|
||||||
sendico-net:
|
sendico-net:
|
||||||
external: true
|
external: true
|
||||||
@@ -51,8 +59,15 @@ services:
|
|||||||
PERMISSION_COLLECTION: ${PERMISSION_COLLECTION}
|
PERMISSION_COLLECTION: ${PERMISSION_COLLECTION}
|
||||||
PERMISSION_TIMEOUT: ${PERMISSION_TIMEOUT}
|
PERMISSION_TIMEOUT: ${PERMISSION_TIMEOUT}
|
||||||
PERMISSION_IS_FILTERED: ${PERMISSION_IS_FILTERED}
|
PERMISSION_IS_FILTERED: ${PERMISSION_IS_FILTERED}
|
||||||
|
VAULT_ADDR: ${VAULT_ADDR}
|
||||||
|
VAULT_TOKEN_FILE: /run/vault/token
|
||||||
ports:
|
ports:
|
||||||
- "0.0.0.0:${BFF_HTTP_PORT}:8081"
|
- "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:
|
healthcheck:
|
||||||
test: ["CMD-SHELL","wget -qO- http://localhost:8081/api/v1/health | grep -q '\"status\":\"ok\"'"]
|
test: ["CMD-SHELL","wget -qO- http://localhost:8081/api/v1/health | grep -q '\"status\":\"ok\"'"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -61,3 +76,32 @@ services:
|
|||||||
start_period: 60s
|
start_period: 60s
|
||||||
networks:
|
networks:
|
||||||
- sendico-net
|
- 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_USER
|
||||||
MONGO_PASSWORD
|
MONGO_PASSWORD
|
||||||
API_ENDPOINT_SECRET
|
API_ENDPOINT_SECRET
|
||||||
|
BFF_VAULT_ROLE_ID
|
||||||
|
BFF_VAULT_SECRET_ID
|
||||||
NATS_USER
|
NATS_USER
|
||||||
NATS_PASSWORD
|
NATS_PASSWORD
|
||||||
NATS_URL
|
NATS_URL
|
||||||
@@ -43,6 +45,8 @@ b64enc() {
|
|||||||
MONGO_USER_B64="$(b64enc "${MONGO_USER}")"
|
MONGO_USER_B64="$(b64enc "${MONGO_USER}")"
|
||||||
MONGO_PASSWORD_B64="$(b64enc "${MONGO_PASSWORD}")"
|
MONGO_PASSWORD_B64="$(b64enc "${MONGO_PASSWORD}")"
|
||||||
API_ENDPOINT_SECRET_B64="$(b64enc "${API_ENDPOINT_SECRET}")"
|
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_USER_B64="$(b64enc "${NATS_USER}")"
|
||||||
NATS_PASSWORD_B64="$(b64enc "${NATS_PASSWORD}")"
|
NATS_PASSWORD_B64="$(b64enc "${NATS_PASSWORD}")"
|
||||||
NATS_URL_B64="$(b64enc "${NATS_URL}")"
|
NATS_URL_B64="$(b64enc "${NATS_URL}")"
|
||||||
@@ -77,6 +81,8 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" \
|
|||||||
MONGO_USER_B64="$MONGO_USER_B64" \
|
MONGO_USER_B64="$MONGO_USER_B64" \
|
||||||
MONGO_PASSWORD_B64="$MONGO_PASSWORD_B64" \
|
MONGO_PASSWORD_B64="$MONGO_PASSWORD_B64" \
|
||||||
API_ENDPOINT_SECRET_B64="$API_ENDPOINT_SECRET_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_USER_B64="$NATS_USER_B64" \
|
||||||
NATS_PASSWORD_B64="$NATS_PASSWORD_B64" \
|
NATS_PASSWORD_B64="$NATS_PASSWORD_B64" \
|
||||||
NATS_URL_B64="$NATS_URL_B64" \
|
NATS_URL_B64="$NATS_URL_B64" \
|
||||||
@@ -124,11 +130,14 @@ decode_b64() {
|
|||||||
MONGO_USER="$(decode_b64 "$MONGO_USER_B64")"
|
MONGO_USER="$(decode_b64 "$MONGO_USER_B64")"
|
||||||
MONGO_PASSWORD="$(decode_b64 "$MONGO_PASSWORD_B64")"
|
MONGO_PASSWORD="$(decode_b64 "$MONGO_PASSWORD_B64")"
|
||||||
API_ENDPOINT_SECRET="$(decode_b64 "$API_ENDPOINT_SECRET_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_USER="$(decode_b64 "$NATS_USER_B64")"
|
||||||
NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")"
|
NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")"
|
||||||
NATS_URL="$(decode_b64 "$NATS_URL_B64")"
|
NATS_URL="$(decode_b64 "$NATS_URL_B64")"
|
||||||
|
|
||||||
export MONGO_USER MONGO_PASSWORD API_ENDPOINT_SECRET
|
export MONGO_USER MONGO_PASSWORD API_ENDPOINT_SECRET
|
||||||
|
export BFF_VAULT_ROLE_ID BFF_VAULT_SECRET_ID
|
||||||
export NATS_USER NATS_PASSWORD NATS_URL
|
export NATS_USER NATS_PASSWORD NATS_URL
|
||||||
COMPOSE_PROJECT_NAME="$COMPOSE_PROJECT"
|
COMPOSE_PROJECT_NAME="$COMPOSE_PROJECT"
|
||||||
export COMPOSE_PROJECT_NAME
|
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_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_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_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 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 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
|
load_nats_env
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ volumes:
|
|||||||
type: tmpfs
|
type: tmpfs
|
||||||
device: tmpfs
|
device: tmpfs
|
||||||
o: size=8m,uid=0,gid=0,mode=0700
|
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
|
# INFRASTRUCTURE SERVICES
|
||||||
@@ -923,6 +929,39 @@ services:
|
|||||||
VAULT_ADDR: ${VAULT_ADDR}
|
VAULT_ADDR: ${VAULT_ADDR}
|
||||||
VAULT_TOKEN_FILE: /run/vault/token
|
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
|
# BFF (Backend for Frontend / Server) Service
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -942,9 +981,11 @@ services:
|
|||||||
dev-payments-quotation: { condition: service_started }
|
dev-payments-quotation: { condition: service_started }
|
||||||
dev-payments-methods: { condition: service_started }
|
dev-payments-methods: { condition: service_started }
|
||||||
dev-chain-gateway: { condition: service_started }
|
dev-chain-gateway: { condition: service_started }
|
||||||
|
dev-bff-vault-agent: { condition: service_healthy }
|
||||||
volumes:
|
volumes:
|
||||||
- ./api/edge/bff:/src/api/edge/bff
|
- ./api/edge/bff:/src/api/edge/bff
|
||||||
- ./api/edge/bff/config.dev.yml:/app/config.yml:ro
|
- ./api/edge/bff/config.dev.yml:/app/config.yml:ro
|
||||||
|
- dev-bff-vault-run:/run/vault:ro
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
networks:
|
networks:
|
||||||
@@ -977,6 +1018,8 @@ services:
|
|||||||
API_PROTOCOL: http
|
API_PROTOCOL: http
|
||||||
SERVICE_HOST: localhost
|
SERVICE_HOST: localhost
|
||||||
API_ENDPOINT: /api/v1
|
API_ENDPOINT: /api/v1
|
||||||
|
VAULT_ADDR: ${VAULT_ADDR}
|
||||||
|
VAULT_TOKEN_FILE: /run/vault/token
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# Frontend (Flutter Web)
|
# Frontend (Flutter Web)
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ tags:
|
|||||||
description: Recipient CRUD and archive flows
|
description: Recipient CRUD and archive flows
|
||||||
- name: Payment Methods
|
- name: Payment Methods
|
||||||
description: Payment method CRUD and archive flows
|
description: Payment method CRUD and archive flows
|
||||||
|
- name: Callbacks
|
||||||
|
description: Webhook callback subscription CRUD and signing secret rotation
|
||||||
- name: Payments
|
- name: Payments
|
||||||
description: Quotation and payment orchestration
|
description: Quotation and payment orchestration
|
||||||
|
|
||||||
@@ -70,6 +72,19 @@ paths:
|
|||||||
/payment_methods/archive/{organizations_ref}/{payment_methods_ref}:
|
/payment_methods/archive/{organizations_ref}/{payment_methods_ref}:
|
||||||
$ref: ./api/payment_methods/archive.yaml
|
$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}:
|
/payments/quote/{organizations_ref}:
|
||||||
$ref: ./api/payments/quote.yaml
|
$ref: ./api/payments/quote.yaml
|
||||||
/payments/multiquote/{organizations_ref}:
|
/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