move api/server to api/edge/bff
This commit is contained in:
46
api/edge/bff/.air.toml
Normal file
46
api/edge/bff/.air.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
entrypoint = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ."
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go", "_templ.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
5
api/edge/bff/.gitignore
vendored
Normal file
5
api/edge/bff/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/app
|
||||
/server
|
||||
/storage
|
||||
.gocache
|
||||
tmp
|
||||
14
api/edge/bff/ampli.json
Normal file
14
api/edge/bff/ampli.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Zone": "eu",
|
||||
"OrgId": "100001828",
|
||||
"WorkspaceId": "c75043a3-1fad-45ec-bd71-c807a99c650d",
|
||||
"SourceId": "81b24ac7-e285-4519-9e82-bb575601120c",
|
||||
"Branch": "main",
|
||||
"Version": "2.0.0",
|
||||
"VersionId": "4fa6851a-4ff0-42f1-b440-8b39f07870e4",
|
||||
"Runtime": "go:go-ampli",
|
||||
"Platform": "Go",
|
||||
"Language": "Go",
|
||||
"SDK": "analytics-go",
|
||||
"Path": "./internal/ampli"
|
||||
}
|
||||
6
api/edge/bff/assets/assets.go
Normal file
6
api/edge/bff/assets/assets.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package assets
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed resources/logo.png
|
||||
var MailLogo []byte
|
||||
BIN
api/edge/bff/assets/resources/logo.png
Normal file
BIN
api/edge/bff/assets/resources/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
133
api/edge/bff/config.dev.yml
Executable file
133
api/edge/bff/config.dev.yml
Executable file
@@ -0,0 +1,133 @@
|
||||
http_server:
|
||||
listen_address: :8080
|
||||
read_header_timeout: 60
|
||||
shutdown_timeout: 5
|
||||
|
||||
api:
|
||||
amplitude:
|
||||
ampli_environment_env: AMPLI_ENVIRONMENT
|
||||
middleware:
|
||||
api_protocol_env: API_PROTOCOL
|
||||
domain_env: SERVICE_HOST
|
||||
api_endpoint_env: API_ENDPOINT
|
||||
signature:
|
||||
secret_key_env: API_ENDPOINT_SECRET
|
||||
algorithm: HS256
|
||||
CORS:
|
||||
max_age: 300
|
||||
allowed_origins:
|
||||
- "*"
|
||||
allowed_methods:
|
||||
- "GET"
|
||||
- "POST"
|
||||
- "PUT"
|
||||
- "PATCH"
|
||||
- "DELETE"
|
||||
- "OPTIONS"
|
||||
allowed_headers:
|
||||
- "Accept"
|
||||
- "Authorization"
|
||||
- "Content-Type"
|
||||
- "X-Requested-With"
|
||||
exposed_headers:
|
||||
allow_credentials: false
|
||||
websocket:
|
||||
endpoint_env: WS_ENDPOINT
|
||||
timeout: 60
|
||||
message_broker:
|
||||
driver: NATS
|
||||
settings:
|
||||
url_env: NATS_URL
|
||||
host_env: NATS_HOST
|
||||
port_env: NATS_PORT
|
||||
username_env: NATS_USER
|
||||
password_env: NATS_PASSWORD
|
||||
broker_name: Sendico Backend server
|
||||
max_reconnects: 10
|
||||
reconnect_wait: 5
|
||||
buffer_size: 1024
|
||||
# type: in-process
|
||||
# settings:
|
||||
# buffer_size: 10
|
||||
token:
|
||||
expiration_hours:
|
||||
account: 24
|
||||
refresh: 720
|
||||
length: 32
|
||||
password:
|
||||
token_length: 32
|
||||
check:
|
||||
min_length: 8
|
||||
digit: true
|
||||
upper: true
|
||||
lower: true
|
||||
special: true
|
||||
|
||||
|
||||
storage:
|
||||
# driver: aws_s3
|
||||
# settings:
|
||||
# access_key_id_env: S3_ACCESS_KEY_ID
|
||||
# secret_access_key_env: S3_ACCESS_KEY_SECRET
|
||||
# region_env: S3_REGION
|
||||
# bucket_name_env: S3_BUCKET_NAME
|
||||
driver: local_fs
|
||||
settings:
|
||||
root_path: ./storage
|
||||
|
||||
chain_gateway:
|
||||
address: dev-tron-gateway:50071
|
||||
address_env: TRON_GATEWAY_ADDRESS
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
default_asset:
|
||||
chain: TRON_NILE
|
||||
token_symbol: USDT
|
||||
contract_address: ""
|
||||
ledger:
|
||||
address: dev-ledger:50052
|
||||
address_env: LEDGER_ADDRESS
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
payment_orchestrator:
|
||||
address: dev-payments-orchestrator:50062
|
||||
address_env: PAYMENTS_ADDRESS
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
payment_quotation:
|
||||
address: dev-payments-quotation:50064
|
||||
address_env: PAYMENTS_QUOTE_ADDRESS
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
payment_methods:
|
||||
address: dev-payments-methods:50066
|
||||
address_env: PAYMENTS_METHODS_ADDRESS
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
|
||||
app:
|
||||
|
||||
database:
|
||||
driver: mongodb
|
||||
settings:
|
||||
host_env: MONGO_HOST
|
||||
port_env: MONGO_PORT
|
||||
database_env: MONGO_DATABASE
|
||||
user_env: MONGO_USER
|
||||
password_env: MONGO_PASSWORD
|
||||
auth_source_env: MONGO_AUTH_SOURCE
|
||||
replica_set_env: MONGO_REPLICA_SET
|
||||
enforcer:
|
||||
driver: native
|
||||
settings:
|
||||
model_path_env: PERMISSION_MODEL
|
||||
adapter:
|
||||
collection_name_env: PERMISSION_COLLECTION
|
||||
database_name_env: MONGO_DATABASE
|
||||
timeout_seconds_env: PERMISSION_TIMEOUT
|
||||
is_filtered_env: PERMISSION_IS_FILTERED
|
||||
135
api/edge/bff/config.yml
Executable file
135
api/edge/bff/config.yml
Executable file
@@ -0,0 +1,135 @@
|
||||
http_server:
|
||||
listen_address: :8081
|
||||
read_header_timeout: 60
|
||||
shutdown_timeout: 5
|
||||
|
||||
api:
|
||||
amplitude:
|
||||
ampli_environment_env: AMPLI_ENVIRONMENT
|
||||
middleware:
|
||||
api_protocol_env: API_PROTOCOL
|
||||
domain_env: SERVICE_HOST
|
||||
api_endpoint_env: API_ENDPOINT
|
||||
signature:
|
||||
secret_key_env: API_ENDPOINT_SECRET
|
||||
algorithm: HS256
|
||||
CORS:
|
||||
max_age: 300
|
||||
allowed_origins:
|
||||
- "https://sendico.io"
|
||||
- "https://app.sendico.io"
|
||||
- "https://www.sendico.io"
|
||||
allowed_methods:
|
||||
- "GET"
|
||||
- "POST"
|
||||
- "PUT"
|
||||
- "PATCH"
|
||||
- "DELETE"
|
||||
- "OPTIONS"
|
||||
allowed_headers:
|
||||
- "Accept"
|
||||
- "Authorization"
|
||||
- "Content-Type"
|
||||
- "X-Requested-With"
|
||||
exposed_headers:
|
||||
allow_credentials: false
|
||||
websocket:
|
||||
endpoint_env: WS_ENDPOINT
|
||||
timeout: 60
|
||||
message_broker:
|
||||
driver: NATS
|
||||
settings:
|
||||
url_env: NATS_URL
|
||||
host_env: NATS_HOST
|
||||
port_env: NATS_PORT
|
||||
username_env: NATS_USER
|
||||
password_env: NATS_PASSWORD
|
||||
broker_name: Sendico Backend server
|
||||
max_reconnects: 10
|
||||
reconnect_wait: 5
|
||||
buffer_size: 1024
|
||||
# type: in-process
|
||||
# settings:
|
||||
# buffer_size: 10
|
||||
token:
|
||||
expiration_hours:
|
||||
account: 24
|
||||
refresh: 720
|
||||
length: 32
|
||||
password:
|
||||
token_length: 32
|
||||
check:
|
||||
min_length: 8
|
||||
digit: true
|
||||
upper: true
|
||||
lower: true
|
||||
special: true
|
||||
|
||||
|
||||
storage:
|
||||
# driver: aws_s3
|
||||
# settings:
|
||||
# access_key_id_env: S3_ACCESS_KEY_ID
|
||||
# secret_access_key_env: S3_ACCESS_KEY_SECRET
|
||||
# region_env: S3_REGION
|
||||
# bucket_name_env: S3_BUCKET_NAME
|
||||
driver: local_fs
|
||||
settings:
|
||||
root_path: ./storage
|
||||
|
||||
chain_gateway:
|
||||
address: sendico_tron_gateway:50071
|
||||
address_env: TRON_GATEWAY_ADDRESS
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
default_asset:
|
||||
chain: TRON_MAINNET
|
||||
token_symbol: USDT
|
||||
contract_address: ""
|
||||
ledger:
|
||||
address: sendico_ledger:50052
|
||||
address_env: LEDGER_ADDRESS
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
payment_orchestrator:
|
||||
address: sendico_payments_orchestrator:50062
|
||||
address_env: PAYMENTS_ADDRESS
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
payment_quotation:
|
||||
address: sendico_payments_quotation:50064
|
||||
address_env: PAYMENTS_QUOTE_ADDRESS
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
payment_methods:
|
||||
address: sendico_payments_methods:50066
|
||||
address_env: PAYMENTS_METHODS_ADDRESS
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
|
||||
app:
|
||||
|
||||
database:
|
||||
driver: mongodb
|
||||
settings:
|
||||
host_env: MONGO_HOST
|
||||
port_env: MONGO_PORT
|
||||
database_env: MONGO_DATABASE
|
||||
user_env: MONGO_USER
|
||||
password_env: MONGO_PASSWORD
|
||||
auth_source_env: MONGO_AUTH_SOURCE
|
||||
replica_set_env: MONGO_REPLICA_SET
|
||||
enforcer:
|
||||
driver: native
|
||||
settings:
|
||||
model_path_env: PERMISSION_MODEL
|
||||
adapter:
|
||||
collection_name_env: PERMISSION_COLLECTION
|
||||
database_name_env: MONGO_DATABASE
|
||||
timeout_seconds_env: PERMISSION_TIMEOUT
|
||||
is_filtered_env: PERMISSION_IS_FILTERED
|
||||
1
api/edge/bff/env/.gitignore
vendored
Normal file
1
api/edge/bff/env/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.env.api
|
||||
150
api/edge/bff/go.mod
Normal file
150
api/edge/bff/go.mod
Normal file
@@ -0,0 +1,150 @@
|
||||
module github.com/tech/sendico/server
|
||||
|
||||
go 1.25.7
|
||||
|
||||
replace github.com/tech/sendico/pkg => ../../pkg
|
||||
|
||||
replace github.com/tech/sendico/ledger => ../../ledger
|
||||
|
||||
replace github.com/tech/sendico/payments/orchestrator => ../../payments/orchestrator
|
||||
|
||||
replace github.com/tech/sendico/payments/methods => ../../payments/methods
|
||||
|
||||
replace github.com/tech/sendico/payments/storage => ../../payments/storage
|
||||
|
||||
replace github.com/tech/sendico/gateway/tron => ../../gateway/tron
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/go-chi/jwtauth/v5 v5.4.0
|
||||
github.com/go-chi/metrics v0.1.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tech/sendico/gateway/tron v0.0.0-00010101000000-000000000000
|
||||
github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000
|
||||
github.com/tech/sendico/payments/methods v0.0.0-00010101000000-000000000000
|
||||
github.com/tech/sendico/payments/orchestrator v0.0.0-00010101000000-000000000000
|
||||
github.com/tech/sendico/pkg v0.1.0
|
||||
github.com/testcontainers/testcontainers-go v0.33.0
|
||||
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/net v0.51.0
|
||||
google.golang.org/grpc v1.79.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
moul.io/chizap v1.0.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||
github.com/casbin/casbin/v2 v2.135.0 // indirect
|
||||
github.com/casbin/govaluate v1.10.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
|
||||
github.com/aws/smithy-go v1.24.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker v27.3.1+incompatible // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-chi/chi v1.5.5 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.18.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-secp256k1 v1.0.0 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.4 // indirect
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.13 // indirect
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nats-io/nats.go v1.49.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.15 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.0 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.10 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.2.0 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
)
|
||||
388
api/edge/bff/go.sum
Normal file
388
api/edge/bff/go.sum
Normal file
@@ -0,0 +1,388 @@
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
|
||||
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
|
||||
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
|
||||
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
|
||||
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
|
||||
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||
github.com/casbin/mongodb-adapter/v4 v4.3.0 h1:yYXky9v1by6vj/0QK7OyHyd/xpz4vzh0lCi7JKrS4qQ=
|
||||
github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
||||
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-chi/jwtauth/v5 v5.4.0 h1:Ieh0xMJsFvqylqJ02/mQHKzbbKO9DYNBh4DPKCwTwYI=
|
||||
github.com/go-chi/jwtauth/v5 v5.4.0/go.mod h1:w6yjqUUXz1b8+oiJel64Sz1KJwduQM6qUA5QNzO5+bQ=
|
||||
github.com/go-chi/metrics v0.1.1 h1:CXhbnkAVVjb0k73EBRQ6Z2YdWFnbXZgNtg1Mboguibk=
|
||||
github.com/go-chi/metrics v0.1.1/go.mod h1:mcGTM1pPalP7WCtb+akNYFO/lwNwBBLCuedepqjoPn4=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
|
||||
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
|
||||
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
|
||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
|
||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.4 h1:pXyH2ppK8GYYggygxJ3TvxpCZnbEUWc9qSwRTTApaLA=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.4/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
|
||||
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
|
||||
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
|
||||
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
|
||||
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
|
||||
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
|
||||
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=
|
||||
github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8=
|
||||
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY=
|
||||
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
||||
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.mongodb.org/mongo-driver v1.17.8 h1:BDP3+U3Y8K0vTrpqDJIRaXNhb/bKyoVeg6tIJsW5EhM=
|
||||
go.mongodb.org/mongo-driver v1.17.8/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
moul.io/chizap v1.0.3 h1:mliXvvuS5HVo3QP8qPXczWtRM5dQ9UmK3bBVIkZo6ek=
|
||||
moul.io/chizap v1.0.3/go.mod h1:pq4R9kGLwz4XjBc4hodQYuoE7Yc9RUabLBFyyi2uErk=
|
||||
422
api/edge/bff/interface/accountservice/internal/service.go
Normal file
422
api/edge/bff/interface/accountservice/internal/service.go
Normal file
@@ -0,0 +1,422 @@
|
||||
package accountserviceimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/auth/management"
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/db/account"
|
||||
"github.com/tech/sendico/pkg/db/organization"
|
||||
"github.com/tech/sendico/pkg/db/policy"
|
||||
"github.com/tech/sendico/pkg/db/verification"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
logger mlogger.Logger
|
||||
accountDB account.DB
|
||||
orgDB organization.DB
|
||||
enforcer auth.Enforcer
|
||||
roleManager management.Role
|
||||
config *middleware.PasswordConfig
|
||||
|
||||
policyDB policy.DB
|
||||
vdb verification.DB
|
||||
}
|
||||
|
||||
func validateUserRequest(u *model.Account) error {
|
||||
if u.Name == "" {
|
||||
return merrors.InvalidArgument("Name must not be empty")
|
||||
}
|
||||
if u.Login == "" {
|
||||
return merrors.InvalidArgument("Login must not be empty")
|
||||
}
|
||||
if u.Password == "" {
|
||||
return merrors.InvalidArgument("Password must not be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) ValidatePassword(
|
||||
password string,
|
||||
oldPassword *string,
|
||||
) error {
|
||||
var hasDigit, hasUpper, hasLower, hasSpecial bool
|
||||
|
||||
if oldPassword != nil {
|
||||
if *oldPassword == password {
|
||||
return merrors.InvalidArgument("New password cannot be the same as the old password")
|
||||
}
|
||||
}
|
||||
|
||||
if len(password) < s.config.Check.MinLength {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("Password must be at least %d characters long", s.config.Check.MinLength))
|
||||
}
|
||||
|
||||
// Check for digit, uppercase, lowercase, and special character
|
||||
for _, char := range password {
|
||||
switch {
|
||||
case unicode.IsDigit(char):
|
||||
hasDigit = true
|
||||
case unicode.IsUpper(char):
|
||||
hasUpper = true
|
||||
case unicode.IsLower(char):
|
||||
hasLower = true
|
||||
case unicode.IsPunct(char) || unicode.IsSymbol(char):
|
||||
hasSpecial = true
|
||||
}
|
||||
}
|
||||
|
||||
if s.config.Check.Digit && !hasDigit {
|
||||
return merrors.InvalidArgument("Password must contain at least one digit")
|
||||
}
|
||||
if s.config.Check.Upper && !hasUpper {
|
||||
return merrors.InvalidArgument("Password must contain at least one uppercase letter")
|
||||
}
|
||||
if s.config.Check.Lower && !hasLower {
|
||||
return merrors.InvalidArgument("Password must contain at least one lowercase letter")
|
||||
}
|
||||
if s.config.Check.Special && !hasSpecial {
|
||||
return merrors.InvalidArgument("Password must contain at least one special character")
|
||||
}
|
||||
|
||||
// If all checks pass, return nil (no error)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) ValidateAccount(acct *model.Account) error {
|
||||
if err := validateUserRequest(acct); err != nil {
|
||||
s.logger.Warn("Invalid signup acccount received", zap.Error(err), zap.String("account", acct.Login))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.ValidatePassword(acct.Password, nil); err != nil {
|
||||
s.logger.Warn("Password validation failed", zap.Error(err), zap.String("account", acct.Login))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := acct.HashPassword(); err != nil {
|
||||
s.logger.Warn("Failed to hash password", zap.Error(err), zap.String("account", acct.Login))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) CreateAccount(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
acct *model.Account,
|
||||
roleDescID bson.ObjectID,
|
||||
) (string, error) {
|
||||
if org == nil {
|
||||
return "", merrors.InvalidArgument("Organization must not be nil")
|
||||
}
|
||||
if acct == nil || len(acct.Login) == 0 {
|
||||
return "", merrors.InvalidArgument("Account must have a non-empty login")
|
||||
}
|
||||
if roleDescID == bson.NilObjectID {
|
||||
return "", merrors.InvalidArgument("Role description must be provided")
|
||||
}
|
||||
|
||||
// 1) Create the account
|
||||
acct.Status = model.AccountPendingVerification
|
||||
if err := s.accountDB.Create(ctx, acct); err != nil {
|
||||
if errors.Is(err, merrors.ErrDataConflict) {
|
||||
s.logger.Info("Username is already taken", zap.String("login", acct.Login))
|
||||
} else {
|
||||
s.logger.Warn("Failed to signup a user", zap.Error(err), zap.String("login", acct.Login))
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 2) Add to organization
|
||||
if err := s.JoinOrganization(ctx, org, acct, roleDescID); err != nil {
|
||||
s.logger.Warn("Failed to register new organization member", zap.Error(err), mzap.StorableRef(acct))
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 3) Issue verification token
|
||||
return s.VerifyAccount(ctx, acct)
|
||||
}
|
||||
|
||||
func (s *service) VerifyAccount(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
) (string, error) {
|
||||
token, err := s.vdb.Create(
|
||||
ctx,
|
||||
verification.NewLinkRequest(*acct.GetID(), model.PurposeAccountActivation, "").
|
||||
WithTTL(time.Duration(time.Hour*24)),
|
||||
)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to create verification token for new account", zap.Error(err), mzap.StorableRef(acct))
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *service) DeleteAccount(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
accountRef bson.ObjectID,
|
||||
) error {
|
||||
// Check if this is the only member in the organization
|
||||
if len(org.Members) <= 1 {
|
||||
s.logger.Warn("Cannot delete account - it's the only member in the organization",
|
||||
mzap.AccRef(accountRef), mzap.StorableRef(org))
|
||||
return merrors.InvalidArgument("Cannot delete the only member of an organization")
|
||||
}
|
||||
|
||||
// 1) Remove from organization
|
||||
if err := s.RemoveAccountFromOrganization(ctx, org, accountRef); err != nil {
|
||||
s.logger.Warn("Failed to revoke account role", zap.Error(err), mzap.AccRef(accountRef))
|
||||
return err
|
||||
}
|
||||
|
||||
// 2) Delete the account document
|
||||
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
|
||||
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.AccRef(accountRef))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) RemoveAccountFromOrganization(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
accountRef bson.ObjectID,
|
||||
) error {
|
||||
if org == nil {
|
||||
return merrors.InvalidArgument("Organization must not be nil")
|
||||
}
|
||||
roles, err := s.enforcer.GetRoles(ctx, accountRef, org.ID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to fetch account permissions", zap.Error(err), mzap.StorableRef(org),
|
||||
mzap.AccRef(accountRef))
|
||||
return err
|
||||
}
|
||||
for _, role := range roles {
|
||||
if err := s.roleManager.Revoke(ctx, role.DescriptionRef, accountRef, org.ID); err != nil {
|
||||
s.logger.Warn("Failed to revoke account role", zap.Error(err),
|
||||
mzap.AccRef(accountRef), mzap.ObjRef("role_ref", role.DescriptionRef))
|
||||
return err
|
||||
}
|
||||
}
|
||||
for i, member := range org.Members {
|
||||
if member == accountRef {
|
||||
// Remove the member by slicing it out
|
||||
org.Members = append(org.Members[:i], org.Members[i+1:]...)
|
||||
if err := s.orgDB.Update(ctx, accountRef, org); err != nil {
|
||||
s.logger.Warn("Failed to remove member from organization", zap.Error(err), mzap.AccRef(accountRef))
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) ResetPassword(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
) (string, error) {
|
||||
return s.vdb.Create(
|
||||
ctx,
|
||||
verification.NewOTPRequest(*acct.GetID(), model.PurposePasswordReset, "").
|
||||
WithTTL(time.Duration(time.Hour*1)),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *service) UpdateLogin(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
newLogin string,
|
||||
) (string, error) {
|
||||
return s.vdb.Create(
|
||||
ctx,
|
||||
verification.NewOTPRequest(*acct.GetID(), model.PurposeEmailChange, newLogin).
|
||||
WithTTL(time.Duration(time.Hour*1)),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *service) JoinOrganization(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
account *model.Account,
|
||||
roleDescID bson.ObjectID,
|
||||
) error {
|
||||
if slices.Contains(org.Members, account.ID) {
|
||||
s.logger.Debug("Account is already a member", mzap.StorableRef(org), mzap.StorableRef(account))
|
||||
return nil
|
||||
}
|
||||
|
||||
role := &model.Role{
|
||||
DescriptionRef: roleDescID,
|
||||
OrganizationRef: org.ID,
|
||||
AccountRef: account.ID,
|
||||
}
|
||||
if err := s.roleManager.Assign(ctx, role); err != nil {
|
||||
s.logger.Warn("Failed to assign role to account", zap.Error(err), mzap.StorableRef(account),
|
||||
mzap.StorableRef(org), mzap.ObjRef("role_description_ref", roleDescID))
|
||||
return err
|
||||
}
|
||||
|
||||
org.Members = append(org.Members, account.ID)
|
||||
if err := s.orgDB.Update(ctx, *account.GetID(), org); err != nil {
|
||||
s.logger.Warn("Failed to update organization members list",
|
||||
zap.Error(err), mzap.StorableRef(account), mzap.StorableRef(org))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) deleteOrganizationRoles(ctx context.Context, orgRef bson.ObjectID) error {
|
||||
s.logger.Info("Deleting roles for organization", mzap.ObjRef("organization_ref", orgRef))
|
||||
|
||||
// Get all roles for the organization
|
||||
roles, err := s.roleManager.List(ctx, orgRef)
|
||||
if err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||
s.logger.Warn("Failed to fetch roles for deletion", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete each role
|
||||
for _, role := range roles {
|
||||
if err := s.roleManager.Delete(ctx, role.ID); err != nil {
|
||||
s.logger.Warn("Failed to delete role", zap.Error(err), mzap.ObjRef("role_ref", role.ID))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("Successfully deleted roles", zap.Int("count", len(roles)), mzap.ObjRef("organization_ref", orgRef))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) deleteOrganizationPolicies(ctx context.Context, orgRef bson.ObjectID) error {
|
||||
s.logger.Info("Deleting policies for organization", mzap.ObjRef("organization_ref", orgRef))
|
||||
|
||||
// Get all policies for the organization
|
||||
policies, err := s.policyDB.All(ctx, orgRef)
|
||||
if err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||
s.logger.Warn("Failed to fetch policies for deletion", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete each policy
|
||||
for _, policy := range policies {
|
||||
if err := s.policyDB.Delete(ctx, policy.ID); err != nil {
|
||||
s.logger.Warn("Failed to delete policy", zap.Error(err), mzap.ObjRef("policy_ref", policy.ID))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("Successfully deleted policies", zap.Int("count", len(policies)), mzap.ObjRef("organization_ref", orgRef))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) DeleteOrganization(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
) error {
|
||||
s.logger.Info("Starting organization deletion", mzap.StorableRef(org))
|
||||
|
||||
// Use transaction to ensure atomicity
|
||||
// 8. Delete all roles and role descriptions in the organization
|
||||
if err := s.deleteOrganizationRoles(ctx, org.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 9. Delete all policies in the organization
|
||||
if err := s.deleteOrganizationPolicies(ctx, org.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 10. Finally, delete the organization itself
|
||||
if err := s.orgDB.Delete(ctx, bson.NilObjectID, org.ID); err != nil {
|
||||
s.logger.Warn("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Organization deleted successfully", mzap.StorableRef(org))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) DeleteAll(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
accountRef bson.ObjectID,
|
||||
) error {
|
||||
s.logger.Info("Starting complete deletion (organization + account)",
|
||||
mzap.StorableRef(org), mzap.AccRef(accountRef))
|
||||
|
||||
// 1. First delete the organization and all its data
|
||||
if err := s.DeleteOrganization(ctx, org); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Then delete the account
|
||||
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
|
||||
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.AccRef(accountRef))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Complete deletion successful", mzap.StorableRef(org), mzap.AccRef(accountRef))
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewAccountService wires in your logger plus the three dependencies.
|
||||
func NewAccountService(
|
||||
l mlogger.Logger,
|
||||
dbf db.Factory,
|
||||
enforcer auth.Enforcer,
|
||||
ra management.Role,
|
||||
config *middleware.PasswordConfig,
|
||||
) (*service, error) {
|
||||
logger := l.Named("account_service")
|
||||
|
||||
if config == nil {
|
||||
return nil, merrors.Internal("Invalid account service configuration provides")
|
||||
}
|
||||
|
||||
res := &service{
|
||||
logger: logger,
|
||||
enforcer: enforcer,
|
||||
roleManager: ra,
|
||||
config: config,
|
||||
}
|
||||
var err error
|
||||
if res.accountDB, err = dbf.NewAccountDB(); err != nil {
|
||||
logger.Warn("Failed to create accounts database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if res.orgDB, err = dbf.NewOrganizationDB(); err != nil {
|
||||
logger.Warn("Failed to create organizations database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize database dependencies for cascade deletion
|
||||
if res.policyDB, err = dbf.NewPoliciesDB(); err != nil {
|
||||
logger.Warn("Failed to create policies database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if res.vdb, err = dbf.NewVerificationsDB(); err != nil {
|
||||
logger.Warn("Failed to create verification database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package accountserviceimp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestDeleteAccount_Validation(t *testing.T) {
|
||||
t.Run("DeleteAccount_LastMemberFails", func(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
accountID := bson.NewObjectID()
|
||||
|
||||
org := &model.Organization{
|
||||
OrganizationBase: model.OrganizationBase{
|
||||
Describable: model.Describable{Name: "Single Member Org"},
|
||||
},
|
||||
Members: []bson.ObjectID{accountID}, // Only one member
|
||||
}
|
||||
org.ID = orgID
|
||||
|
||||
// This should fail because it's the only member
|
||||
err := validateDeleteAccount(org)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Cannot delete the only member")
|
||||
})
|
||||
|
||||
t.Run("DeleteAccount_MultipleMembersSuccess", func(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
accountID := bson.NewObjectID()
|
||||
otherAccountID := bson.NewObjectID()
|
||||
|
||||
org := &model.Organization{
|
||||
OrganizationBase: model.OrganizationBase{
|
||||
Describable: model.Describable{Name: "Multi Member Org"},
|
||||
},
|
||||
Members: []bson.ObjectID{accountID, otherAccountID}, // Multiple members
|
||||
}
|
||||
org.ID = orgID
|
||||
|
||||
// This should succeed because there are multiple members
|
||||
err := validateDeleteAccount(org)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("DeleteAccount_EmptyMembersList", func(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
|
||||
org := &model.Organization{
|
||||
OrganizationBase: model.OrganizationBase{
|
||||
Describable: model.Describable{Name: "Empty Org"},
|
||||
},
|
||||
Members: []bson.ObjectID{}, // No members
|
||||
}
|
||||
org.ID = orgID
|
||||
|
||||
// This should fail because there are no members
|
||||
err := validateDeleteAccount(org)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Cannot delete the only member")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteOrganization_Validation(t *testing.T) {
|
||||
t.Run("DeleteOrganization_NilOrganization", func(t *testing.T) {
|
||||
err := validateDeleteOrganization(nil)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "organization cannot be nil")
|
||||
})
|
||||
|
||||
t.Run("DeleteOrganization_EmptyOrganization", func(t *testing.T) {
|
||||
org := &model.Organization{}
|
||||
err := validateDeleteOrganization(org)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "organization ID cannot be empty")
|
||||
})
|
||||
|
||||
t.Run("DeleteOrganization_ValidOrganization", func(t *testing.T) {
|
||||
org := &model.Organization{
|
||||
OrganizationBase: model.OrganizationBase{
|
||||
Describable: model.Describable{Name: "Valid Organization"},
|
||||
},
|
||||
}
|
||||
org.ID = bson.NewObjectID()
|
||||
|
||||
err := validateDeleteOrganization(org)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteAll_Validation(t *testing.T) {
|
||||
t.Run("DeleteAll_NilOrganization", func(t *testing.T) {
|
||||
accountID := bson.NewObjectID()
|
||||
err := validateDeleteAll(nil, accountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "organization cannot be nil")
|
||||
})
|
||||
|
||||
t.Run("DeleteAll_EmptyAccountID", func(t *testing.T) {
|
||||
org := &model.Organization{
|
||||
OrganizationBase: model.OrganizationBase{
|
||||
Describable: model.Describable{Name: "Valid Organization"},
|
||||
},
|
||||
}
|
||||
org.ID = bson.NewObjectID()
|
||||
|
||||
err := validateDeleteAll(org, bson.NilObjectID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "account ID cannot be empty")
|
||||
})
|
||||
|
||||
t.Run("DeleteAll_ValidInput", func(t *testing.T) {
|
||||
org := &model.Organization{
|
||||
OrganizationBase: model.OrganizationBase{
|
||||
Describable: model.Describable{Name: "Valid Organization"},
|
||||
},
|
||||
}
|
||||
org.ID = bson.NewObjectID()
|
||||
accountID := bson.NewObjectID()
|
||||
|
||||
err := validateDeleteAll(org, accountID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions that implement the validation logic from the service
|
||||
func validateDeleteAccount(org *model.Organization) error {
|
||||
if len(org.Members) <= 1 {
|
||||
return merrors.InvalidArgument("Cannot delete the only member of an organization")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDeleteOrganization(org *model.Organization) error {
|
||||
if org == nil {
|
||||
return merrors.InvalidArgument("organization cannot be nil")
|
||||
}
|
||||
if org.ID == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("organization ID cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDeleteAll(org *model.Organization, accountRef bson.ObjectID) error {
|
||||
if org == nil {
|
||||
return merrors.InvalidArgument("organization cannot be nil")
|
||||
}
|
||||
if accountRef == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("account ID cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
245
api/edge/bff/interface/accountservice/internal/service_test.go
Normal file
245
api/edge/bff/interface/accountservice/internal/service_test.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package accountserviceimp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
apiconfig "github.com/tech/sendico/server/internal/api/config"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TestValidatePassword tests the password validation logic directly
|
||||
func TestValidatePassword(t *testing.T) {
|
||||
config := &apiconfig.PasswordConfig{
|
||||
Check: apiconfig.PasswordChecks{
|
||||
MinLength: 8,
|
||||
Digit: true,
|
||||
Upper: true,
|
||||
Lower: true,
|
||||
Special: true,
|
||||
},
|
||||
TokenLength: 32,
|
||||
}
|
||||
|
||||
// Create a minimal service for testing password validation
|
||||
logger := zap.NewNop() // Use no-op logger for tests
|
||||
service := &service{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
t.Run("ValidPassword", func(t *testing.T) {
|
||||
err := service.ValidatePassword("TestPassword123!", nil)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("PasswordTooShort", func(t *testing.T) {
|
||||
err := service.ValidatePassword("Test1!", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "at least 8 characters")
|
||||
})
|
||||
|
||||
t.Run("PasswordMissingDigit", func(t *testing.T) {
|
||||
err := service.ValidatePassword("TestPassword!", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "at least one digit")
|
||||
})
|
||||
|
||||
t.Run("PasswordMissingUppercase", func(t *testing.T) {
|
||||
err := service.ValidatePassword("testpassword123!", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "at least one uppercase")
|
||||
})
|
||||
|
||||
t.Run("PasswordMissingLowercase", func(t *testing.T) {
|
||||
err := service.ValidatePassword("TESTPASSWORD123!", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "at least one lowercase")
|
||||
})
|
||||
|
||||
t.Run("PasswordMissingSpecialCharacter", func(t *testing.T) {
|
||||
err := service.ValidatePassword("TestPassword123", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "at least one special character")
|
||||
})
|
||||
|
||||
t.Run("PasswordSameAsOld", func(t *testing.T) {
|
||||
oldPassword := "TestPassword123!"
|
||||
err := service.ValidatePassword("TestPassword123!", &oldPassword)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cannot be the same as the old password")
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidateAccount tests the account validation logic directly
|
||||
func TestValidateAccount(t *testing.T) {
|
||||
config := &apiconfig.PasswordConfig{
|
||||
Check: apiconfig.PasswordChecks{
|
||||
MinLength: 8,
|
||||
Digit: true,
|
||||
Upper: true,
|
||||
Lower: true,
|
||||
Special: true,
|
||||
},
|
||||
TokenLength: 32,
|
||||
}
|
||||
|
||||
logger := zap.NewNop() // Use no-op logger for tests
|
||||
service := &service{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
t.Run("ValidAccount", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
AccountBase: model.AccountBase{
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
}
|
||||
|
||||
originalPassword := account.Password
|
||||
err := service.ValidateAccount(account)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Password should be hashed after validation
|
||||
assert.NotEqual(t, originalPassword, account.Password)
|
||||
})
|
||||
|
||||
t.Run("AccountMissingName", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
}
|
||||
|
||||
err := service.ValidateAccount(account)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Name must not be empty")
|
||||
})
|
||||
|
||||
t.Run("AccountMissingLogin", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
AccountBase: model.AccountBase{
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
}
|
||||
|
||||
err := service.ValidateAccount(account)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Login must not be empty")
|
||||
})
|
||||
|
||||
t.Run("AccountMissingPassword", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
AccountBase: model.AccountBase{
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
},
|
||||
Password: "",
|
||||
}
|
||||
|
||||
err := service.ValidateAccount(account)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Password must not be empty")
|
||||
})
|
||||
|
||||
t.Run("AccountInvalidPassword", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
AccountBase: model.AccountBase{
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
},
|
||||
Password: "weak", // Should fail validation
|
||||
}
|
||||
|
||||
err := service.ValidateAccount(account)
|
||||
assert.Error(t, err)
|
||||
// Should fail on password validation
|
||||
assert.Contains(t, err.Error(), "at least 8 characters")
|
||||
})
|
||||
}
|
||||
|
||||
// TestPasswordConfiguration verifies different password rule configurations
|
||||
func TestPasswordConfiguration(t *testing.T) {
|
||||
t.Run("MinimalRequirements", func(t *testing.T) {
|
||||
config := &apiconfig.PasswordConfig{
|
||||
Check: apiconfig.PasswordChecks{
|
||||
MinLength: 4,
|
||||
Digit: false,
|
||||
Upper: false,
|
||||
Lower: false,
|
||||
Special: false,
|
||||
},
|
||||
TokenLength: 16,
|
||||
}
|
||||
|
||||
logger := zap.NewNop() // Use no-op logger for tests
|
||||
service := &service{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Should pass with minimal requirements
|
||||
err := service.ValidatePassword("test", nil)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("StrictRequirements", func(t *testing.T) {
|
||||
config := &apiconfig.PasswordConfig{
|
||||
Check: apiconfig.PasswordChecks{
|
||||
MinLength: 12,
|
||||
Digit: true,
|
||||
Upper: true,
|
||||
Lower: true,
|
||||
Special: true,
|
||||
},
|
||||
TokenLength: 64,
|
||||
}
|
||||
|
||||
logger := zap.NewNop() // Use no-op logger for tests
|
||||
service := &service{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Should fail with shorter password
|
||||
err := service.ValidatePassword("Test123!", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "at least 12 characters")
|
||||
|
||||
// Should pass with longer password
|
||||
err = service.ValidatePassword("TestPassword123!", nil)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
104
api/edge/bff/interface/accountservice/types.go
Normal file
104
api/edge/bff/interface/accountservice/types.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package accountservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/auth/management"
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
accountserviceimp "github.com/tech/sendico/server/interface/accountservice/internal"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// AccountService defines all account-related workflows.
|
||||
type AccountService interface {
|
||||
// ValidateAccount will:
|
||||
// 1) check it's completeness
|
||||
// 2) hash password
|
||||
// 3) prepare verification token
|
||||
ValidateAccount(
|
||||
acct *model.Account,
|
||||
) error
|
||||
|
||||
// ValidatePassword will:
|
||||
// 1) check passsword conformance
|
||||
ValidatePassword(
|
||||
password string,
|
||||
oldPassword *string,
|
||||
) error
|
||||
|
||||
// ResetPassword will:
|
||||
// 1) generate reset password token
|
||||
ResetPassword(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
) (verificationToken string, err error)
|
||||
|
||||
// CreateAccount will:
|
||||
// 1) create the account
|
||||
// 2) add it to the org’s member list
|
||||
// 3) assign the given role description to it
|
||||
CreateAccount(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
acct *model.Account,
|
||||
roleDescID bson.ObjectID,
|
||||
) (verificationToken string, err error)
|
||||
|
||||
VerifyAccount(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
) (verificationToken string, err error)
|
||||
|
||||
JoinOrganization(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
acct *model.Account,
|
||||
roleDescID bson.ObjectID,
|
||||
) error
|
||||
|
||||
UpdateLogin(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
newLogin string,
|
||||
) (verificationToken string, err error)
|
||||
|
||||
// DeleteAccount deletes the account and removes it from the org.
|
||||
DeleteAccount(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
accountRef bson.ObjectID,
|
||||
) error
|
||||
|
||||
// RemoveAccountFromOrganization just drops it from the member slice.
|
||||
RemoveAccountFromOrganization(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
accountRef bson.ObjectID,
|
||||
) error
|
||||
|
||||
DeleteOrganization(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
) error
|
||||
|
||||
// DeleteAll deletes both the organization and the account.
|
||||
DeleteAll(
|
||||
ctx context.Context,
|
||||
org *model.Organization,
|
||||
accountRef bson.ObjectID,
|
||||
) error
|
||||
}
|
||||
|
||||
func NewAccountService(
|
||||
logger mlogger.Logger,
|
||||
dbf db.Factory,
|
||||
enforcer auth.Enforcer,
|
||||
roleManeger management.Role,
|
||||
config *middleware.PasswordConfig,
|
||||
) (AccountService, error) {
|
||||
return accountserviceimp.NewAccountService(logger, dbf, enforcer, roleManeger, config)
|
||||
}
|
||||
20
api/edge/bff/interface/api/api.go
Normal file
20
api/edge/bff/interface/api/api.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/domainprovider"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
Logger() mlogger.Logger
|
||||
DomainProvider() domainprovider.DomainProvider
|
||||
Config() *Config
|
||||
DBFactory() db.Factory
|
||||
Permissions() auth.Provider
|
||||
Register() Register
|
||||
}
|
||||
|
||||
type MicroServiceFactoryT = func(API) (mservice.MicroService, error)
|
||||
47
api/edge/bff/interface/api/config.go
Normal file
47
api/edge/bff/interface/api/config.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
mwa "github.com/tech/sendico/server/interface/middleware"
|
||||
fsc "github.com/tech/sendico/server/interface/services/fileservice/config"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Mw *mwa.Config `yaml:"middleware"`
|
||||
Storage *fsc.Config `yaml:"storage"`
|
||||
ChainGateway *ChainGatewayConfig `yaml:"chain_gateway"`
|
||||
Ledger *LedgerConfig `yaml:"ledger"`
|
||||
PaymentOrchestrator *PaymentOrchestratorConfig `yaml:"payment_orchestrator"`
|
||||
PaymentQuotation *PaymentOrchestratorConfig `yaml:"payment_quotation"`
|
||||
PaymentMethods *PaymentOrchestratorConfig `yaml:"payment_methods"`
|
||||
}
|
||||
|
||||
type ChainGatewayConfig struct {
|
||||
Address string `yaml:"address"`
|
||||
AddressEnv string `yaml:"address_env"`
|
||||
DialTimeoutSeconds int `yaml:"dial_timeout_seconds"`
|
||||
CallTimeoutSeconds int `yaml:"call_timeout_seconds"`
|
||||
Insecure bool `yaml:"insecure"`
|
||||
DefaultAsset ChainGatewayAssetConfig `yaml:"default_asset"`
|
||||
}
|
||||
|
||||
type ChainGatewayAssetConfig struct {
|
||||
Chain string `yaml:"chain"`
|
||||
TokenSymbol string `yaml:"token_symbol"`
|
||||
ContractAddress string `yaml:"contract_address"`
|
||||
}
|
||||
|
||||
type LedgerConfig struct {
|
||||
Address string `yaml:"address"`
|
||||
AddressEnv string `yaml:"address_env"`
|
||||
DialTimeoutSeconds int `yaml:"dial_timeout_seconds"`
|
||||
CallTimeoutSeconds int `yaml:"call_timeout_seconds"`
|
||||
Insecure bool `yaml:"insecure"`
|
||||
}
|
||||
|
||||
type PaymentOrchestratorConfig struct {
|
||||
Address string `yaml:"address"`
|
||||
AddressEnv string `yaml:"address_env"`
|
||||
DialTimeoutSeconds int `yaml:"dial_timeout_seconds"`
|
||||
CallTimeoutSeconds int `yaml:"call_timeout_seconds"`
|
||||
Insecure bool `yaml:"insecure"`
|
||||
}
|
||||
10
api/edge/bff/interface/api/permissions/deny.go
Normal file
10
api/edge/bff/interface/api/permissions/deny.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
func Deny(_ *model.Account, _ *auth.Enforcer) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
10
api/edge/bff/interface/api/permissions/donotcheck.go
Normal file
10
api/edge/bff/interface/api/permissions/donotcheck.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
func DoNotCheck(_ *model.Account, _ *auth.Enforcer) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
18
api/edge/bff/interface/api/register.go
Normal file
18
api/edge/bff/interface/api/register.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"github.com/tech/sendico/server/interface/api/ws"
|
||||
)
|
||||
|
||||
type Register interface {
|
||||
Handler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc)
|
||||
AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc)
|
||||
PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc)
|
||||
WSHandler(messageType string, handler ws.HandlerFunc)
|
||||
|
||||
Messaging() messaging.Register
|
||||
}
|
||||
7
api/edge/bff/interface/api/srequest/acceptinvitation.go
Normal file
7
api/edge/bff/interface/api/srequest/acceptinvitation.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package srequest
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
type AcceptInvitation struct {
|
||||
Account *model.AccountData `json:"account,omitempty"`
|
||||
}
|
||||
12
api/edge/bff/interface/api/srequest/changepolicies.go
Normal file
12
api/edge/bff/interface/api/srequest/changepolicies.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type ChangePolicies struct {
|
||||
RoleRef bson.ObjectID `json:"roleRef"`
|
||||
Add *[]model.RolePolicy `json:"add,omitempty"`
|
||||
Remove *[]model.RolePolicy `json:"remove,omitempty"`
|
||||
}
|
||||
8
api/edge/bff/interface/api/srequest/changerole.go
Normal file
8
api/edge/bff/interface/api/srequest/changerole.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package srequest
|
||||
|
||||
import "go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
type ChangeRole struct {
|
||||
AccountRef bson.ObjectID `json:"accountRef"`
|
||||
NewRoleDescriptionRef bson.ObjectID `json:"newRoleDescriptionRef"`
|
||||
}
|
||||
15
api/edge/bff/interface/api/srequest/customer.go
Normal file
15
api/edge/bff/interface/api/srequest/customer.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package srequest
|
||||
|
||||
// Customer captures payer/recipient identity details for downstream processing.
|
||||
type Customer struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
MiddleName string `json:"middle_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
Zip string `json:"zip,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
}
|
||||
76
api/edge/bff/interface/api/srequest/endpoint_payloads.go
Normal file
76
api/edge/bff/interface/api/srequest/endpoint_payloads.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package srequest
|
||||
|
||||
// Asset represents a chain/token pair for blockchain endpoints.
|
||||
type Asset struct {
|
||||
Chain ChainNetwork `json:"chain"`
|
||||
TokenSymbol string `json:"token_symbol"`
|
||||
ContractAddress string `json:"contract_address,omitempty"`
|
||||
}
|
||||
|
||||
// LedgerEndpoint represents a ledger account payload.
|
||||
type LedgerEndpoint struct {
|
||||
LedgerAccountRef string `json:"ledger_account_ref"`
|
||||
ContraLedgerAccountRef string `json:"contra_ledger_account_ref,omitempty"`
|
||||
}
|
||||
|
||||
// ManagedWalletEndpoint represents a managed wallet payload.
|
||||
type ManagedWalletEndpoint struct {
|
||||
ManagedWalletRef string `json:"managed_wallet_ref"`
|
||||
Asset *Asset `json:"asset,omitempty"`
|
||||
}
|
||||
|
||||
// ExternalChainEndpoint represents an external chain address payload.
|
||||
type ExternalChainEndpoint struct {
|
||||
Asset *Asset `json:"asset,omitempty"`
|
||||
Address string `json:"address"`
|
||||
Memo string `json:"memo,omitempty"`
|
||||
}
|
||||
|
||||
// CardEndpoint represents a card payout payload (PAN or network token).
|
||||
type CardEndpoint struct {
|
||||
Pan string `json:"pan"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
ExpMonth uint32 `json:"exp_month,omitempty"`
|
||||
ExpYear uint32 `json:"exp_year,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
}
|
||||
|
||||
// CardTokenEndpoint represents a vaulted card token payout payload.
|
||||
type CardTokenEndpoint struct {
|
||||
Token string `json:"token"`
|
||||
MaskedPan string `json:"masked_pan"`
|
||||
}
|
||||
|
||||
// WalletEndpoint represents a Sendico wallet payout payload.
|
||||
type WalletEndpoint struct {
|
||||
WalletID string `json:"walletId"`
|
||||
}
|
||||
|
||||
// BankAccountEndpoint represents a domestic bank account payout payload.
|
||||
type BankAccountEndpoint struct {
|
||||
RecipientName string `json:"recipientName"`
|
||||
Inn string `json:"inn"`
|
||||
Kpp string `json:"kpp"`
|
||||
BankName string `json:"bankName"`
|
||||
Bik string `json:"bik"`
|
||||
AccountNumber string `json:"accountNumber"`
|
||||
CorrespondentAccount string `json:"correspondentAccount"`
|
||||
}
|
||||
|
||||
// IBANEndpoint represents an international bank account payout payload.
|
||||
type IBANEndpoint struct {
|
||||
IBAN string `json:"iban"`
|
||||
AccountHolder string `json:"accountHolder"`
|
||||
BIC string `json:"bic,omitempty"`
|
||||
BankName string `json:"bankName,omitempty"`
|
||||
}
|
||||
|
||||
// LegacyPaymentEndpoint mirrors the previous bag-of-pointers DTO for backward compatibility.
|
||||
type LegacyPaymentEndpoint struct {
|
||||
Ledger *LedgerEndpoint `json:"ledger,omitempty"`
|
||||
ManagedWallet *ManagedWalletEndpoint `json:"managed_wallet,omitempty"`
|
||||
ExternalChain *ExternalChainEndpoint `json:"external_chain,omitempty"`
|
||||
Card *CardEndpoint `json:"card,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
266
api/edge/bff/interface/api/srequest/endpoint_union.go
Normal file
266
api/edge/bff/interface/api/srequest/endpoint_union.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
type EndpointType string
|
||||
|
||||
const (
|
||||
EndpointTypeLedger EndpointType = "ledger"
|
||||
EndpointTypeManagedWallet EndpointType = "managedWallet"
|
||||
EndpointTypeExternalChain EndpointType = "cryptoAddress"
|
||||
EndpointTypeCard EndpointType = "card"
|
||||
EndpointTypeCardToken EndpointType = "cardToken"
|
||||
EndpointTypeWallet EndpointType = "wallet"
|
||||
EndpointTypeBankAccount EndpointType = "bankAccount"
|
||||
EndpointTypeIBAN EndpointType = "iban"
|
||||
)
|
||||
|
||||
// Endpoint is a discriminated union for payment endpoints.
|
||||
type Endpoint struct {
|
||||
Type EndpointType `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func newEndpoint(kind EndpointType, payload interface{}, metadata map[string]string) (Endpoint, error) {
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return Endpoint{}, merrors.Internal("marshal endpoint payload failed")
|
||||
}
|
||||
return Endpoint{
|
||||
Type: kind,
|
||||
Data: data,
|
||||
Metadata: cloneStringMap(metadata),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e Endpoint) decodePayload(expected EndpointType, dst interface{}) error {
|
||||
actual := normalizeEndpointType(e.Type)
|
||||
if actual == "" {
|
||||
return merrors.InvalidArgument("endpoint type is required")
|
||||
}
|
||||
if actual != expected {
|
||||
return merrors.InvalidArgument("expected endpoint type " + string(expected) + ", got " + string(e.Type))
|
||||
}
|
||||
if len(e.Data) == 0 {
|
||||
return merrors.InvalidArgument("endpoint data is required for type " + string(expected))
|
||||
}
|
||||
if err := json.Unmarshal(e.Data, dst); err != nil {
|
||||
return merrors.InvalidArgument("decode " + string(expected) + " endpoint: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Endpoint) UnmarshalJSON(data []byte) error {
|
||||
var envelope struct {
|
||||
Type EndpointType `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &envelope); err == nil {
|
||||
if envelope.Type != "" || len(envelope.Data) > 0 {
|
||||
if envelope.Type == "" {
|
||||
return merrors.InvalidArgument("endpoint type is required")
|
||||
}
|
||||
*e = Endpoint{
|
||||
Type: normalizeEndpointType(envelope.Type),
|
||||
Data: envelope.Data,
|
||||
Metadata: cloneStringMap(envelope.Metadata),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var legacy LegacyPaymentEndpoint
|
||||
if err := json.Unmarshal(data, &legacy); err != nil {
|
||||
return err
|
||||
}
|
||||
endpoint, err := LegacyPaymentEndpointToEndpointDTO(&legacy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if endpoint == nil {
|
||||
return merrors.InvalidArgument("endpoint payload is empty")
|
||||
}
|
||||
*e = *endpoint
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLedgerEndpointDTO(payload LedgerEndpoint, metadata map[string]string) (Endpoint, error) {
|
||||
return newEndpoint(EndpointTypeLedger, payload, metadata)
|
||||
}
|
||||
|
||||
func NewManagedWalletEndpointDTO(payload ManagedWalletEndpoint, metadata map[string]string) (Endpoint, error) {
|
||||
return newEndpoint(EndpointTypeManagedWallet, payload, metadata)
|
||||
}
|
||||
|
||||
func NewExternalChainEndpointDTO(payload ExternalChainEndpoint, metadata map[string]string) (Endpoint, error) {
|
||||
return newEndpoint(EndpointTypeExternalChain, payload, metadata)
|
||||
}
|
||||
|
||||
func NewCardEndpointDTO(payload CardEndpoint, metadata map[string]string) (Endpoint, error) {
|
||||
return newEndpoint(EndpointTypeCard, payload, metadata)
|
||||
}
|
||||
|
||||
func NewCardTokenEndpointDTO(payload CardTokenEndpoint, metadata map[string]string) (Endpoint, error) {
|
||||
return newEndpoint(EndpointTypeCardToken, payload, metadata)
|
||||
}
|
||||
|
||||
func NewWalletEndpointDTO(payload WalletEndpoint, metadata map[string]string) (Endpoint, error) {
|
||||
return newEndpoint(EndpointTypeWallet, payload, metadata)
|
||||
}
|
||||
|
||||
func NewBankAccountEndpointDTO(payload BankAccountEndpoint, metadata map[string]string) (Endpoint, error) {
|
||||
return newEndpoint(EndpointTypeBankAccount, payload, metadata)
|
||||
}
|
||||
|
||||
func NewIBANEndpointDTO(payload IBANEndpoint, metadata map[string]string) (Endpoint, error) {
|
||||
return newEndpoint(EndpointTypeIBAN, payload, metadata)
|
||||
}
|
||||
|
||||
func (e Endpoint) DecodeLedger() (LedgerEndpoint, error) {
|
||||
var payload LedgerEndpoint
|
||||
return payload, e.decodePayload(EndpointTypeLedger, &payload)
|
||||
}
|
||||
|
||||
func (e Endpoint) DecodeManagedWallet() (ManagedWalletEndpoint, error) {
|
||||
var payload ManagedWalletEndpoint
|
||||
return payload, e.decodePayload(EndpointTypeManagedWallet, &payload)
|
||||
}
|
||||
|
||||
func (e Endpoint) DecodeExternalChain() (ExternalChainEndpoint, error) {
|
||||
var payload ExternalChainEndpoint
|
||||
return payload, e.decodePayload(EndpointTypeExternalChain, &payload)
|
||||
}
|
||||
|
||||
func (e Endpoint) DecodeCard() (CardEndpoint, error) {
|
||||
var payload CardEndpoint
|
||||
return payload, e.decodePayload(EndpointTypeCard, &payload)
|
||||
}
|
||||
|
||||
func (e Endpoint) DecodeCardToken() (CardTokenEndpoint, error) {
|
||||
var payload CardTokenEndpoint
|
||||
return payload, e.decodePayload(EndpointTypeCardToken, &payload)
|
||||
}
|
||||
|
||||
func (e Endpoint) DecodeWallet() (WalletEndpoint, error) {
|
||||
var payload WalletEndpoint
|
||||
return payload, e.decodePayload(EndpointTypeWallet, &payload)
|
||||
}
|
||||
|
||||
func (e Endpoint) DecodeBankAccount() (BankAccountEndpoint, error) {
|
||||
var payload BankAccountEndpoint
|
||||
return payload, e.decodePayload(EndpointTypeBankAccount, &payload)
|
||||
}
|
||||
|
||||
func (e Endpoint) DecodeIBAN() (IBANEndpoint, error) {
|
||||
var payload IBANEndpoint
|
||||
return payload, e.decodePayload(EndpointTypeIBAN, &payload)
|
||||
}
|
||||
|
||||
func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint, error) {
|
||||
if old == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
count := 0
|
||||
var endpoint Endpoint
|
||||
var err error
|
||||
|
||||
if old.Ledger != nil {
|
||||
count++
|
||||
endpoint, err = NewLedgerEndpointDTO(*old.Ledger, old.Metadata)
|
||||
}
|
||||
if old.ManagedWallet != nil {
|
||||
count++
|
||||
endpoint, err = NewManagedWalletEndpointDTO(*old.ManagedWallet, old.Metadata)
|
||||
}
|
||||
if old.ExternalChain != nil {
|
||||
count++
|
||||
endpoint, err = NewExternalChainEndpointDTO(*old.ExternalChain, old.Metadata)
|
||||
}
|
||||
if old.Card != nil {
|
||||
count++
|
||||
endpoint, err = NewCardEndpointDTO(*old.Card, old.Metadata)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count == 0 {
|
||||
return nil, merrors.InvalidArgument("exactly one endpoint must be set")
|
||||
}
|
||||
if count > 1 {
|
||||
return nil, merrors.InvalidArgument("only one endpoint can be set")
|
||||
}
|
||||
return &endpoint, nil
|
||||
}
|
||||
|
||||
func EndpointDTOToLegacyPaymentEndpoint(new *Endpoint) (*LegacyPaymentEndpoint, error) {
|
||||
if new == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
legacy := &LegacyPaymentEndpoint{
|
||||
Metadata: cloneStringMap(new.Metadata),
|
||||
}
|
||||
|
||||
switch normalizeEndpointType(new.Type) {
|
||||
case EndpointTypeLedger:
|
||||
payload, err := new.DecodeLedger()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
legacy.Ledger = &payload
|
||||
case EndpointTypeManagedWallet:
|
||||
payload, err := new.DecodeManagedWallet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
legacy.ManagedWallet = &payload
|
||||
case EndpointTypeExternalChain:
|
||||
payload, err := new.DecodeExternalChain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
legacy.ExternalChain = &payload
|
||||
case EndpointTypeCard:
|
||||
payload, err := new.DecodeCard()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
legacy.Card = &payload
|
||||
default:
|
||||
return nil, merrors.InvalidArgument("unsupported endpoint type: " + string(new.Type))
|
||||
}
|
||||
return legacy, nil
|
||||
}
|
||||
|
||||
var endpointTypeAliases = map[EndpointType]EndpointType{
|
||||
"managed_wallet": EndpointTypeManagedWallet,
|
||||
"external_chain": EndpointTypeExternalChain,
|
||||
"card_token": EndpointTypeCardToken,
|
||||
"bank_account": EndpointTypeBankAccount,
|
||||
}
|
||||
|
||||
func normalizeEndpointType(t EndpointType) EndpointType {
|
||||
if canonical, ok := endpointTypeAliases[t]; ok {
|
||||
return canonical
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func cloneStringMap(src map[string]string) map[string]string {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
dst := make(map[string]string, len(src))
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
7
api/edge/bff/interface/api/srequest/file.go
Normal file
7
api/edge/bff/interface/api/srequest/file.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package srequest
|
||||
|
||||
import "go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
type FileUpload struct {
|
||||
ObjRef bson.ObjectID `json:"objRef"`
|
||||
}
|
||||
7
api/edge/bff/interface/api/srequest/invitation.go
Normal file
7
api/edge/bff/interface/api/srequest/invitation.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type CreateInvitation = model.Invitation
|
||||
54
api/edge/bff/interface/api/srequest/ledger.go
Normal file
54
api/edge/bff/interface/api/srequest/ledger.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/ledgerconv"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/model/account_role"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type LedgerAccountType string
|
||||
|
||||
const (
|
||||
LedgerAccountTypeUnspecified LedgerAccountType = "unspecified"
|
||||
LedgerAccountTypeAsset LedgerAccountType = "asset"
|
||||
LedgerAccountTypeLiability LedgerAccountType = "liability"
|
||||
LedgerAccountTypeRevenue LedgerAccountType = "revenue"
|
||||
LedgerAccountTypeExpense LedgerAccountType = "expense"
|
||||
)
|
||||
|
||||
type LedgerAccountStatus string
|
||||
|
||||
const (
|
||||
LedgerAccountStatusUnspecified LedgerAccountStatus = "unspecified"
|
||||
LedgerAccountStatusActive LedgerAccountStatus = "active"
|
||||
LedgerAccountStatusFrozen LedgerAccountStatus = "frozen"
|
||||
)
|
||||
|
||||
type CreateLedgerAccount struct {
|
||||
AccountType LedgerAccountType `json:"accountType"`
|
||||
Currency string `json:"currency"`
|
||||
AllowNegative bool `json:"allowNegative,omitempty"`
|
||||
Role account_role.AccountRole `json:"role"`
|
||||
Describable model.Describable `json:"describable"`
|
||||
OwnerRef *bson.ObjectID `json:"ownerRef,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (r *CreateLedgerAccount) Validate() error {
|
||||
if strings.TrimSpace(r.Currency) == "" {
|
||||
return merrors.InvalidArgument("currency is required", "currency")
|
||||
}
|
||||
if strings.TrimSpace(string(r.AccountType)) == "" || strings.EqualFold(string(r.AccountType), string(LedgerAccountTypeUnspecified)) {
|
||||
return merrors.InvalidArgument("accountType is required", "accountType")
|
||||
}
|
||||
if role := strings.TrimSpace(string(r.Role)); role != "" {
|
||||
if _, ok := ledgerconv.ParseAccountRole(role); !ok || ledgerconv.IsAccountRoleUnspecified(role) {
|
||||
return merrors.InvalidArgument("role is invalid", "role")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
8
api/edge/bff/interface/api/srequest/login.go
Normal file
8
api/edge/bff/interface/api/srequest/login.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package srequest
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
type Login struct {
|
||||
model.SessionIdentifier `json:",inline"`
|
||||
model.LoginData `json:"login"`
|
||||
}
|
||||
15
api/edge/bff/interface/api/srequest/password.go
Normal file
15
api/edge/bff/interface/api/srequest/password.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package srequest
|
||||
|
||||
type ChangePassword struct {
|
||||
Old string `json:"old"`
|
||||
New string `json:"new"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
}
|
||||
|
||||
type ResetPassword struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type ForgotPassword struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
126
api/edge/bff/interface/api/srequest/payment.go
Normal file
126
api/edge/bff/interface/api/srequest/payment.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
type PaymentBase struct {
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (b *PaymentBase) Validate() error {
|
||||
if b.IdempotencyKey == "" {
|
||||
return merrors.InvalidArgument("idempotencyKey is required", "idempotencyKey")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type QuotePayment struct {
|
||||
PaymentBase `json:",inline"`
|
||||
Intent PaymentIntent `json:"intent"`
|
||||
PreviewOnly bool `json:"previewOnly"`
|
||||
}
|
||||
|
||||
func (r *QuotePayment) Validate() error {
|
||||
if err := validateQuoteIdempotency(r.PreviewOnly, r.IdempotencyKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// intent is mandatory, so validate always
|
||||
if err := r.Intent.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type QuotePayments struct {
|
||||
PaymentBase `json:",inline"`
|
||||
Intents []PaymentIntent `json:"intents"`
|
||||
PreviewOnly bool `json:"previewOnly"`
|
||||
}
|
||||
|
||||
func (r *QuotePayments) Validate() error {
|
||||
if err := validateQuoteIdempotency(r.PreviewOnly, r.IdempotencyKey); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(r.Intents) == 0 {
|
||||
return merrors.InvalidArgument("intents are required", "intents")
|
||||
}
|
||||
for i := range r.Intents {
|
||||
if err := r.Intents[i].Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateQuoteIdempotency(previewOnly bool, idempotencyKey string) error {
|
||||
key := strings.TrimSpace(idempotencyKey)
|
||||
if previewOnly {
|
||||
if key != "" {
|
||||
return merrors.InvalidArgument("previewOnly requests must not include idempotencyKey", "idempotencyKey")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if key == "" {
|
||||
return merrors.InvalidArgument("idempotencyKey is required", "idempotencyKey")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type InitiatePayment struct {
|
||||
PaymentBase `json:",inline"`
|
||||
Intent *PaymentIntent `json:"intent,omitempty"`
|
||||
QuoteRef string `json:"quoteRef,omitempty"`
|
||||
}
|
||||
|
||||
func (r InitiatePayment) Validate() error {
|
||||
// base checks
|
||||
if err := r.PaymentBase.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasIntent := r.Intent != nil
|
||||
hasQuote := r.QuoteRef != ""
|
||||
|
||||
// must be exactly one
|
||||
switch {
|
||||
case !hasIntent && !hasQuote:
|
||||
return merrors.NoData("either intent or quoteRef must be provided")
|
||||
case hasIntent && hasQuote:
|
||||
return merrors.DataConflict("intent and quoteRef are mutually exclusive")
|
||||
}
|
||||
|
||||
// if intent provided → validate it
|
||||
if hasIntent {
|
||||
if err := r.Intent.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type InitiatePayments struct {
|
||||
PaymentBase `json:",inline"`
|
||||
QuoteRef string `json:"quoteRef,omitempty"`
|
||||
}
|
||||
|
||||
func (r *InitiatePayments) Validate() error {
|
||||
if r == nil {
|
||||
return merrors.InvalidArgument("request is required")
|
||||
}
|
||||
if err := r.PaymentBase.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
r.QuoteRef = strings.TrimSpace(r.QuoteRef)
|
||||
|
||||
if r.QuoteRef == "" {
|
||||
return merrors.InvalidArgument("quoteRef is required", "quoteRef")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
60
api/edge/bff/interface/api/srequest/payment_enums.go
Normal file
60
api/edge/bff/interface/api/srequest/payment_enums.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package srequest
|
||||
|
||||
// PaymentKind mirrors the orchestrator payment kinds without importing generated proto types.
|
||||
// Strings keep JSON readable; conversion helpers map these to proto enums.
|
||||
type PaymentKind string
|
||||
|
||||
const (
|
||||
PaymentKindUnspecified PaymentKind = "unspecified"
|
||||
PaymentKindPayout PaymentKind = "payout"
|
||||
PaymentKindInternalTransfer PaymentKind = "internal_transfer"
|
||||
PaymentKindFxConversion PaymentKind = "fx_conversion"
|
||||
)
|
||||
|
||||
// SettlementMode matches orchestrator settlement behavior.
|
||||
type SettlementMode string
|
||||
|
||||
const (
|
||||
SettlementModeUnspecified SettlementMode = "unspecified"
|
||||
SettlementModeFixSource SettlementMode = "fix_source"
|
||||
SettlementModeFixReceived SettlementMode = "fix_received"
|
||||
)
|
||||
|
||||
// FeeTreatment controls where fee impact is applied by quotation.
|
||||
type FeeTreatment string
|
||||
|
||||
const (
|
||||
FeeTreatmentUnspecified FeeTreatment = "unspecified"
|
||||
FeeTreatmentAddToSource FeeTreatment = "add_to_source"
|
||||
FeeTreatmentDeductFromDestination FeeTreatment = "deduct_from_destination"
|
||||
)
|
||||
|
||||
// FXSide mirrors the common FX side enum.
|
||||
type FXSide string
|
||||
|
||||
const (
|
||||
FXSideUnspecified FXSide = "unspecified"
|
||||
FXSideBuyBaseSellQuote FXSide = "buy_base_sell_quote"
|
||||
FXSideSellBaseBuyQuote FXSide = "sell_base_buy_quote"
|
||||
)
|
||||
|
||||
// ChainNetwork mirrors the chain network enum used by managed wallets.
|
||||
type ChainNetwork string
|
||||
|
||||
const (
|
||||
ChainNetworkUnspecified ChainNetwork = "unspecified"
|
||||
ChainNetworkEthereumMainnet ChainNetwork = "ethereum_mainnet"
|
||||
ChainNetworkArbitrumOne ChainNetwork = "arbitrum_one"
|
||||
ChainNetworkTronMainnet ChainNetwork = "tron_mainnet"
|
||||
ChainNetworkTronNile ChainNetwork = "tron_nile"
|
||||
)
|
||||
|
||||
// InsufficientNetPolicy mirrors the fee engine policy override.
|
||||
type InsufficientNetPolicy string
|
||||
|
||||
const (
|
||||
InsufficientNetPolicyUnspecified InsufficientNetPolicy = "unspecified"
|
||||
InsufficientNetPolicyBlockPosting InsufficientNetPolicy = "block_posting"
|
||||
InsufficientNetPolicySweepOrgCash InsufficientNetPolicy = "sweep_org_cash"
|
||||
InsufficientNetPolicyInvoiceLater InsufficientNetPolicy = "invoice_later"
|
||||
)
|
||||
56
api/edge/bff/interface/api/srequest/payment_intent.go
Normal file
56
api/edge/bff/interface/api/srequest/payment_intent.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
type PaymentIntent struct {
|
||||
Kind PaymentKind `json:"kind,omitempty"`
|
||||
Source *Endpoint `json:"source,omitempty"`
|
||||
Destination *Endpoint `json:"destination,omitempty"`
|
||||
Amount *paymenttypes.Money `json:"amount,omitempty"`
|
||||
FX *FXIntent `json:"fx,omitempty"`
|
||||
SettlementMode SettlementMode `json:"settlement_mode,omitempty"`
|
||||
FeeTreatment FeeTreatment `json:"fee_treatment,omitempty"`
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
Customer *Customer `json:"customer,omitempty"`
|
||||
}
|
||||
|
||||
type AssetResolverStub struct{}
|
||||
|
||||
func (a *AssetResolverStub) IsSupported(_ string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *PaymentIntent) Validate() error {
|
||||
// Kind must be set (non-zero)
|
||||
var zeroKind PaymentKind
|
||||
if p.Kind == zeroKind {
|
||||
return merrors.InvalidArgument("kind is required", "intent.kind")
|
||||
}
|
||||
|
||||
if p.Source == nil {
|
||||
return merrors.InvalidArgument("source is required", "intent.source")
|
||||
}
|
||||
|
||||
if p.Destination == nil {
|
||||
return merrors.InvalidArgument("destination is required", "intent.destination")
|
||||
}
|
||||
|
||||
if p.Amount == nil {
|
||||
return merrors.InvalidArgument("amount is required", "intent.amount")
|
||||
}
|
||||
//TODO: collect supported currencies and validate against them
|
||||
if err := ValidateMoney(p.Amount, &AssetResolverStub{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.FX != nil {
|
||||
if err := p.FX.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
func TestPaymentIntentValidate_AcceptsBaseIntentWithoutFX(t *testing.T) {
|
||||
intent := mustValidBaseIntent(t)
|
||||
|
||||
if err := intent.Validate(); err != nil {
|
||||
t.Fatalf("unexpected validation error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentIntentValidate_RejectsFXWithoutPair(t *testing.T) {
|
||||
intent := mustValidBaseIntent(t)
|
||||
intent.FX = &FXIntent{
|
||||
Side: FXSideSellBaseBuyQuote,
|
||||
}
|
||||
|
||||
if err := intent.Validate(); err == nil {
|
||||
t.Fatalf("expected validation error for missing fx pair")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentIntentValidate_RejectsInvalidFXSide(t *testing.T) {
|
||||
intent := mustValidBaseIntent(t)
|
||||
intent.FX = &FXIntent{
|
||||
Pair: &CurrencyPair{
|
||||
Base: "USDT",
|
||||
Quote: "RUB",
|
||||
},
|
||||
Side: FXSide("wrong"),
|
||||
}
|
||||
|
||||
if err := intent.Validate(); err == nil {
|
||||
t.Fatalf("expected validation error for invalid fx side")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentIntentValidate_AcceptsValidFX(t *testing.T) {
|
||||
intent := mustValidBaseIntent(t)
|
||||
intent.FX = &FXIntent{
|
||||
Pair: &CurrencyPair{
|
||||
Base: "USDT",
|
||||
Quote: "RUB",
|
||||
},
|
||||
Side: FXSideSellBaseBuyQuote,
|
||||
}
|
||||
|
||||
if err := intent.Validate(); err != nil {
|
||||
t.Fatalf("unexpected validation error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustValidBaseIntent(t *testing.T) *PaymentIntent {
|
||||
t.Helper()
|
||||
|
||||
source, err := NewManagedWalletEndpointDTO(ManagedWalletEndpoint{ManagedWalletRef: "mw-src"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build source endpoint: %v", err)
|
||||
}
|
||||
destination, err := NewCardEndpointDTO(CardEndpoint{
|
||||
Pan: "2200700142860161",
|
||||
FirstName: "Jane",
|
||||
LastName: "Doe",
|
||||
ExpMonth: 2,
|
||||
ExpYear: 2030,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build destination endpoint: %v", err)
|
||||
}
|
||||
|
||||
return &PaymentIntent{
|
||||
Kind: PaymentKindPayout,
|
||||
Source: &source,
|
||||
Destination: &destination,
|
||||
Amount: &paymenttypes.Money{Amount: "10", Currency: "USDT"},
|
||||
SettlementMode: SettlementModeFixSource,
|
||||
FeeTreatment: FeeTreatmentAddToSource,
|
||||
}
|
||||
}
|
||||
427
api/edge/bff/interface/api/srequest/payment_types_test.go
Normal file
427
api/edge/bff/interface/api/srequest/payment_types_test.go
Normal file
@@ -0,0 +1,427 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
func TestEndpointDTOBuildersAndDecoders(t *testing.T) {
|
||||
meta := map[string]string{"note": "meta"}
|
||||
|
||||
t.Run("ledger", func(t *testing.T) {
|
||||
payload := LedgerEndpoint{LedgerAccountRef: "acc-1", ContraLedgerAccountRef: "contra-1"}
|
||||
endpoint, err := NewLedgerEndpointDTO(payload, meta)
|
||||
if err != nil {
|
||||
t.Fatalf("build ledger endpoint: %v", err)
|
||||
}
|
||||
if endpoint.Type != EndpointTypeLedger {
|
||||
t.Fatalf("expected type %s got %s", EndpointTypeLedger, endpoint.Type)
|
||||
}
|
||||
if string(endpoint.Data) != `{"ledger_account_ref":"acc-1","contra_ledger_account_ref":"contra-1"}` {
|
||||
t.Fatalf("unexpected data: %s", endpoint.Data)
|
||||
}
|
||||
decoded, err := endpoint.DecodeLedger()
|
||||
if err != nil {
|
||||
t.Fatalf("decode ledger: %v", err)
|
||||
}
|
||||
if decoded != payload {
|
||||
t.Fatalf("decoded payload mismatch: %#v vs %#v", decoded, payload)
|
||||
}
|
||||
meta["note"] = "changed"
|
||||
if endpoint.Metadata["note"] != "meta" {
|
||||
t.Fatalf("metadata should be copied, got %s", endpoint.Metadata["note"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("managed wallet", func(t *testing.T) {
|
||||
payload := ManagedWalletEndpoint{
|
||||
ManagedWalletRef: "mw-1",
|
||||
Asset: &Asset{
|
||||
Chain: ChainNetworkArbitrumOne,
|
||||
TokenSymbol: "USDC",
|
||||
ContractAddress: "0xabc",
|
||||
},
|
||||
}
|
||||
endpoint, err := NewManagedWalletEndpointDTO(payload, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build managed wallet endpoint: %v", err)
|
||||
}
|
||||
if endpoint.Type != EndpointTypeManagedWallet {
|
||||
t.Fatalf("expected type %s got %s", EndpointTypeManagedWallet, endpoint.Type)
|
||||
}
|
||||
decoded, err := endpoint.DecodeManagedWallet()
|
||||
if err != nil {
|
||||
t.Fatalf("decode managed wallet: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(decoded, payload) {
|
||||
t.Fatalf("decoded payload mismatch: %#v vs %#v", decoded, payload)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("external chain", func(t *testing.T) {
|
||||
payload := ExternalChainEndpoint{
|
||||
Asset: &Asset{
|
||||
Chain: ChainNetworkEthereumMainnet,
|
||||
TokenSymbol: "ETH",
|
||||
},
|
||||
Address: "0x123",
|
||||
Memo: "memo",
|
||||
}
|
||||
endpoint, err := NewExternalChainEndpointDTO(payload, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build external chain endpoint: %v", err)
|
||||
}
|
||||
if endpoint.Type != EndpointTypeExternalChain {
|
||||
t.Fatalf("expected type %s got %s", EndpointTypeExternalChain, endpoint.Type)
|
||||
}
|
||||
decoded, err := endpoint.DecodeExternalChain()
|
||||
if err != nil {
|
||||
t.Fatalf("decode external chain: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(decoded, payload) {
|
||||
t.Fatalf("decoded payload mismatch: %#v vs %#v", decoded, payload)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("card", func(t *testing.T) {
|
||||
payload := CardEndpoint{
|
||||
Pan: "pan",
|
||||
FirstName: "Jane",
|
||||
LastName: "Doe",
|
||||
ExpMonth: 12,
|
||||
ExpYear: 2030,
|
||||
Country: "US",
|
||||
}
|
||||
endpoint, err := NewCardEndpointDTO(payload, map[string]string{"k": "v"})
|
||||
if err != nil {
|
||||
t.Fatalf("build card endpoint: %v", err)
|
||||
}
|
||||
if endpoint.Type != EndpointTypeCard {
|
||||
t.Fatalf("expected type %s got %s", EndpointTypeCard, endpoint.Type)
|
||||
}
|
||||
decoded, err := endpoint.DecodeCard()
|
||||
if err != nil {
|
||||
t.Fatalf("decode card: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(decoded, payload) {
|
||||
t.Fatalf("decoded payload mismatch: %#v vs %#v", decoded, payload)
|
||||
}
|
||||
if endpoint.Metadata["k"] != "v" {
|
||||
t.Fatalf("expected metadata copy, got %s", endpoint.Metadata["k"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("card token", func(t *testing.T) {
|
||||
payload := CardTokenEndpoint{Token: "token", MaskedPan: "****1234"}
|
||||
endpoint, err := NewCardTokenEndpointDTO(payload, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build card token endpoint: %v", err)
|
||||
}
|
||||
if endpoint.Type != EndpointTypeCardToken {
|
||||
t.Fatalf("expected type %s got %s", EndpointTypeCardToken, endpoint.Type)
|
||||
}
|
||||
decoded, err := endpoint.DecodeCardToken()
|
||||
if err != nil {
|
||||
t.Fatalf("decode card token: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(decoded, payload) {
|
||||
t.Fatalf("decoded payload mismatch: %#v vs %#v", decoded, payload)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wallet", func(t *testing.T) {
|
||||
payload := WalletEndpoint{WalletID: "wallet-1"}
|
||||
endpoint, err := NewWalletEndpointDTO(payload, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build wallet endpoint: %v", err)
|
||||
}
|
||||
if endpoint.Type != EndpointTypeWallet {
|
||||
t.Fatalf("expected type %s got %s", EndpointTypeWallet, endpoint.Type)
|
||||
}
|
||||
decoded, err := endpoint.DecodeWallet()
|
||||
if err != nil {
|
||||
t.Fatalf("decode wallet: %v", err)
|
||||
}
|
||||
if decoded != payload {
|
||||
t.Fatalf("decoded payload mismatch: %#v vs %#v", decoded, payload)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bank account", func(t *testing.T) {
|
||||
payload := BankAccountEndpoint{
|
||||
RecipientName: "ACME",
|
||||
Inn: "inn",
|
||||
Kpp: "kpp",
|
||||
BankName: "bank",
|
||||
Bik: "bik",
|
||||
AccountNumber: "123",
|
||||
CorrespondentAccount: "456",
|
||||
}
|
||||
endpoint, err := NewBankAccountEndpointDTO(payload, map[string]string{"note": "n"})
|
||||
if err != nil {
|
||||
t.Fatalf("build bank account endpoint: %v", err)
|
||||
}
|
||||
if endpoint.Type != EndpointTypeBankAccount {
|
||||
t.Fatalf("expected type %s got %s", EndpointTypeBankAccount, endpoint.Type)
|
||||
}
|
||||
decoded, err := endpoint.DecodeBankAccount()
|
||||
if err != nil {
|
||||
t.Fatalf("decode bank account: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(decoded, payload) {
|
||||
t.Fatalf("decoded payload mismatch: %#v vs %#v", decoded, payload)
|
||||
}
|
||||
if endpoint.Metadata["note"] != "n" {
|
||||
t.Fatalf("expected metadata copy, got %s", endpoint.Metadata["note"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("iban", func(t *testing.T) {
|
||||
payload := IBANEndpoint{
|
||||
IBAN: "DE123",
|
||||
AccountHolder: "John Doe",
|
||||
BIC: "BICCODE",
|
||||
BankName: "BankName",
|
||||
}
|
||||
endpoint, err := NewIBANEndpointDTO(payload, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build iban endpoint: %v", err)
|
||||
}
|
||||
if endpoint.Type != EndpointTypeIBAN {
|
||||
t.Fatalf("expected type %s got %s", EndpointTypeIBAN, endpoint.Type)
|
||||
}
|
||||
decoded, err := endpoint.DecodeIBAN()
|
||||
if err != nil {
|
||||
t.Fatalf("decode iban: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(decoded, payload) {
|
||||
t.Fatalf("decoded payload mismatch: %#v vs %#v", decoded, payload)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("type mismatch", func(t *testing.T) {
|
||||
endpoint, err := NewLedgerEndpointDTO(LedgerEndpoint{LedgerAccountRef: "acc"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build ledger endpoint: %v", err)
|
||||
}
|
||||
if _, err := endpoint.DecodeCard(); err == nil || !strings.Contains(err.Error(), "expected endpoint type") {
|
||||
t.Fatalf("expected type mismatch error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid json data", func(t *testing.T) {
|
||||
endpoint := Endpoint{Type: EndpointTypeLedger, Data: json.RawMessage("not-json")}
|
||||
if _, err := endpoint.DecodeLedger(); err == nil {
|
||||
t.Fatalf("expected decode error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("legacy type alias normalizes", func(t *testing.T) {
|
||||
raw := []byte(`{"type":"managed_wallet","data":{"managed_wallet_ref":"mw-legacy"}}`)
|
||||
var endpoint Endpoint
|
||||
if err := json.Unmarshal(raw, &endpoint); err != nil {
|
||||
t.Fatalf("unmarshal with legacy type: %v", err)
|
||||
}
|
||||
if endpoint.Type != EndpointTypeManagedWallet {
|
||||
t.Fatalf("expected normalized type %s got %s", EndpointTypeManagedWallet, endpoint.Type)
|
||||
}
|
||||
payload, err := endpoint.DecodeManagedWallet()
|
||||
if err != nil {
|
||||
t.Fatalf("decode managed wallet with alias: %v", err)
|
||||
}
|
||||
if payload.ManagedWalletRef != "mw-legacy" {
|
||||
t.Fatalf("decoded payload mismatch from alias: %#v", payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentIntentJSONRoundTrip(t *testing.T) {
|
||||
sourcePayload := LedgerEndpoint{LedgerAccountRef: "source"}
|
||||
source, err := NewLedgerEndpointDTO(sourcePayload, map[string]string{"src": "meta"})
|
||||
if err != nil {
|
||||
t.Fatalf("build source endpoint: %v", err)
|
||||
}
|
||||
destPayload := ExternalChainEndpoint{Address: "0xabc", Asset: &Asset{Chain: ChainNetworkEthereumMainnet, TokenSymbol: "USDC"}}
|
||||
dest, err := NewExternalChainEndpointDTO(destPayload, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build destination endpoint: %v", err)
|
||||
}
|
||||
|
||||
intent := &PaymentIntent{
|
||||
Kind: PaymentKindPayout,
|
||||
Source: &source,
|
||||
Destination: &dest,
|
||||
Amount: &paymenttypes.Money{Amount: "10", Currency: "USD"},
|
||||
FX: &FXIntent{
|
||||
Pair: &CurrencyPair{Base: "USD", Quote: "EUR"},
|
||||
Side: FXSideBuyBaseSellQuote,
|
||||
Firm: true,
|
||||
TTLms: 5000,
|
||||
PreferredProvider: "provider",
|
||||
MaxAgeMs: 10,
|
||||
},
|
||||
SettlementMode: SettlementModeFixReceived,
|
||||
Attributes: map[string]string{"k": "v"},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(intent)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal intent: %v", err)
|
||||
}
|
||||
var decoded PaymentIntent
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal intent: %v", err)
|
||||
}
|
||||
|
||||
if decoded.Kind != intent.Kind || decoded.SettlementMode != intent.SettlementMode {
|
||||
t.Fatalf("scalar fields changed after round trip")
|
||||
}
|
||||
if decoded.Amount == nil || *decoded.Amount != *intent.Amount {
|
||||
t.Fatalf("amount mismatch after round trip")
|
||||
}
|
||||
if decoded.FX == nil || decoded.FX.PreferredProvider != intent.FX.PreferredProvider {
|
||||
t.Fatalf("fx mismatch after round trip")
|
||||
}
|
||||
if decoded.Source == nil || decoded.Destination == nil {
|
||||
t.Fatalf("source/destination missing after round trip")
|
||||
}
|
||||
sourceDecoded, err := decoded.Source.DecodeLedger()
|
||||
if err != nil {
|
||||
t.Fatalf("decode source after round trip: %v", err)
|
||||
}
|
||||
if sourceDecoded != sourcePayload {
|
||||
t.Fatalf("source payload mismatch after round trip: %#v vs %#v", sourceDecoded, sourcePayload)
|
||||
}
|
||||
destDecoded, err := decoded.Destination.DecodeExternalChain()
|
||||
if err != nil {
|
||||
t.Fatalf("decode destination after round trip: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(destDecoded, destPayload) {
|
||||
t.Fatalf("destination payload mismatch after round trip: %#v vs %#v", destDecoded, destPayload)
|
||||
}
|
||||
if decoded.Attributes["k"] != "v" {
|
||||
t.Fatalf("attributes mismatch after round trip")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentIntentMinimalRoundTrip(t *testing.T) {
|
||||
sourcePayload := ManagedWalletEndpoint{ManagedWalletRef: "mw"}
|
||||
source, err := NewManagedWalletEndpointDTO(sourcePayload, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build source endpoint: %v", err)
|
||||
}
|
||||
destPayload := LedgerEndpoint{LedgerAccountRef: "dest-ledger"}
|
||||
dest, err := NewLedgerEndpointDTO(destPayload, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build destination endpoint: %v", err)
|
||||
}
|
||||
|
||||
intent := &PaymentIntent{
|
||||
Kind: PaymentKindInternalTransfer,
|
||||
Source: &source,
|
||||
Destination: &dest,
|
||||
Amount: &paymenttypes.Money{Amount: "1", Currency: "USD"},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(intent)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal intent: %v", err)
|
||||
}
|
||||
var decoded PaymentIntent
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal intent: %v", err)
|
||||
}
|
||||
|
||||
if decoded.Kind != intent.Kind || decoded.FX != nil {
|
||||
t.Fatalf("unexpected fx data in minimal intent: %#v", decoded)
|
||||
}
|
||||
if decoded.Amount == nil || *decoded.Amount != *intent.Amount {
|
||||
t.Fatalf("amount mismatch after round trip")
|
||||
}
|
||||
if decoded.Source == nil || decoded.Destination == nil {
|
||||
t.Fatalf("endpoints missing after round trip")
|
||||
}
|
||||
sourceDecoded, err := decoded.Source.DecodeManagedWallet()
|
||||
if err != nil {
|
||||
t.Fatalf("decode source: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(sourceDecoded, sourcePayload) {
|
||||
t.Fatalf("source payload mismatch: %#v vs %#v", sourceDecoded, sourcePayload)
|
||||
}
|
||||
destDecoded, err := decoded.Destination.DecodeLedger()
|
||||
if err != nil {
|
||||
t.Fatalf("decode destination: %v", err)
|
||||
}
|
||||
if destDecoded != destPayload {
|
||||
t.Fatalf("destination payload mismatch: %#v vs %#v", destDecoded, destPayload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyEndpointRoundTrip(t *testing.T) {
|
||||
legacy := &LegacyPaymentEndpoint{
|
||||
ExternalChain: &ExternalChainEndpoint{
|
||||
Asset: &Asset{Chain: ChainNetworkEthereumMainnet, TokenSymbol: "DAI", ContractAddress: "0xdef"},
|
||||
Address: "0x123",
|
||||
Memo: "memo",
|
||||
},
|
||||
Metadata: map[string]string{"note": "legacy"},
|
||||
}
|
||||
|
||||
endpoint, err := LegacyPaymentEndpointToEndpointDTO(legacy)
|
||||
if err != nil {
|
||||
t.Fatalf("convert legacy to dto: %v", err)
|
||||
}
|
||||
if endpoint == nil || endpoint.Type != EndpointTypeExternalChain {
|
||||
t.Fatalf("unexpected endpoint result: %#v", endpoint)
|
||||
}
|
||||
legacy.Metadata["note"] = "changed"
|
||||
if endpoint.Metadata["note"] != "legacy" {
|
||||
t.Fatalf("metadata should be copied from legacy")
|
||||
}
|
||||
|
||||
roundTrip, err := EndpointDTOToLegacyPaymentEndpoint(endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("convert dto back to legacy: %v", err)
|
||||
}
|
||||
if roundTrip == nil || roundTrip.ExternalChain == nil {
|
||||
t.Fatalf("round trip legacy missing payload: %#v", roundTrip)
|
||||
}
|
||||
if !reflect.DeepEqual(roundTrip.ExternalChain, legacy.ExternalChain) {
|
||||
t.Fatalf("round trip payload mismatch: %#v vs %#v", roundTrip.ExternalChain, legacy.ExternalChain)
|
||||
}
|
||||
if roundTrip.Metadata["note"] != "legacy" {
|
||||
t.Fatalf("metadata mismatch after round trip: %v", roundTrip.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyEndpointConversionRejectsMultiple(t *testing.T) {
|
||||
_, err := LegacyPaymentEndpointToEndpointDTO(&LegacyPaymentEndpoint{
|
||||
Ledger: &LedgerEndpoint{LedgerAccountRef: "a"},
|
||||
Card: &CardEndpoint{Pan: "t"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error when multiple legacy endpoints are set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpointUnmarshalLegacyShape(t *testing.T) {
|
||||
raw := []byte(`{"ledger":{"ledger_account_ref":"abc"}}`)
|
||||
var endpoint Endpoint
|
||||
if err := json.Unmarshal(raw, &endpoint); err != nil {
|
||||
t.Fatalf("unmarshal legacy shape: %v", err)
|
||||
}
|
||||
if endpoint.Type != EndpointTypeLedger {
|
||||
t.Fatalf("expected type %s got %s", EndpointTypeLedger, endpoint.Type)
|
||||
}
|
||||
payload, err := endpoint.DecodeLedger()
|
||||
if err != nil {
|
||||
t.Fatalf("decode ledger from legacy shape: %v", err)
|
||||
}
|
||||
if payload.LedgerAccountRef != "abc" {
|
||||
t.Fatalf("unexpected payload from legacy shape: %#v", payload)
|
||||
}
|
||||
}
|
||||
53
api/edge/bff/interface/api/srequest/payment_validate_test.go
Normal file
53
api/edge/bff/interface/api/srequest/payment_validate_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package srequest
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateQuoteIdempotency(t *testing.T) {
|
||||
t.Run("non-preview requires idempotency key", func(t *testing.T) {
|
||||
if err := validateQuoteIdempotency(false, ""); err == nil {
|
||||
t.Fatalf("expected error for empty idempotency key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preview rejects idempotency key", func(t *testing.T) {
|
||||
if err := validateQuoteIdempotency(true, "idem-1"); err == nil {
|
||||
t.Fatalf("expected error when preview request has idempotency key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preview accepts empty idempotency key", func(t *testing.T) {
|
||||
if err := validateQuoteIdempotency(true, ""); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-preview accepts idempotency key", func(t *testing.T) {
|
||||
if err := validateQuoteIdempotency(false, "idem-1"); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInitiatePaymentsValidate(t *testing.T) {
|
||||
t.Run("accepts quoteRef", func(t *testing.T) {
|
||||
req := &InitiatePayments{
|
||||
PaymentBase: PaymentBase{IdempotencyKey: "idem-1"},
|
||||
QuoteRef: " quote-1 ",
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if got, want := req.QuoteRef, "quote-1"; got != want {
|
||||
t.Fatalf("quoteRef mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects missing quoteRef", func(t *testing.T) {
|
||||
req := &InitiatePayments{
|
||||
PaymentBase: PaymentBase{IdempotencyKey: "idem-1"},
|
||||
}
|
||||
if err := req.Validate(); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
}
|
||||
133
api/edge/bff/interface/api/srequest/payment_value_objects.go
Normal file
133
api/edge/bff/interface/api/srequest/payment_value_objects.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
// AssetResolver defines environment-specific supported assets.
|
||||
// Implementations should check:
|
||||
// - fiat assets (ISO-4217)
|
||||
// - crypto assets supported by gateways / FX providers
|
||||
type AssetResolver interface {
|
||||
IsSupported(ticker string) bool
|
||||
}
|
||||
|
||||
// Precompile regex for efficiency.
|
||||
var currencySyntax = regexp.MustCompile(`^[A-Z0-9]{2,10}$`)
|
||||
|
||||
// ValidateCurrency validates currency syntax and checks dictionary via assetResolver.
|
||||
func ValidateCurrency(cur string, assetResolver AssetResolver) error {
|
||||
// Basic presence
|
||||
if strings.TrimSpace(cur) == "" {
|
||||
return merrors.InvalidArgument("currency is required", "intent.currency")
|
||||
}
|
||||
|
||||
// Normalize
|
||||
cur = strings.ToUpper(strings.TrimSpace(cur))
|
||||
|
||||
// Syntax check
|
||||
if !currencySyntax.MatchString(cur) {
|
||||
return merrors.InvalidArgument(
|
||||
"invalid currency format (must be A–Z0–9, length 2–10)",
|
||||
"intent.currency",
|
||||
)
|
||||
}
|
||||
|
||||
// Dictionary validation
|
||||
if assetResolver == nil {
|
||||
return merrors.InvalidArgument("asset resolver is not configured", "intent.currency")
|
||||
}
|
||||
|
||||
if !assetResolver.IsSupported(cur) {
|
||||
return merrors.InvalidArgument("unsupported currency/asset", "intent.currency")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateMoney(m *paymenttypes.Money, assetResolver AssetResolver) error {
|
||||
if m == nil {
|
||||
return merrors.InvalidArgument("money is required", "intent.amount")
|
||||
}
|
||||
|
||||
// 1) Basic presence
|
||||
if strings.TrimSpace(m.Amount) == "" {
|
||||
return merrors.InvalidArgument("amount is required", "intent.amount")
|
||||
}
|
||||
|
||||
// 2) Validate decimal amount
|
||||
amount, err := decimal.NewFromString(m.Amount)
|
||||
if err != nil {
|
||||
return merrors.InvalidArgument("invalid decimal amount", "intent.amount")
|
||||
}
|
||||
if amount.IsNegative() {
|
||||
return merrors.InvalidArgument("amount must be >= 0", "intent.amount")
|
||||
}
|
||||
|
||||
// 3) Validate currency via helper
|
||||
if err := ValidateCurrency(m.Currency, assetResolver); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type CurrencyPair struct {
|
||||
Base string `json:"base"`
|
||||
Quote string `json:"quote"`
|
||||
}
|
||||
|
||||
func (p *CurrencyPair) Validate() error {
|
||||
if p == nil {
|
||||
return merrors.InvalidArgument("currency pair is required", "currncy_pair")
|
||||
}
|
||||
if err := ValidateCurrency(p.Base, &AssetResolverStub{}); err != nil {
|
||||
return merrors.InvalidArgument("invalid base currency in pair: "+err.Error(), "currency_pair.base")
|
||||
}
|
||||
if err := ValidateCurrency(p.Quote, &AssetResolverStub{}); err != nil {
|
||||
return merrors.InvalidArgument("invalid quote currency in pair: "+err.Error(), "currency_pair.quote")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FXIntent struct {
|
||||
Pair *CurrencyPair `json:"pair,omitempty"`
|
||||
Side FXSide `json:"side,omitempty"`
|
||||
Firm bool `json:"firm,omitempty"`
|
||||
TTLms int64 `json:"ttl_ms,omitempty"`
|
||||
PreferredProvider string `json:"preferred_provider,omitempty"`
|
||||
MaxAgeMs int32 `json:"max_age_ms,omitempty"`
|
||||
}
|
||||
|
||||
func (fx *FXIntent) Validate() error {
|
||||
if fx.Pair == nil {
|
||||
return merrors.InvalidArgument("fx pair is required", "intent.fx.pair")
|
||||
}
|
||||
if err := fx.Pair.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(string(fx.Side)) {
|
||||
case string(FXSideBuyBaseSellQuote), string(FXSideSellBaseBuyQuote):
|
||||
default:
|
||||
return merrors.InvalidArgument("fx side is invalid", "intent.fx.side")
|
||||
}
|
||||
|
||||
if fx.TTLms < 0 {
|
||||
return merrors.InvalidArgument("fx ttl_ms cannot be negative", "intent.fx.ttl_ms")
|
||||
}
|
||||
if fx.TTLms == 0 && fx.Firm {
|
||||
return merrors.InvalidArgument("firm quote requires positive ttl_ms", "intent.fx.ttl_ms")
|
||||
}
|
||||
|
||||
if fx.MaxAgeMs < 0 {
|
||||
return merrors.InvalidArgument("fx max_age_ms cannot be negative", "intent.fx.max_age_ms")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
5
api/edge/bff/interface/api/srequest/refresh.go
Normal file
5
api/edge/bff/interface/api/srequest/refresh.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package srequest
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
type AccessTokenRefresh = model.ClientRefreshToken
|
||||
19
api/edge/bff/interface/api/srequest/reorder.go
Normal file
19
api/edge/bff/interface/api/srequest/reorder.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package srequest
|
||||
|
||||
import "go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
type Reorder struct {
|
||||
ParentRef bson.ObjectID `json:"parentRef"`
|
||||
From int `json:"from"`
|
||||
To int `json:"to"`
|
||||
}
|
||||
|
||||
type ReorderX struct {
|
||||
ObjectRef bson.ObjectID `json:"objectRef"`
|
||||
To int `json:"to"`
|
||||
}
|
||||
|
||||
type ReorderXDefault struct {
|
||||
ReorderX `json:",inline"`
|
||||
ParentRef bson.ObjectID `json:"parentRef"`
|
||||
}
|
||||
5
api/edge/bff/interface/api/srequest/rotate.go
Normal file
5
api/edge/bff/interface/api/srequest/rotate.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package srequest
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
type TokenRefreshRotate = model.ClientRefreshToken
|
||||
13
api/edge/bff/interface/api/srequest/sgchange.go
Normal file
13
api/edge/bff/interface/api/srequest/sgchange.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package srequest
|
||||
|
||||
import "go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
type GroupItemChange struct {
|
||||
GroupRef bson.ObjectID `json:"groupRef"`
|
||||
ItemRef bson.ObjectID `json:"itemRef"`
|
||||
}
|
||||
|
||||
type RemoveItemFromGroup struct {
|
||||
GroupItemChange `json:",inline"`
|
||||
TargetItemRef bson.ObjectID `json:"targetItemRef"`
|
||||
}
|
||||
31
api/edge/bff/interface/api/srequest/signup.go
Normal file
31
api/edge/bff/interface/api/srequest/signup.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type Signup struct {
|
||||
Account model.AccountData `json:"account"`
|
||||
Organization model.Describable `json:"organization"`
|
||||
OrganizationTimeZone string `json:"organizationTimeZone"`
|
||||
OwnerRole model.Describable `json:"ownerRole"`
|
||||
CryptoWallet model.Describable `json:"cryptoWallet"`
|
||||
LedgerWallet model.Describable `json:"ledgerWallet"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON enforces strict parsing to catch malformed or unexpected fields.
|
||||
func (s *Signup) UnmarshalJSON(data []byte) error {
|
||||
type alias Signup
|
||||
var payload alias
|
||||
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&payload); err != nil {
|
||||
return err
|
||||
}
|
||||
*s = Signup(payload)
|
||||
return nil
|
||||
}
|
||||
150
api/edge/bff/interface/api/srequest/signup_test.go
Normal file
150
api/edge/bff/interface/api/srequest/signup_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package srequest_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
)
|
||||
|
||||
func TestSignupRequest_JSONSerialization(t *testing.T) {
|
||||
signup := srequest.Signup{
|
||||
Account: model.AccountData{
|
||||
LoginData: model.LoginData{
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
},
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
Organization: model.Describable{
|
||||
Name: "Test Organization",
|
||||
},
|
||||
OrganizationTimeZone: "UTC",
|
||||
OwnerRole: model.Describable{
|
||||
Name: "Owner",
|
||||
},
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(signup)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, jsonData)
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled srequest.Signup
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify all fields are properly serialized/deserialized
|
||||
assert.Equal(t, signup.Account.Name, unmarshaled.Account.Name)
|
||||
assert.Equal(t, signup.Account.Login, unmarshaled.Account.Login)
|
||||
assert.Equal(t, signup.Account.Password, unmarshaled.Account.Password)
|
||||
assert.Equal(t, signup.Organization.Name, unmarshaled.Organization.Name)
|
||||
assert.Equal(t, signup.OrganizationTimeZone, unmarshaled.OrganizationTimeZone)
|
||||
assert.Equal(t, signup.OwnerRole.Name, unmarshaled.OwnerRole.Name)
|
||||
}
|
||||
|
||||
func TestSignupRequest_MinimalValidRequest(t *testing.T) {
|
||||
signup := srequest.Signup{
|
||||
Account: model.AccountData{
|
||||
LoginData: model.LoginData{
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
},
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
Organization: model.Describable{
|
||||
Name: "Test Organization",
|
||||
},
|
||||
OrganizationTimeZone: "UTC",
|
||||
OwnerRole: model.Describable{
|
||||
Name: "Owner",
|
||||
},
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(signup)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, jsonData)
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled srequest.Signup
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify minimal request is valid
|
||||
assert.Equal(t, signup.Account.Name, unmarshaled.Account.Name)
|
||||
assert.Equal(t, signup.Account.Login, unmarshaled.Account.Login)
|
||||
assert.Equal(t, signup.Organization.Name, unmarshaled.Organization.Name)
|
||||
}
|
||||
|
||||
func TestSignupRequest_InvalidJSON(t *testing.T) {
|
||||
invalidJSONs := []string{
|
||||
`{"account": invalid}`,
|
||||
`{"organization": 123}`,
|
||||
`{"organizationTimeZone": true}`,
|
||||
`{"defaultPriorityGroup": "not_an_object"}`,
|
||||
`{"anonymousUser": []}`,
|
||||
`{"anonymousRole": 456}`,
|
||||
`{invalid json}`,
|
||||
}
|
||||
|
||||
for i, invalidJSON := range invalidJSONs {
|
||||
t.Run(fmt.Sprintf("Invalid JSON %d", i), func(t *testing.T) {
|
||||
var signup srequest.Signup
|
||||
err := json.Unmarshal([]byte(invalidJSON), &signup)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignupRequest_UnicodeCharacters(t *testing.T) {
|
||||
signup := srequest.Signup{
|
||||
Account: model.AccountData{
|
||||
LoginData: model.LoginData{
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "测试@example.com",
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
},
|
||||
Describable: model.Describable{
|
||||
Name: "Test 用户 Üser",
|
||||
},
|
||||
},
|
||||
Organization: model.Describable{
|
||||
Name: "测试 Organization",
|
||||
},
|
||||
OrganizationTimeZone: "UTC",
|
||||
OwnerRole: model.Describable{
|
||||
Name: "所有者",
|
||||
},
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(signup)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, jsonData)
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled srequest.Signup
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify unicode characters are properly handled
|
||||
assert.Equal(t, "测试@example.com", unmarshaled.Account.Login)
|
||||
assert.Equal(t, "Test 用户 Üser", unmarshaled.Account.Name)
|
||||
assert.Equal(t, "测试 Organization", unmarshaled.Organization.Name)
|
||||
assert.Equal(t, "所有者", unmarshaled.OwnerRole.Name)
|
||||
}
|
||||
20
api/edge/bff/interface/api/srequest/taggable.go
Normal file
20
api/edge/bff/interface/api/srequest/taggable.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package srequest
|
||||
|
||||
import "go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
// TaggableSingle is used for single tag operations (add/remove tag)
|
||||
type TaggableSingle struct {
|
||||
ObjectRef bson.ObjectID `json:"objectRef"`
|
||||
TagRef bson.ObjectID `json:"tagRef"`
|
||||
}
|
||||
|
||||
// TaggableMultiple is used for multiple tag operations (add tags, set tags)
|
||||
type TaggableMultiple struct {
|
||||
ObjectRef bson.ObjectID `json:"objectRef"`
|
||||
TagRefs []bson.ObjectID `json:"tagRefs"`
|
||||
}
|
||||
|
||||
// TaggableObject is used for object-only operations (remove all tags, get tags)
|
||||
type TaggableObject struct {
|
||||
ObjectRef bson.ObjectID `json:"objectRef"`
|
||||
}
|
||||
5
api/edge/bff/interface/api/srequest/validateable.go
Normal file
5
api/edge/bff/interface/api/srequest/validateable.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package srequest
|
||||
|
||||
type Validatable interface {
|
||||
Validate() error
|
||||
}
|
||||
12
api/edge/bff/interface/api/srequest/wallet.go
Normal file
12
api/edge/bff/interface/api/srequest/wallet.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type CreateWallet struct {
|
||||
Description model.Describable `json:"description"`
|
||||
Asset model.ChainAssetKey `json:"asset"`
|
||||
OwnerRef *bson.ObjectID `json:"ownerRef,omitempty"`
|
||||
}
|
||||
62
api/edge/bff/interface/api/sresponse/account.go
Normal file
62
api/edge/bff/interface/api/sresponse/account.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type accountData struct {
|
||||
model.AccountPublic `json:",inline"`
|
||||
IsAnonymous bool `json:"isAnonymous"`
|
||||
}
|
||||
|
||||
type accountResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Account accountData `json:"account"`
|
||||
}
|
||||
|
||||
func _createAccount(account *model.Account, isAnonymous bool) *accountData {
|
||||
return &accountData{
|
||||
AccountPublic: account.AccountPublic,
|
||||
IsAnonymous: isAnonymous,
|
||||
}
|
||||
}
|
||||
|
||||
func _toAccount(account *model.Account, orgRef bson.ObjectID) *accountData {
|
||||
return _createAccount(account, model.AccountIsAnonymous(&account.UserDataBase, orgRef))
|
||||
}
|
||||
|
||||
func Account(logger mlogger.Logger, account *model.Account, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(
|
||||
logger,
|
||||
&accountResponse{
|
||||
Account: *_createAccount(account, false),
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
type accountsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Accounts []accountData `json:"accounts"`
|
||||
}
|
||||
|
||||
func Accounts(logger mlogger.Logger, accounts []model.Account, orgRef bson.ObjectID, accessToken *TokenData) http.HandlerFunc {
|
||||
// Convert each account to its public representation.
|
||||
publicAccounts := make([]accountData, len(accounts))
|
||||
for i, a := range accounts {
|
||||
publicAccounts[i] = *_toAccount(&a, orgRef)
|
||||
}
|
||||
|
||||
return response.Ok(
|
||||
logger,
|
||||
&accountsResponse{
|
||||
Accounts: publicAccounts,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
},
|
||||
)
|
||||
}
|
||||
5
api/edge/bff/interface/api/sresponse/authresp.go
Normal file
5
api/edge/bff/interface/api/sresponse/authresp.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package sresponse
|
||||
|
||||
type authResponse struct {
|
||||
AccessToken TokenData `json:"accessToken"`
|
||||
}
|
||||
15
api/edge/bff/interface/api/sresponse/badpassword.go
Normal file
15
api/edge/bff/interface/api/sresponse/badpassword.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func BadRPassword(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc {
|
||||
logger.Info("Failed password validation check", zap.Error(err))
|
||||
return response.BadRequest(logger, source, "invalid_request", err.Error())
|
||||
}
|
||||
24
api/edge/bff/interface/api/sresponse/dzone.go
Normal file
24
api/edge/bff/interface/api/sresponse/dzone.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type dzoneResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
DZone model.DZone `json:"dzone"`
|
||||
}
|
||||
|
||||
func DZone(logger mlogger.Logger, dzone *model.DZone, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(
|
||||
logger,
|
||||
&dzoneResponse{
|
||||
DZone: *dzone,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
},
|
||||
)
|
||||
}
|
||||
16
api/edge/bff/interface/api/sresponse/file.go
Normal file
16
api/edge/bff/interface/api/sresponse/file.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
)
|
||||
|
||||
type fileUpladed struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func FileUploaded(logger mlogger.Logger, url string) http.HandlerFunc {
|
||||
return response.Ok(logger, &fileUpladed{URL: url})
|
||||
}
|
||||
21
api/edge/bff/interface/api/sresponse/invitation.go
Normal file
21
api/edge/bff/interface/api/sresponse/invitation.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type invitationResp struct {
|
||||
Invitation model.PublicInvitation `json:"invitation"`
|
||||
}
|
||||
|
||||
func Invitation(logger mlogger.Logger, invitation *model.PublicInvitation) http.HandlerFunc {
|
||||
return response.Ok(logger, &invitationResp{Invitation: *invitation})
|
||||
}
|
||||
|
||||
func Invitations(logger mlogger.Logger, invitations []model.Invitation) http.HandlerFunc {
|
||||
return response.Ok(logger, invitations)
|
||||
}
|
||||
126
api/edge/bff/interface/api/sresponse/ledger.go
Normal file
126
api/edge/bff/interface/api/sresponse/ledger.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
)
|
||||
|
||||
type ledgerAccount struct {
|
||||
model.Describable `bson:",inline" json:",inline"`
|
||||
LedgerAccountRef string `json:"ledgerAccountRef"`
|
||||
OrganizationRef string `json:"organizationRef"`
|
||||
OwnerRef string `json:"ownerRef,omitempty"`
|
||||
AccountCode string `json:"accountCode"`
|
||||
AccountType string `json:"accountType"`
|
||||
Currency string `json:"currency"`
|
||||
Status string `json:"status"`
|
||||
AllowNegative bool `json:"allowNegative"`
|
||||
Role string `json:"role"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||
}
|
||||
|
||||
type ledgerAccountsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Accounts []ledgerAccount `json:"accounts"`
|
||||
}
|
||||
|
||||
type ledgerAccountResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Account ledgerAccount `json:"account"`
|
||||
}
|
||||
|
||||
type ledgerMoney struct {
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
type ledgerBalance struct {
|
||||
LedgerAccountRef string `json:"ledgerAccountRef"`
|
||||
Balance *ledgerMoney `json:"balance,omitempty"`
|
||||
Version int64 `json:"version"`
|
||||
LastUpdated time.Time `json:"lastUpdated,omitempty"`
|
||||
}
|
||||
|
||||
type ledgerBalanceResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Balance ledgerBalance `json:"balance"`
|
||||
}
|
||||
|
||||
func LedgerAccounts(logger mlogger.Logger, accounts []*ledgerv1.LedgerAccount, accessToken *TokenData) http.HandlerFunc {
|
||||
dto := make([]ledgerAccount, 0, len(accounts))
|
||||
for _, acc := range accounts {
|
||||
dto = append(dto, toLedgerAccount(acc))
|
||||
}
|
||||
return response.Ok(logger, ledgerAccountsResponse{
|
||||
Accounts: dto,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
func LedgerAccountCreated(logger mlogger.Logger, account *ledgerv1.LedgerAccount, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Created(logger, ledgerAccountResponse{
|
||||
Account: toLedgerAccount(account),
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
func LedgerBalance(logger mlogger.Logger, resp *ledgerv1.BalanceResponse, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, ledgerBalanceResponse{
|
||||
Balance: toLedgerBalance(resp),
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
func toLedgerAccount(acc *ledgerv1.LedgerAccount) ledgerAccount {
|
||||
if acc == nil {
|
||||
return ledgerAccount{}
|
||||
}
|
||||
return ledgerAccount{
|
||||
Describable: model.Describable{
|
||||
Name: acc.GetDescribable().GetName(),
|
||||
Description: acc.GetDescribable().Description,
|
||||
},
|
||||
LedgerAccountRef: acc.GetLedgerAccountRef(),
|
||||
OrganizationRef: acc.GetOrganizationRef(),
|
||||
OwnerRef: acc.GetOwnerRef(),
|
||||
AccountCode: acc.GetAccountCode(),
|
||||
AccountType: acc.GetAccountType().String(),
|
||||
Currency: acc.GetCurrency(),
|
||||
Status: acc.GetStatus().String(),
|
||||
AllowNegative: acc.GetAllowNegative(),
|
||||
Role: acc.GetRole().String(),
|
||||
Metadata: acc.GetMetadata(),
|
||||
CreatedAt: acc.GetCreatedAt().AsTime(),
|
||||
UpdatedAt: acc.GetUpdatedAt().AsTime(),
|
||||
}
|
||||
}
|
||||
|
||||
func toLedgerBalance(resp *ledgerv1.BalanceResponse) ledgerBalance {
|
||||
if resp == nil {
|
||||
return ledgerBalance{}
|
||||
}
|
||||
return ledgerBalance{
|
||||
LedgerAccountRef: resp.GetLedgerAccountRef(),
|
||||
Balance: toLedgerMoney(resp.GetBalance()),
|
||||
Version: resp.GetVersion(),
|
||||
LastUpdated: resp.GetLastUpdated().AsTime(),
|
||||
}
|
||||
}
|
||||
|
||||
func toLedgerMoney(m *moneyv1.Money) *ledgerMoney {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return &ledgerMoney{
|
||||
Amount: m.GetAmount(),
|
||||
Currency: m.GetCurrency(),
|
||||
}
|
||||
}
|
||||
27
api/edge/bff/interface/api/sresponse/login.go
Normal file
27
api/edge/bff/interface/api/sresponse/login.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type loginResponse struct {
|
||||
accountResponse
|
||||
RefreshToken TokenData `json:"refreshToken"`
|
||||
}
|
||||
|
||||
func Login(logger mlogger.Logger, account *model.Account, accessToken, refreshToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(
|
||||
logger,
|
||||
&loginResponse{
|
||||
accountResponse: accountResponse{
|
||||
Account: *_createAccount(account, false),
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
},
|
||||
RefreshToken: *refreshToken,
|
||||
},
|
||||
)
|
||||
}
|
||||
29
api/edge/bff/interface/api/sresponse/login_pending.go
Normal file
29
api/edge/bff/interface/api/sresponse/login_pending.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type pendingLoginResponse struct {
|
||||
Account accountResponse `json:"account"`
|
||||
PendingToken TokenData `json:"pendingToken"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
func LoginPending(logger mlogger.Logger, account *model.Account, pendingToken *TokenData, target string) http.HandlerFunc {
|
||||
return response.Accepted(
|
||||
logger,
|
||||
&pendingLoginResponse{
|
||||
Account: accountResponse{
|
||||
Account: *_createAccount(account, false),
|
||||
authResponse: authResponse{},
|
||||
},
|
||||
PendingToken: *pendingToken,
|
||||
Target: target,
|
||||
},
|
||||
)
|
||||
}
|
||||
16
api/edge/bff/interface/api/sresponse/money.go
Normal file
16
api/edge/bff/interface/api/sresponse/money.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
)
|
||||
|
||||
func toMoney(m *moneyv1.Money) *paymenttypes.Money {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: m.GetAmount(),
|
||||
Currency: m.GetCurrency(),
|
||||
}
|
||||
}
|
||||
49
api/edge/bff/interface/api/sresponse/objects.go
Normal file
49
api/edge/bff/interface/api/sresponse/objects.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
)
|
||||
|
||||
type DynamicResponse[T any] struct {
|
||||
authResponse `json:",inline"`
|
||||
Items []T
|
||||
// FieldName is the JSON key to use for the items.
|
||||
FieldName string
|
||||
}
|
||||
|
||||
func (dr DynamicResponse[T]) MarshalJSON() ([]byte, error) {
|
||||
// Create a temporary map to hold the keys and values.
|
||||
m := map[string]any{
|
||||
dr.FieldName: dr.Items,
|
||||
"accessToken": dr.AccessToken,
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
type handler = func(logger mlogger.Logger, data any) http.HandlerFunc
|
||||
|
||||
func objectsAuth[T any](logger mlogger.Logger, items []T, accessToken *TokenData, resource mservice.Type, handler handler) http.HandlerFunc {
|
||||
resp := &DynamicResponse[T]{
|
||||
Items: items,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
FieldName: resource,
|
||||
}
|
||||
return handler(logger, resp)
|
||||
}
|
||||
|
||||
func ObjectsAuth[T any](logger mlogger.Logger, items []T, accessToken *TokenData, resource mservice.Type) http.HandlerFunc {
|
||||
return objectsAuth(logger, items, accessToken, resource, response.Ok)
|
||||
}
|
||||
|
||||
func ObjectAuth[T any](logger mlogger.Logger, item *T, accessToken *TokenData, resource mservice.Type) http.HandlerFunc {
|
||||
return ObjectsAuth(logger, []T{*item}, accessToken, resource)
|
||||
}
|
||||
|
||||
func ObjectAuthCreated[T any](logger mlogger.Logger, item *T, accessToken *TokenData, resource mservice.Type) http.HandlerFunc {
|
||||
return objectsAuth(logger, []T{*item}, accessToken, resource, response.Created)
|
||||
}
|
||||
35
api/edge/bff/interface/api/sresponse/orgnization.go
Normal file
35
api/edge/bff/interface/api/sresponse/orgnization.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type organizationsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Organizations []model.Organization `json:"organizations"`
|
||||
}
|
||||
|
||||
func Organization(logger mlogger.Logger, organization *model.Organization, accessToken *TokenData) http.HandlerFunc {
|
||||
return Organizations(logger, []model.Organization{*organization}, accessToken)
|
||||
}
|
||||
|
||||
func Organizations(logger mlogger.Logger, organizations []model.Organization, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, organizationsResponse{
|
||||
Organizations: organizations,
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
type organizationPublicResponse struct {
|
||||
Organizations []model.OrganizationBase `json:"organizations"`
|
||||
}
|
||||
|
||||
func OrganizationPublic(logger mlogger.Logger, organization *model.OrganizationBase) http.HandlerFunc {
|
||||
return response.Ok(logger, organizationPublicResponse{
|
||||
[]model.OrganizationBase{*organization},
|
||||
})
|
||||
}
|
||||
389
api/edge/bff/interface/api/sresponse/payment.go
Normal file
389
api/edge/bff/interface/api/sresponse/payment.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type FeeLine struct {
|
||||
LedgerAccountRef string `json:"ledgerAccountRef,omitempty"`
|
||||
Amount *paymenttypes.Money `json:"amount,omitempty"`
|
||||
LineType string `json:"lineType,omitempty"`
|
||||
Side string `json:"side,omitempty"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type FxQuote struct {
|
||||
QuoteRef string `json:"quoteRef,omitempty"`
|
||||
BaseCurrency string `json:"baseCurrency,omitempty"`
|
||||
QuoteCurrency string `json:"quoteCurrency,omitempty"`
|
||||
Side string `json:"side,omitempty"`
|
||||
Price string `json:"price,omitempty"`
|
||||
BaseAmount *paymenttypes.Money `json:"baseAmount,omitempty"`
|
||||
QuoteAmount *paymenttypes.Money `json:"quoteAmount,omitempty"`
|
||||
ExpiresAtUnixMs int64 `json:"expiresAtUnixMs,omitempty"`
|
||||
PricedAtUnixMs int64 `json:"pricedAtUnixMs,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
RateRef string `json:"rateRef,omitempty"`
|
||||
Firm bool `json:"firm,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentQuote struct {
|
||||
QuoteRef string `json:"quoteRef,omitempty"`
|
||||
IntentRef string `json:"intentRef,omitempty"`
|
||||
Amounts *QuoteAmounts `json:"amounts,omitempty"`
|
||||
Fees *QuoteFees `json:"fees,omitempty"`
|
||||
FxQuote *FxQuote `json:"fxQuote,omitempty"`
|
||||
}
|
||||
|
||||
type QuoteAmounts struct {
|
||||
SourcePrincipal *paymenttypes.Money `json:"sourcePrincipal,omitempty"`
|
||||
SourceDebitTotal *paymenttypes.Money `json:"sourceDebitTotal,omitempty"`
|
||||
DestinationSettlement *paymenttypes.Money `json:"destinationSettlement,omitempty"`
|
||||
}
|
||||
|
||||
type QuoteFees struct {
|
||||
Lines []FeeLine `json:"lines,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentQuotes struct {
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
QuoteRef string `json:"quoteRef,omitempty"`
|
||||
Items []PaymentQuote `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
type Payment struct {
|
||||
PaymentRef string `json:"paymentRef,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
FailureCode string `json:"failureCode,omitempty"`
|
||||
FailureReason string `json:"failureReason,omitempty"`
|
||||
Operations []PaymentOperation `json:"operations,omitempty"`
|
||||
LastQuote *PaymentQuote `json:"lastQuote,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentOperation struct {
|
||||
StepRef string `json:"stepRef,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
FailureCode string `json:"failureCode,omitempty"`
|
||||
FailureReason string `json:"failureReason,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
}
|
||||
|
||||
type paymentQuoteResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
Quote *PaymentQuote `json:"quote"`
|
||||
}
|
||||
|
||||
type paymentQuotesResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Quote *PaymentQuotes `json:"quote"`
|
||||
}
|
||||
|
||||
type paymentsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Payments []Payment `json:"payments"`
|
||||
Page *paginationv1.CursorPageResponse `json:"page,omitempty"`
|
||||
}
|
||||
|
||||
type paymentResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Payment *Payment `json:"payment"`
|
||||
}
|
||||
|
||||
// PaymentQuote wraps a payment quote with refreshed access token.
|
||||
func PaymentQuoteResponse(logger mlogger.Logger, idempotencyKey string, quote *quotationv2.PaymentQuote, token *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, paymentQuoteResponse{
|
||||
Quote: toPaymentQuote(quote),
|
||||
IdempotencyKey: idempotencyKey,
|
||||
authResponse: authResponse{AccessToken: *token},
|
||||
})
|
||||
}
|
||||
|
||||
// PaymentQuotes wraps batch quotes with refreshed access token.
|
||||
func PaymentQuotesResponse(logger mlogger.Logger, resp *quotationv2.QuotePaymentsResponse, token *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, paymentQuotesResponse{
|
||||
Quote: toPaymentQuotes(resp),
|
||||
authResponse: authResponse{AccessToken: *token},
|
||||
})
|
||||
}
|
||||
|
||||
// Payments wraps a list of payments with refreshed access token.
|
||||
func PaymentsResponse(logger mlogger.Logger, payments []*orchestrationv2.Payment, token *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, paymentsResponse{
|
||||
Payments: toPayments(payments),
|
||||
authResponse: authResponse{AccessToken: *token},
|
||||
})
|
||||
}
|
||||
|
||||
// PaymentsList wraps a list of payments with refreshed access token and pagination data.
|
||||
func PaymentsListResponse(logger mlogger.Logger, resp *orchestrationv2.ListPaymentsResponse, token *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, paymentsResponse{
|
||||
Payments: toPayments(resp.GetPayments()),
|
||||
Page: resp.GetPage(),
|
||||
authResponse: authResponse{AccessToken: *token},
|
||||
})
|
||||
}
|
||||
|
||||
// Payment wraps a payment with refreshed access token.
|
||||
func PaymentResponse(logger mlogger.Logger, payment *orchestrationv2.Payment, token *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, paymentResponse{
|
||||
Payment: toPayment(payment),
|
||||
authResponse: authResponse{AccessToken: *token},
|
||||
})
|
||||
}
|
||||
|
||||
func toFeeLines(lines []*feesv1.DerivedPostingLine) []FeeLine {
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]FeeLine, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
if line == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, FeeLine{
|
||||
LedgerAccountRef: line.GetLedgerAccountRef(),
|
||||
Amount: toMoney(line.GetMoney()),
|
||||
LineType: enumJSONName(line.GetLineType().String()),
|
||||
Side: enumJSONName(line.GetSide().String()),
|
||||
Meta: line.GetMeta(),
|
||||
})
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toFxQuote(q *oraclev1.Quote) *FxQuote {
|
||||
if q == nil {
|
||||
return nil
|
||||
}
|
||||
pair := q.GetPair()
|
||||
pricedAtUnixMs := int64(0)
|
||||
if ts := q.GetPricedAt(); ts != nil {
|
||||
pricedAtUnixMs = ts.AsTime().UnixMilli()
|
||||
}
|
||||
base := ""
|
||||
quote := ""
|
||||
if pair != nil {
|
||||
base = pair.GetBase()
|
||||
quote = pair.GetQuote()
|
||||
}
|
||||
return &FxQuote{
|
||||
QuoteRef: q.GetQuoteRef(),
|
||||
BaseCurrency: base,
|
||||
QuoteCurrency: quote,
|
||||
Side: enumJSONName(q.GetSide().String()),
|
||||
Price: q.GetPrice().GetValue(),
|
||||
BaseAmount: toMoney(q.GetBaseAmount()),
|
||||
QuoteAmount: toMoney(q.GetQuoteAmount()),
|
||||
ExpiresAtUnixMs: q.GetExpiresAtUnixMs(),
|
||||
PricedAtUnixMs: pricedAtUnixMs,
|
||||
Provider: q.GetProvider(),
|
||||
RateRef: q.GetRateRef(),
|
||||
Firm: q.GetFirm(),
|
||||
}
|
||||
}
|
||||
|
||||
func toPaymentQuote(q *quotationv2.PaymentQuote) *PaymentQuote {
|
||||
if q == nil {
|
||||
return nil
|
||||
}
|
||||
amounts := toQuoteAmounts(q)
|
||||
fees := toQuoteFees(q.GetFeeLines())
|
||||
return &PaymentQuote{
|
||||
QuoteRef: q.GetQuoteRef(),
|
||||
IntentRef: strings.TrimSpace(q.GetIntentRef()),
|
||||
Amounts: amounts,
|
||||
Fees: fees,
|
||||
FxQuote: toFxQuote(q.GetFxQuote()),
|
||||
}
|
||||
}
|
||||
|
||||
func toPaymentQuotes(resp *quotationv2.QuotePaymentsResponse) *PaymentQuotes {
|
||||
if resp == nil {
|
||||
return nil
|
||||
}
|
||||
items := make([]PaymentQuote, 0, len(resp.GetQuotes()))
|
||||
for _, quote := range resp.GetQuotes() {
|
||||
if dto := toPaymentQuote(quote); dto != nil {
|
||||
items = append(items, *dto)
|
||||
}
|
||||
}
|
||||
if len(items) == 0 {
|
||||
items = nil
|
||||
}
|
||||
return &PaymentQuotes{
|
||||
IdempotencyKey: resp.GetIdempotencyKey(),
|
||||
QuoteRef: resp.GetQuoteRef(),
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
|
||||
func toQuoteAmounts(q *quotationv2.PaymentQuote) *QuoteAmounts {
|
||||
if q == nil {
|
||||
return nil
|
||||
}
|
||||
amounts := &QuoteAmounts{
|
||||
SourcePrincipal: toMoney(q.GetTransferPrincipalAmount()),
|
||||
SourceDebitTotal: toMoney(q.GetPayerTotalDebitAmount()),
|
||||
DestinationSettlement: toMoney(q.GetDestinationAmount()),
|
||||
}
|
||||
if amounts.SourcePrincipal == nil && amounts.SourceDebitTotal == nil && amounts.DestinationSettlement == nil {
|
||||
return nil
|
||||
}
|
||||
return amounts
|
||||
}
|
||||
|
||||
func toQuoteFees(lines []*feesv1.DerivedPostingLine) *QuoteFees {
|
||||
feeLines := toFeeLines(lines)
|
||||
if len(feeLines) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &QuoteFees{Lines: feeLines}
|
||||
}
|
||||
|
||||
func toPayments(items []*orchestrationv2.Payment) []Payment {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]Payment, 0, len(items))
|
||||
for _, item := range items {
|
||||
if p := toPayment(item); p != nil {
|
||||
result = append(result, *p)
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toPayment(p *orchestrationv2.Payment) *Payment {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
operations := toUserVisibleOperations(p.GetStepExecutions())
|
||||
failureCode, failureReason := firstFailure(operations)
|
||||
return &Payment{
|
||||
PaymentRef: p.GetPaymentRef(),
|
||||
State: enumJSONName(p.GetState().String()),
|
||||
FailureCode: failureCode,
|
||||
FailureReason: failureReason,
|
||||
Operations: operations,
|
||||
LastQuote: toPaymentQuote(p.GetQuoteSnapshot()),
|
||||
CreatedAt: timestampAsTime(p.GetCreatedAt()),
|
||||
Meta: paymentMeta(p),
|
||||
IdempotencyKey: "",
|
||||
}
|
||||
}
|
||||
|
||||
func firstFailure(operations []PaymentOperation) (string, string) {
|
||||
for _, op := range operations {
|
||||
if strings.TrimSpace(op.FailureCode) == "" && strings.TrimSpace(op.FailureReason) == "" {
|
||||
continue
|
||||
}
|
||||
return strings.TrimSpace(op.FailureCode), strings.TrimSpace(op.FailureReason)
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func toUserVisibleOperations(steps []*orchestrationv2.StepExecution) []PaymentOperation {
|
||||
if len(steps) == 0 {
|
||||
return nil
|
||||
}
|
||||
ops := make([]PaymentOperation, 0, len(steps))
|
||||
for _, step := range steps {
|
||||
if step == nil || !isUserVisibleStep(step.GetReportVisibility()) {
|
||||
continue
|
||||
}
|
||||
ops = append(ops, toPaymentOperation(step))
|
||||
}
|
||||
if len(ops) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ops
|
||||
}
|
||||
|
||||
func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation {
|
||||
op := PaymentOperation{
|
||||
StepRef: step.GetStepRef(),
|
||||
Code: step.GetStepCode(),
|
||||
State: enumJSONName(step.GetState().String()),
|
||||
Label: strings.TrimSpace(step.GetUserLabel()),
|
||||
StartedAt: timestampAsTime(step.GetStartedAt()),
|
||||
CompletedAt: timestampAsTime(step.GetCompletedAt()),
|
||||
}
|
||||
failure := step.GetFailure()
|
||||
if failure == nil {
|
||||
return op
|
||||
}
|
||||
op.FailureCode = enumJSONName(failure.GetCategory().String())
|
||||
op.FailureReason = strings.TrimSpace(failure.GetMessage())
|
||||
if op.FailureReason == "" {
|
||||
op.FailureReason = strings.TrimSpace(failure.GetCode())
|
||||
}
|
||||
return op
|
||||
}
|
||||
|
||||
func isUserVisibleStep(visibility orchestrationv2.ReportVisibility) bool {
|
||||
switch visibility {
|
||||
case orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN,
|
||||
orchestrationv2.ReportVisibility_REPORT_VISIBILITY_BACKOFFICE,
|
||||
orchestrationv2.ReportVisibility_REPORT_VISIBILITY_AUDIT:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func paymentMeta(p *orchestrationv2.Payment) map[string]string {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
meta := make(map[string]string)
|
||||
if quotationRef := strings.TrimSpace(p.GetQuotationRef()); quotationRef != "" {
|
||||
meta["quotationRef"] = quotationRef
|
||||
}
|
||||
if clientPaymentRef := strings.TrimSpace(p.GetClientPaymentRef()); clientPaymentRef != "" {
|
||||
meta["clientPaymentRef"] = clientPaymentRef
|
||||
}
|
||||
if version := p.GetVersion(); version > 0 {
|
||||
meta["version"] = strconv.FormatUint(version, 10)
|
||||
}
|
||||
if len(meta) == 0 {
|
||||
return nil
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
func timestampAsTime(ts *timestamppb.Timestamp) time.Time {
|
||||
if ts == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return ts.AsTime()
|
||||
}
|
||||
|
||||
func enumJSONName(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
136
api/edge/bff/interface/api/sresponse/payment_test.go
Normal file
136
api/edge/bff/interface/api/sresponse/payment_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||
)
|
||||
|
||||
func TestToUserVisibleOperationsFiltersByVisibility(t *testing.T) {
|
||||
steps := []*orchestrationv2.StepExecution{
|
||||
{
|
||||
StepRef: "hidden",
|
||||
ReportVisibility: orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN,
|
||||
},
|
||||
{
|
||||
StepRef: "user",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_RUNNING,
|
||||
ReportVisibility: orchestrationv2.ReportVisibility_REPORT_VISIBILITY_USER,
|
||||
},
|
||||
{
|
||||
StepRef: "unspecified",
|
||||
StepCode: "hop.4.card_payout.observe",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
ReportVisibility: orchestrationv2.ReportVisibility_REPORT_VISIBILITY_UNSPECIFIED,
|
||||
},
|
||||
{
|
||||
StepRef: "backoffice",
|
||||
ReportVisibility: orchestrationv2.ReportVisibility_REPORT_VISIBILITY_BACKOFFICE,
|
||||
},
|
||||
}
|
||||
|
||||
ops := toUserVisibleOperations(steps)
|
||||
if len(ops) != 2 {
|
||||
t.Fatalf("operations count mismatch: got=%d want=2", len(ops))
|
||||
}
|
||||
if got, want := ops[0].StepRef, "user"; got != want {
|
||||
t.Fatalf("first operation step_ref mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := ops[1].StepRef, "unspecified"; got != want {
|
||||
t.Fatalf("second operation step_ref mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentFailureUsesVisibleOperationsOnly(t *testing.T) {
|
||||
dto := toPayment(&orchestrationv2.Payment{
|
||||
PaymentRef: "pay-1",
|
||||
State: orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_FAILED,
|
||||
StepExecutions: []*orchestrationv2.StepExecution{
|
||||
{
|
||||
StepRef: "hidden_failed",
|
||||
StepCode: "edge.1_2.ledger.debit",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_FAILED,
|
||||
ReportVisibility: orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN,
|
||||
Failure: &orchestrationv2.Failure{
|
||||
Category: sharedv1.PaymentFailureCode_FAILURE_LEDGER,
|
||||
Message: "internal hold release failure",
|
||||
},
|
||||
},
|
||||
{
|
||||
StepRef: "user_failed",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_FAILED,
|
||||
ReportVisibility: orchestrationv2.ReportVisibility_REPORT_VISIBILITY_USER,
|
||||
Failure: &orchestrationv2.Failure{
|
||||
Category: sharedv1.PaymentFailureCode_FAILURE_CHAIN,
|
||||
Message: "card declined",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if dto == nil {
|
||||
t.Fatal("expected non-nil payment dto")
|
||||
}
|
||||
if got, want := dto.FailureCode, "failure_chain"; got != want {
|
||||
t.Fatalf("failure_code mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := dto.FailureReason, "card declined"; got != want {
|
||||
t.Fatalf("failure_reason mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if len(dto.Operations) != 1 {
|
||||
t.Fatalf("operations count mismatch: got=%d want=1", len(dto.Operations))
|
||||
}
|
||||
if got, want := dto.Operations[0].StepRef, "user_failed"; got != want {
|
||||
t.Fatalf("visible operation mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentIgnoresHiddenFailures(t *testing.T) {
|
||||
dto := toPayment(&orchestrationv2.Payment{
|
||||
PaymentRef: "pay-2",
|
||||
State: orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_FAILED,
|
||||
StepExecutions: []*orchestrationv2.StepExecution{
|
||||
{
|
||||
StepRef: "hidden_failed",
|
||||
StepCode: "edge.1_2.ledger.release",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_FAILED,
|
||||
ReportVisibility: orchestrationv2.ReportVisibility_REPORT_VISIBILITY_BACKOFFICE,
|
||||
Failure: &orchestrationv2.Failure{
|
||||
Category: sharedv1.PaymentFailureCode_FAILURE_LEDGER,
|
||||
Message: "backoffice only failure",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if dto == nil {
|
||||
t.Fatal("expected non-nil payment dto")
|
||||
}
|
||||
if got := dto.FailureCode; got != "" {
|
||||
t.Fatalf("expected empty failure_code, got=%q", got)
|
||||
}
|
||||
if got := dto.FailureReason; got != "" {
|
||||
t.Fatalf("expected empty failure_reason, got=%q", got)
|
||||
}
|
||||
if len(dto.Operations) != 0 {
|
||||
t.Fatalf("expected no visible operations, got=%d", len(dto.Operations))
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentQuote_MapsIntentRef(t *testing.T) {
|
||||
dto := toPaymentQuote("ationv2.PaymentQuote{
|
||||
QuoteRef: "quote-1",
|
||||
IntentRef: "intent-1",
|
||||
})
|
||||
if dto == nil {
|
||||
t.Fatal("expected non-nil quote dto")
|
||||
}
|
||||
if got, want := dto.QuoteRef, "quote-1"; got != want {
|
||||
t.Fatalf("quote_ref mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := dto.IntentRef, "intent-1"; got != want {
|
||||
t.Fatalf("intent_ref mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
45
api/edge/bff/interface/api/sresponse/permissions.go
Normal file
45
api/edge/bff/interface/api/sresponse/permissions.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type permissionsDescription struct {
|
||||
Roles []model.RoleDescription `json:"roles"`
|
||||
Policies []model.PolicyDescription `json:"policies"`
|
||||
}
|
||||
|
||||
type permissionsData struct {
|
||||
Roles []model.Role `json:"roles"`
|
||||
Policies []model.RolePolicy `json:"policies"`
|
||||
Permissions []model.Permission `json:"permissions"`
|
||||
}
|
||||
|
||||
type permissionsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Descriptions permissionsDescription `json:"descriptions"`
|
||||
Permissions permissionsData `json:"permissions"`
|
||||
}
|
||||
|
||||
func Permisssions(logger mlogger.Logger,
|
||||
rolesDescs []model.RoleDescription, policiesDescs []model.PolicyDescription,
|
||||
roles []model.Role, policies []model.RolePolicy, permissions []model.Permission,
|
||||
accessToken *TokenData,
|
||||
) http.HandlerFunc {
|
||||
return response.Ok(logger, permissionsResponse{
|
||||
Descriptions: permissionsDescription{
|
||||
Roles: rolesDescs,
|
||||
Policies: policiesDescs,
|
||||
},
|
||||
Permissions: permissionsData{
|
||||
Roles: roles,
|
||||
Policies: policies,
|
||||
Permissions: permissions,
|
||||
},
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
14
api/edge/bff/interface/api/sresponse/response.go
Normal file
14
api/edge/bff/interface/api/sresponse/response.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
emodel "github.com/tech/sendico/server/interface/model"
|
||||
)
|
||||
|
||||
type (
|
||||
HandlerFunc = func(r *http.Request) http.HandlerFunc
|
||||
AccountHandlerFunc = func(r *http.Request, account *model.Account, accessToken *TokenData) http.HandlerFunc
|
||||
PendingAccountHandlerFunc = func(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc
|
||||
)
|
||||
27
api/edge/bff/interface/api/sresponse/result.go
Normal file
27
api/edge/bff/interface/api/sresponse/result.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
)
|
||||
|
||||
type resultAuth struct {
|
||||
authResponse `json:",inline"`
|
||||
response.Result `json:",inline"`
|
||||
}
|
||||
|
||||
func Success(logger mlogger.Logger, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, &resultAuth{
|
||||
Result: response.Result{Result: true},
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
func Failed(logger mlogger.Logger, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Accepted(logger, &resultAuth{
|
||||
Result: response.Result{Result: false},
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
16
api/edge/bff/interface/api/sresponse/signup.go
Normal file
16
api/edge/bff/interface/api/sresponse/signup.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
func SignUp(logger mlogger.Logger, account *model.Account) http.HandlerFunc {
|
||||
return response.Ok(
|
||||
logger,
|
||||
&account.AccountBase,
|
||||
)
|
||||
}
|
||||
23
api/edge/bff/interface/api/sresponse/signupavailability.go
Normal file
23
api/edge/bff/interface/api/sresponse/signupavailability.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
)
|
||||
|
||||
type SignupAvailability struct {
|
||||
Login string `json:"login"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
|
||||
func SignUpAvailability(logger mlogger.Logger, login string, available bool) http.HandlerFunc {
|
||||
return response.Ok(
|
||||
logger,
|
||||
SignupAvailability{
|
||||
Login: login,
|
||||
Available: available,
|
||||
},
|
||||
)
|
||||
}
|
||||
8
api/edge/bff/interface/api/sresponse/token.go
Normal file
8
api/edge/bff/interface/api/sresponse/token.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package sresponse
|
||||
|
||||
import "time"
|
||||
|
||||
type TokenData struct {
|
||||
Token string `json:"token"`
|
||||
Expiration time.Time `json:"expiration"`
|
||||
}
|
||||
292
api/edge/bff/interface/api/sresponse/wallet.go
Normal file
292
api/edge/bff/interface/api/sresponse/wallet.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type walletAsset struct {
|
||||
Chain string `json:"chain"`
|
||||
TokenSymbol string `json:"tokenSymbol"`
|
||||
ContractAddress string `json:"contractAddress"`
|
||||
}
|
||||
|
||||
type wallet struct {
|
||||
WalletRef string `json:"walletRef"`
|
||||
OrganizationRef string `json:"organizationRef"`
|
||||
OwnerRef string `json:"ownerRef"`
|
||||
Asset walletAsset `json:"asset"`
|
||||
DepositAddress string `json:"depositAddress"`
|
||||
Status string `json:"status"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
CreatedAt string `json:"createdAt,omitempty"`
|
||||
UpdatedAt string `json:"updatedAt,omitempty"`
|
||||
}
|
||||
|
||||
type walletsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Wallets []wallet `json:"wallets"`
|
||||
Page *paginationv1.CursorPageResponse `json:"page,omitempty"`
|
||||
}
|
||||
|
||||
type walletBalance struct {
|
||||
Available *paymenttypes.Money `json:"available,omitempty"`
|
||||
PendingInbound *paymenttypes.Money `json:"pendingInbound,omitempty"`
|
||||
PendingOutbound *paymenttypes.Money `json:"pendingOutbound,omitempty"`
|
||||
CalculatedAt string `json:"calculatedAt,omitempty"`
|
||||
}
|
||||
|
||||
type walletBalanceResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Balance walletBalance `json:"balance"`
|
||||
}
|
||||
|
||||
func Wallets(logger mlogger.Logger, resp *chainv1.ListManagedWalletsResponse, accessToken *TokenData) http.HandlerFunc {
|
||||
dto := walletsResponse{
|
||||
Page: resp.GetPage(),
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
}
|
||||
dto.Wallets = make([]wallet, 0, len(resp.GetWallets()))
|
||||
for _, w := range resp.GetWallets() {
|
||||
dto.Wallets = append(dto.Wallets, toWallet(w))
|
||||
}
|
||||
return response.Ok(logger, dto)
|
||||
}
|
||||
|
||||
func WalletBalance(logger mlogger.Logger, bal *chainv1.WalletBalance, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, walletBalanceResponse{
|
||||
Balance: toWalletBalance(bal),
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
func toWallet(w *chainv1.ManagedWallet) wallet {
|
||||
if w == nil {
|
||||
return wallet{}
|
||||
}
|
||||
asset := w.GetAsset()
|
||||
chain := ""
|
||||
token := ""
|
||||
contract := ""
|
||||
if asset != nil {
|
||||
chain = chainNetworkValue(asset.GetChain())
|
||||
token = asset.GetTokenSymbol()
|
||||
contract = asset.GetContractAddress()
|
||||
}
|
||||
name := ""
|
||||
if d := w.GetDescribable(); d != nil {
|
||||
name = strings.TrimSpace(d.GetName())
|
||||
}
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(w.GetMetadata()["name"])
|
||||
}
|
||||
if name == "" {
|
||||
name = w.GetWalletRef()
|
||||
}
|
||||
var description *string
|
||||
if d := w.GetDescribable(); d != nil && d.Description != nil {
|
||||
if trimmed := strings.TrimSpace(d.GetDescription()); trimmed != "" {
|
||||
description = &trimmed
|
||||
}
|
||||
}
|
||||
if description == nil {
|
||||
if trimmed := strings.TrimSpace(w.GetMetadata()["description"]); trimmed != "" {
|
||||
description = &trimmed
|
||||
}
|
||||
}
|
||||
return wallet{
|
||||
WalletRef: w.GetWalletRef(),
|
||||
OrganizationRef: w.GetOrganizationRef(),
|
||||
OwnerRef: w.GetOwnerRef(),
|
||||
Asset: walletAsset{
|
||||
Chain: chain,
|
||||
TokenSymbol: token,
|
||||
ContractAddress: contract,
|
||||
},
|
||||
DepositAddress: w.GetDepositAddress(),
|
||||
Status: w.GetStatus().String(),
|
||||
Metadata: w.GetMetadata(),
|
||||
Name: name,
|
||||
Description: description,
|
||||
CreatedAt: tsToString(w.GetCreatedAt()),
|
||||
UpdatedAt: tsToString(w.GetUpdatedAt()),
|
||||
}
|
||||
}
|
||||
|
||||
func toWalletBalance(b *chainv1.WalletBalance) walletBalance {
|
||||
if b == nil {
|
||||
return walletBalance{}
|
||||
}
|
||||
return walletBalance{
|
||||
Available: toMoney(b.GetAvailable()),
|
||||
PendingInbound: toMoney(b.GetPendingInbound()),
|
||||
PendingOutbound: toMoney(b.GetPendingOutbound()),
|
||||
CalculatedAt: tsToString(b.GetCalculatedAt()),
|
||||
}
|
||||
}
|
||||
|
||||
func tsToString(ts *timestamppb.Timestamp) string {
|
||||
if ts == nil {
|
||||
return ""
|
||||
}
|
||||
return ts.AsTime().UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func chainNetworkValue(chain chainv1.ChainNetwork) string {
|
||||
name := chain.String()
|
||||
if !strings.HasPrefix(name, "CHAIN_NETWORK_") {
|
||||
return "unspecified"
|
||||
}
|
||||
trimmed := strings.TrimPrefix(name, "CHAIN_NETWORK_")
|
||||
if trimmed == "" {
|
||||
return "unspecified"
|
||||
}
|
||||
return strings.ToLower(trimmed)
|
||||
}
|
||||
|
||||
// WalletsFromAccounts converts connector accounts to wallet response format.
|
||||
// Used when querying multiple gateways via discovery.
|
||||
func WalletsFromAccounts(logger mlogger.Logger, accounts []*connectorv1.Account, accessToken *TokenData) http.HandlerFunc {
|
||||
dto := walletsResponse{
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
}
|
||||
dto.Wallets = make([]wallet, 0, len(accounts))
|
||||
for _, acc := range accounts {
|
||||
if acc == nil {
|
||||
continue
|
||||
}
|
||||
dto.Wallets = append(dto.Wallets, accountToWallet(acc))
|
||||
}
|
||||
return response.Ok(logger, dto)
|
||||
}
|
||||
|
||||
func accountToWallet(acc *connectorv1.Account) wallet {
|
||||
if acc == nil {
|
||||
return wallet{}
|
||||
}
|
||||
|
||||
// Extract wallet details from provider details
|
||||
details := map[string]interface{}{}
|
||||
if acc.GetProviderDetails() != nil {
|
||||
details = acc.GetProviderDetails().AsMap()
|
||||
}
|
||||
|
||||
walletRef := ""
|
||||
if ref := acc.GetRef(); ref != nil {
|
||||
walletRef = strings.TrimSpace(ref.GetAccountId())
|
||||
}
|
||||
if v := stringFromDetails(details, "wallet_ref"); v != "" {
|
||||
walletRef = v
|
||||
}
|
||||
|
||||
organizationRef := stringFromDetails(details, "organization_ref")
|
||||
ownerRef := strings.TrimSpace(acc.GetOwnerRef())
|
||||
if v := stringFromDetails(details, "owner_ref"); v != "" {
|
||||
ownerRef = v
|
||||
}
|
||||
|
||||
chain := stringFromDetails(details, "network")
|
||||
tokenSymbol := stringFromDetails(details, "token_symbol")
|
||||
contractAddress := stringFromDetails(details, "contract_address")
|
||||
depositAddress := stringFromDetails(details, "deposit_address")
|
||||
|
||||
name := ""
|
||||
if d := acc.GetDescribable(); d != nil {
|
||||
name = strings.TrimSpace(d.GetName())
|
||||
}
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(acc.GetLabel())
|
||||
}
|
||||
if name == "" {
|
||||
name = walletRef
|
||||
}
|
||||
|
||||
var description *string
|
||||
if d := acc.GetDescribable(); d != nil && d.Description != nil {
|
||||
if trimmed := strings.TrimSpace(d.GetDescription()); trimmed != "" {
|
||||
description = &trimmed
|
||||
}
|
||||
}
|
||||
|
||||
status := acc.GetState().String()
|
||||
// Convert connector state to wallet status format
|
||||
switch acc.GetState() {
|
||||
case connectorv1.AccountState_ACCOUNT_ACTIVE:
|
||||
status = "MANAGED_WALLET_ACTIVE"
|
||||
case connectorv1.AccountState_ACCOUNT_SUSPENDED:
|
||||
status = "MANAGED_WALLET_SUSPENDED"
|
||||
case connectorv1.AccountState_ACCOUNT_CLOSED:
|
||||
status = "MANAGED_WALLET_CLOSED"
|
||||
}
|
||||
|
||||
return wallet{
|
||||
WalletRef: walletRef,
|
||||
OrganizationRef: organizationRef,
|
||||
OwnerRef: ownerRef,
|
||||
Asset: walletAsset{
|
||||
Chain: chain,
|
||||
TokenSymbol: tokenSymbol,
|
||||
ContractAddress: contractAddress,
|
||||
},
|
||||
DepositAddress: depositAddress,
|
||||
Status: status,
|
||||
Name: name,
|
||||
Description: description,
|
||||
CreatedAt: tsToString(acc.GetCreatedAt()),
|
||||
UpdatedAt: tsToString(acc.GetUpdatedAt()),
|
||||
}
|
||||
}
|
||||
|
||||
func stringFromDetails(details map[string]interface{}, key string) string {
|
||||
if details == nil {
|
||||
return ""
|
||||
}
|
||||
if value, ok := details[key]; ok {
|
||||
return strings.TrimSpace(fmt.Sprint(value))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// WalletBalanceFromConnector converts connector balance to wallet balance response format.
|
||||
// Used when querying gateways via discovery.
|
||||
func WalletBalanceFromConnector(logger mlogger.Logger, bal *connectorv1.Balance, accessToken *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, walletBalanceResponse{
|
||||
Balance: connectorBalanceToWalletBalance(bal),
|
||||
authResponse: authResponse{AccessToken: *accessToken},
|
||||
})
|
||||
}
|
||||
|
||||
func connectorBalanceToWalletBalance(b *connectorv1.Balance) walletBalance {
|
||||
if b == nil {
|
||||
return walletBalance{}
|
||||
}
|
||||
return walletBalance{
|
||||
Available: connectorMoneyToModel(b.GetAvailable()),
|
||||
PendingInbound: connectorMoneyToModel(b.GetPendingInbound()),
|
||||
PendingOutbound: connectorMoneyToModel(b.GetPendingOutbound()),
|
||||
CalculatedAt: tsToString(b.GetCalculatedAt()),
|
||||
}
|
||||
}
|
||||
|
||||
func connectorMoneyToModel(m *moneyv1.Money) *paymenttypes.Money {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: m.GetAmount(),
|
||||
Currency: m.GetCurrency(),
|
||||
}
|
||||
}
|
||||
57
api/edge/bff/interface/api/sresponse/ws/response.go
Normal file
57
api/edge/bff/interface/api/sresponse/ws/response.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
r "github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/server/interface/api/ws"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
func respond(logger mlogger.Logger, conn *websocket.Conn, messageType, apiStatus, requestID string, data any) {
|
||||
message := ws.Message{
|
||||
BaseResponse: r.BaseResponse{
|
||||
Status: apiStatus,
|
||||
Data: data,
|
||||
},
|
||||
ID: requestID,
|
||||
MessageType: messageType,
|
||||
}
|
||||
|
||||
if err := websocket.JSON.Send(conn, message); err != nil {
|
||||
logger.Warn("Failed to send error message", zap.Error(err), zap.Any("message", message))
|
||||
}
|
||||
}
|
||||
|
||||
func errorf(logger mlogger.Logger, messageType, requestID string, conn *websocket.Conn, resp r.ErrorResponse) {
|
||||
logger.Debug(
|
||||
"Writing error sresponse",
|
||||
zap.String("error", resp.Error),
|
||||
zap.String("details", resp.Details),
|
||||
zap.Int("code", resp.Code),
|
||||
)
|
||||
respond(logger, conn, messageType, api.MSError, requestID, &resp)
|
||||
}
|
||||
|
||||
func Ok(logger mlogger.Logger, requestID string, data any) ws.ResponseHandler {
|
||||
res := func(messageType string, conn *websocket.Conn) {
|
||||
logger.Debug("Successfully executed request", zap.Any("sresponse", data))
|
||||
respond(logger, conn, messageType, api.MSSuccess, requestID, data)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func Internal(logger mlogger.Logger, requestID string, err error) ws.ResponseHandler {
|
||||
res := func(messageType string, conn *websocket.Conn) {
|
||||
errorf(logger, messageType, requestID, conn,
|
||||
r.ErrorResponse{
|
||||
Error: "internal_error",
|
||||
Details: err.Error(),
|
||||
Code: http.StatusInternalServerError,
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
9
api/edge/bff/interface/api/ws/config.go
Normal file
9
api/edge/bff/interface/api/ws/config.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
ac "github.com/tech/sendico/server/internal/api/config"
|
||||
)
|
||||
|
||||
type (
|
||||
Config = ac.WebSocketConfig
|
||||
)
|
||||
12
api/edge/bff/interface/api/ws/handler.go
Normal file
12
api/edge/bff/interface/api/ws/handler.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
type (
|
||||
ResponseHandler func(messageType string, conn *websocket.Conn)
|
||||
HandlerFunc func(ctx context.Context, msg Message) ResponseHandler
|
||||
)
|
||||
9
api/edge/bff/interface/api/ws/message.go
Normal file
9
api/edge/bff/interface/api/ws/message.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package ws
|
||||
|
||||
import "github.com/tech/sendico/pkg/api/http/response"
|
||||
|
||||
type Message struct {
|
||||
response.BaseResponse
|
||||
ID string `json:"id"`
|
||||
MessageType string `json:"messageType"`
|
||||
}
|
||||
31
api/edge/bff/interface/middleware/middleware.go
Normal file
31
api/edge/bff/interface/middleware/middleware.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
ai "github.com/tech/sendico/server/internal/api/config"
|
||||
)
|
||||
|
||||
type (
|
||||
TokenConfig = ai.TokenConfig
|
||||
Config = ai.Config
|
||||
Signature = ai.SignatureConf
|
||||
PasswordConfig = ai.PasswordConfig
|
||||
)
|
||||
|
||||
type MapClaims = ai.MapClaims
|
||||
|
||||
func getKey(osEnv string) any {
|
||||
if len(osEnv) == 0 {
|
||||
return nil
|
||||
}
|
||||
return []byte(os.Getenv(osEnv))
|
||||
}
|
||||
|
||||
func SignatureConf(conf *Config) Signature {
|
||||
return Signature{
|
||||
PrivateKey: []byte(os.Getenv(conf.Signature.PrivateKeyEnv)),
|
||||
PublicKey: getKey(conf.Signature.PublicKeyEnv),
|
||||
Algorithm: conf.Signature.Algorithm,
|
||||
}
|
||||
}
|
||||
117
api/edge/bff/interface/model/token.go
Normal file
117
api/edge/bff/interface/model/token.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
mduration "github.com/tech/sendico/pkg/mutil/duration"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type AccountToken struct {
|
||||
AccountRef bson.ObjectID
|
||||
Login string
|
||||
Name string
|
||||
Locale string
|
||||
Expiration time.Time
|
||||
Pending bool
|
||||
}
|
||||
|
||||
func createAccountToken(a *model.Account, expiration int) AccountToken {
|
||||
return AccountToken{
|
||||
AccountRef: *a.GetID(),
|
||||
Login: a.Login,
|
||||
Name: a.Name,
|
||||
Locale: a.Locale,
|
||||
Expiration: time.Now().Add(mduration.Param2Duration(expiration, time.Hour)),
|
||||
Pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
func getTokenParam(claims middleware.MapClaims, param string) (string, error) {
|
||||
id, ok := claims[param].(string)
|
||||
if !ok {
|
||||
return "", merrors.NoData(fmt.Sprintf("param '%s' not found", param))
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
const (
|
||||
paramNameID = "id"
|
||||
paramNameName = "name"
|
||||
paramNameLocale = "locale"
|
||||
paramNameLogin = "login"
|
||||
paramNameExpiration = "exp"
|
||||
paramNamePending = "pending"
|
||||
)
|
||||
|
||||
func Claims2Token(claims middleware.MapClaims) (*AccountToken, error) {
|
||||
var at AccountToken
|
||||
var err error
|
||||
var account string
|
||||
if account, err = getTokenParam(claims, paramNameID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.AccountRef, err = bson.ObjectIDFromHex(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.Login, err = getTokenParam(claims, paramNameLogin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.Name, err = getTokenParam(claims, paramNameName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.Locale, err = getTokenParam(claims, paramNameLocale); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pending, ok := claims[paramNamePending]; ok {
|
||||
if pbool, ok := pending.(bool); ok {
|
||||
at.Pending = pbool
|
||||
}
|
||||
}
|
||||
if expValue, ok := claims[paramNameExpiration]; ok {
|
||||
switch exp := expValue.(type) {
|
||||
case time.Time:
|
||||
at.Expiration = exp
|
||||
case float64:
|
||||
at.Expiration = time.Unix(int64(exp), 0)
|
||||
case int64:
|
||||
at.Expiration = time.Unix(exp, 0)
|
||||
default:
|
||||
return nil, merrors.InvalidDataType(fmt.Sprintf("expiration param is of invalid type: %T", expValue))
|
||||
}
|
||||
} else {
|
||||
return nil, merrors.InvalidDataType(fmt.Sprintf("expiration param is of invalid type: %T", expValue))
|
||||
}
|
||||
return &at, nil
|
||||
}
|
||||
|
||||
func Account2Claims(a *model.Account, expiration int) middleware.MapClaims {
|
||||
t := createAccountToken(a, expiration)
|
||||
return middleware.MapClaims{
|
||||
paramNameID: t.AccountRef.Hex(),
|
||||
paramNameLogin: t.Login,
|
||||
paramNameName: t.Name,
|
||||
paramNameLocale: t.Locale,
|
||||
paramNameExpiration: int64(t.Expiration.Unix()),
|
||||
paramNamePending: t.Pending,
|
||||
}
|
||||
}
|
||||
|
||||
func PendingAccount2Claims(a *model.Account, expirationMinutes int) middleware.MapClaims {
|
||||
t := createAccountToken(a, expirationMinutes/60)
|
||||
t.Expiration = time.Now().Add(time.Duration(expirationMinutes) * time.Minute)
|
||||
t.Pending = true
|
||||
return middleware.MapClaims{
|
||||
paramNameID: t.AccountRef.Hex(),
|
||||
paramNameLogin: t.Login,
|
||||
paramNameName: t.Name,
|
||||
paramNameLocale: t.Locale,
|
||||
paramNameExpiration: t.Expiration.Unix(),
|
||||
paramNamePending: t.Pending,
|
||||
}
|
||||
}
|
||||
11
api/edge/bff/interface/services/account/account.go
Normal file
11
api/edge/bff/interface/services/account/account.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/accountapiimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return accountapiimp.CreateAPI(a)
|
||||
}
|
||||
12
api/edge/bff/interface/services/fileservice/config/config.go
Normal file
12
api/edge/bff/interface/services/fileservice/config/config.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package fileservice
|
||||
|
||||
import "github.com/tech/sendico/pkg/model"
|
||||
|
||||
type StorageType string
|
||||
|
||||
const (
|
||||
LocalFS StorageType = "local_fs"
|
||||
AwsS3 StorageType = "aws_s3"
|
||||
)
|
||||
|
||||
type Config = model.DriverConfig[StorageType]
|
||||
11
api/edge/bff/interface/services/fileservice/fileservice.go
Normal file
11
api/edge/bff/interface/services/fileservice/fileservice.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package fileservice
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/fileserviceimp"
|
||||
)
|
||||
|
||||
func CreateAPI(a api.API, directory string) (mservice.MicroService, error) {
|
||||
return fileserviceimp.CreateAPI(a, directory)
|
||||
}
|
||||
11
api/edge/bff/interface/services/invitation/invitation.go
Normal file
11
api/edge/bff/interface/services/invitation/invitation.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package invitation
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/invitationimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return invitationimp.CreateAPI(a)
|
||||
}
|
||||
11
api/edge/bff/interface/services/ledger/ledger.go
Normal file
11
api/edge/bff/interface/services/ledger/ledger.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package ledger
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/ledgerapiimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return ledgerapiimp.CreateAPI(a)
|
||||
}
|
||||
11
api/edge/bff/interface/services/logo/logo.go
Normal file
11
api/edge/bff/interface/services/logo/logo.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package logo
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/logoimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return logoimp.CreateAPI(a)
|
||||
}
|
||||
11
api/edge/bff/interface/services/organization/organization.go
Normal file
11
api/edge/bff/interface/services/organization/organization.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package organization
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/organizationimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return organizationimp.CreateAPI(a)
|
||||
}
|
||||
12
api/edge/bff/interface/services/payment/payment.go
Normal file
12
api/edge/bff/interface/services/payment/payment.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/paymentapiimp"
|
||||
)
|
||||
|
||||
// Create wires payment orchestrator BFF API.
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return paymentapiimp.CreateAPI(a)
|
||||
}
|
||||
11
api/edge/bff/interface/services/paymethod/paymethod.go
Normal file
11
api/edge/bff/interface/services/paymethod/paymethod.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package paymethod
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/paymethodsimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return paymethodsimp.CreateAPI(a)
|
||||
}
|
||||
11
api/edge/bff/interface/services/permission/permission.go
Normal file
11
api/edge/bff/interface/services/permission/permission.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package permission
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/permissionsimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return permissionsimp.CreateAPI(a)
|
||||
}
|
||||
11
api/edge/bff/interface/services/recipient/recipient.go
Normal file
11
api/edge/bff/interface/services/recipient/recipient.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package recipient
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/recipientimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return recipientimp.CreateAPI(a)
|
||||
}
|
||||
11
api/edge/bff/interface/services/site/site.go
Normal file
11
api/edge/bff/interface/services/site/site.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package site
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
eapi "github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/siteimp"
|
||||
)
|
||||
|
||||
func Create(a eapi.API) (mservice.MicroService, error) {
|
||||
return siteimp.CreateAPI(a)
|
||||
}
|
||||
11
api/edge/bff/interface/services/verification/verification.go
Normal file
11
api/edge/bff/interface/services/verification/verification.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package verification
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/verificationimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return verificationimp.CreateAPI(a)
|
||||
}
|
||||
11
api/edge/bff/interface/services/wallet/wallet.go
Normal file
11
api/edge/bff/interface/services/wallet/wallet.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package wallet
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/walletapiimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return walletapiimp.CreateAPI(a)
|
||||
}
|
||||
157
api/edge/bff/internal/api/api.go
Normal file
157
api/edge/bff/internal/api/api.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package apiimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/tech/sendico/pkg/api/routers/health"
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/domainprovider"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/interface/services/account"
|
||||
"github.com/tech/sendico/server/interface/services/invitation"
|
||||
"github.com/tech/sendico/server/interface/services/ledger"
|
||||
"github.com/tech/sendico/server/interface/services/logo"
|
||||
"github.com/tech/sendico/server/interface/services/organization"
|
||||
"github.com/tech/sendico/server/interface/services/payment"
|
||||
"github.com/tech/sendico/server/interface/services/paymethod"
|
||||
"github.com/tech/sendico/server/interface/services/permission"
|
||||
"github.com/tech/sendico/server/interface/services/recipient"
|
||||
"github.com/tech/sendico/server/interface/services/site"
|
||||
"github.com/tech/sendico/server/interface/services/verification"
|
||||
"github.com/tech/sendico/server/interface/services/wallet"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Microservices = []mservice.MicroService
|
||||
|
||||
// APIImp represents the structure of the APIImp
|
||||
type APIImp struct {
|
||||
logger mlogger.Logger
|
||||
db db.Factory
|
||||
domain domainprovider.DomainProvider
|
||||
config *api.Config
|
||||
services Microservices
|
||||
mw *Middleware
|
||||
}
|
||||
|
||||
func (a *APIImp) installMicroservice(srv mservice.MicroService) {
|
||||
a.services = append(a.services, srv)
|
||||
a.logger.Info("Microservice installed", zap.String("service", srv.Name()))
|
||||
}
|
||||
|
||||
func (a *APIImp) addMicroservice(srvf api.MicroServiceFactoryT) error {
|
||||
srv, err := srvf(a)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to install a microservice", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
a.installMicroservice(srv)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *APIImp) Logger() mlogger.Logger {
|
||||
return a.logger
|
||||
}
|
||||
|
||||
func (a *APIImp) Config() *api.Config {
|
||||
return a.config
|
||||
}
|
||||
|
||||
func (a *APIImp) DBFactory() db.Factory {
|
||||
return a.db
|
||||
}
|
||||
|
||||
func (a *APIImp) DomainProvider() domainprovider.DomainProvider {
|
||||
return a.domain
|
||||
}
|
||||
|
||||
func (a *APIImp) Register() api.Register {
|
||||
return a.mw
|
||||
}
|
||||
|
||||
func (a *APIImp) Permissions() auth.Provider {
|
||||
return a.db.Permissions()
|
||||
}
|
||||
|
||||
func (a *APIImp) installServices() error {
|
||||
srvf := make([]api.MicroServiceFactoryT, 0)
|
||||
|
||||
srvf = append(srvf, account.Create)
|
||||
srvf = append(srvf, verification.Create)
|
||||
srvf = append(srvf, organization.Create)
|
||||
srvf = append(srvf, invitation.Create)
|
||||
srvf = append(srvf, logo.Create)
|
||||
srvf = append(srvf, permission.Create)
|
||||
srvf = append(srvf, site.Create)
|
||||
srvf = append(srvf, wallet.Create)
|
||||
srvf = append(srvf, ledger.Create)
|
||||
srvf = append(srvf, recipient.Create)
|
||||
srvf = append(srvf, paymethod.Create)
|
||||
srvf = append(srvf, payment.Create)
|
||||
|
||||
for _, v := range srvf {
|
||||
if err := a.addMicroservice(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
a.mw.SetStatus(health.SSRunning)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *APIImp) Finish(ctx context.Context) error {
|
||||
a.mw.SetStatus(health.SSTerminating)
|
||||
a.mw.Finish()
|
||||
var lastError error
|
||||
for i := len(a.services) - 1; i >= 0; i-- {
|
||||
if err := (a.services[i]).Finish(ctx); err != nil {
|
||||
lastError = err
|
||||
a.logger.Warn("Error occurred when finishing service",
|
||||
zap.Error(err), zap.String("service_name", (a.services[i]).Name()))
|
||||
} else {
|
||||
a.logger.Info("Microservice is down", zap.String("service_name", (a.services[i]).Name()))
|
||||
}
|
||||
}
|
||||
return lastError
|
||||
}
|
||||
|
||||
func (a *APIImp) Name() string {
|
||||
return "api"
|
||||
}
|
||||
|
||||
func CreateAPI(logger mlogger.Logger, config *api.Config, db db.Factory, router *chi.Mux, debug bool) (mservice.MicroService, error) {
|
||||
p := &APIImp{
|
||||
logger: logger.Named("api"),
|
||||
config: config,
|
||||
db: db,
|
||||
}
|
||||
|
||||
var err error
|
||||
if p.domain, err = domainprovider.CreateDomainProvider(p.logger, config.Mw.DomainEnv, config.Mw.APIProtocolEnv, config.Mw.EndPointEnv); err != nil {
|
||||
p.logger.Error("Failed to initizlize domain provider")
|
||||
return nil, err
|
||||
}
|
||||
p.logger.Info("Domain provider installed")
|
||||
|
||||
if p.mw, err = CreateMiddleware(logger, db, p.db.Permissions().Enforcer(), router, config.Mw, debug); err != nil {
|
||||
p.logger.Error("Failed to create middleware", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
p.logger.Info("Middleware installed", zap.Bool("debug_mode", debug))
|
||||
|
||||
p.resolveServiceAddressesFromDiscovery()
|
||||
|
||||
p.logger.Info("Installing microservices...")
|
||||
if err := p.installServices(); err != nil {
|
||||
p.logger.Error("Failed to install a microservice", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
p.logger.Info("Microservices installation complete", zap.Int("microservices", len(p.services)))
|
||||
|
||||
return p, nil
|
||||
}
|
||||
66
api/edge/bff/internal/api/config/config.go
Executable file
66
api/edge/bff/internal/api/config/config.go
Executable file
@@ -0,0 +1,66 @@
|
||||
package apiimp
|
||||
|
||||
import "github.com/tech/sendico/pkg/messaging"
|
||||
|
||||
type CORSSettings struct {
|
||||
MaxAge int `yaml:"max_age"`
|
||||
AllowedOrigins []string `yaml:"allowed_origins"`
|
||||
AllowedMethods []string `yaml:"allowed_methods"`
|
||||
AllowedHeaders []string `yaml:"allowed_headers"`
|
||||
ExposedHeaders []string `yaml:"exposed_headers"`
|
||||
AllowCredentials bool `yaml:"allow_credentials"`
|
||||
}
|
||||
|
||||
type SignatureConf struct {
|
||||
PublicKey any
|
||||
PrivateKey []byte
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
type Signature struct {
|
||||
PublicKeyEnv string `yaml:"public_key_env,omitempty"`
|
||||
PrivateKeyEnv string `yaml:"secret_key_env"`
|
||||
Algorithm string `yaml:"algorithm"`
|
||||
}
|
||||
|
||||
type TokenExpiration struct {
|
||||
Account int `yaml:"account"`
|
||||
Refresh int `yaml:"refresh"`
|
||||
}
|
||||
|
||||
type TokenConfig struct {
|
||||
Expiration TokenExpiration `yaml:"expiration_hours"`
|
||||
Length int `yaml:"length"`
|
||||
}
|
||||
|
||||
type WebSocketConfig struct {
|
||||
EndpointEnv string `yaml:"endpoint_env"`
|
||||
Timeout int `yaml:"timeout"`
|
||||
}
|
||||
|
||||
type PasswordChecks struct {
|
||||
Digit bool `yaml:"digit"`
|
||||
Upper bool `yaml:"upper"`
|
||||
Lower bool `yaml:"lower"`
|
||||
Special bool `yaml:"special"`
|
||||
MinLength int `yaml:"min_length"`
|
||||
}
|
||||
|
||||
type PasswordConfig struct {
|
||||
TokenLength int `yaml:"token_length"`
|
||||
Check PasswordChecks `yaml:"check"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
DomainEnv string `yaml:"domain_env"`
|
||||
EndPointEnv string `yaml:"api_endpoint_env"`
|
||||
APIProtocolEnv string `yaml:"api_protocol_env"`
|
||||
Signature Signature `yaml:"signature"`
|
||||
CORS CORSSettings `yaml:"CORS"`
|
||||
WebSocket WebSocketConfig `yaml:"websocket"`
|
||||
Messaging messaging.Config `yaml:"message_broker"`
|
||||
Token TokenConfig `yaml:"token"`
|
||||
Password PasswordConfig `yaml:"password"`
|
||||
}
|
||||
|
||||
type MapClaims = map[string]any
|
||||
482
api/edge/bff/internal/api/discovery_resolver.go
Normal file
482
api/edge/bff/internal/api/discovery_resolver.go
Normal file
@@ -0,0 +1,482 @@
|
||||
package apiimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
eapi "github.com/tech/sendico/server/interface/api"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
discoveryBootstrapTimeout = 3 * time.Second
|
||||
discoveryBootstrapSender = "server_bootstrap"
|
||||
defaultClientDialTimeoutSecs = 5
|
||||
defaultClientCallTimeoutSecs = 5
|
||||
)
|
||||
|
||||
var (
|
||||
ledgerDiscoveryServiceNames = []string{
|
||||
"LEDGER",
|
||||
string(mservice.Ledger),
|
||||
}
|
||||
paymentOrchestratorDiscoveryServiceNames = []string{
|
||||
"PAYMENTS_ORCHESTRATOR",
|
||||
string(mservice.PaymentOrchestrator),
|
||||
}
|
||||
paymentQuotationDiscoveryServiceNames = []string{
|
||||
"PAYMENTS_QUOTATION",
|
||||
"PAYMENTS_QUOTE",
|
||||
"PAYMENT_QUOTATION",
|
||||
"payment_quotation",
|
||||
}
|
||||
paymentMethodsDiscoveryServiceNames = []string{
|
||||
"PAYMENTS_METHODS",
|
||||
"PAYMENT_METHODS",
|
||||
string(mservice.PaymentMethods),
|
||||
}
|
||||
)
|
||||
|
||||
type discoveryEndpoint struct {
|
||||
address string
|
||||
insecure bool
|
||||
raw string
|
||||
}
|
||||
|
||||
type serviceSelection struct {
|
||||
service discovery.ServiceSummary
|
||||
endpoint discoveryEndpoint
|
||||
opMatch bool
|
||||
nameRank int
|
||||
}
|
||||
|
||||
type gatewaySelection struct {
|
||||
gateway discovery.GatewaySummary
|
||||
endpoint discoveryEndpoint
|
||||
networkMatch bool
|
||||
opMatch bool
|
||||
}
|
||||
|
||||
// resolveServiceAddressesFromDiscovery looks up downstream service addresses once
|
||||
// during startup and applies them to the runtime config.
|
||||
func (a *APIImp) resolveServiceAddressesFromDiscovery() {
|
||||
if a == nil || a.config == nil || a.config.Mw == nil {
|
||||
return
|
||||
}
|
||||
|
||||
msgCfg := a.config.Mw.Messaging
|
||||
if msgCfg.Driver == "" {
|
||||
return
|
||||
}
|
||||
|
||||
logger := a.logger.Named("discovery_bootstrap")
|
||||
broker, err := msg.CreateMessagingBroker(logger.Named("bus"), &msgCfg)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create discovery bootstrap broker", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := discovery.NewClient(logger, broker, nil, discoveryBootstrapSender)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create discovery bootstrap client", zap.Error(err))
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), discoveryBootstrapTimeout)
|
||||
defer cancel()
|
||||
|
||||
lookup, err := client.Lookup(ctx)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to fetch discovery registry during startup", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
a.resolveChainGatewayAddress(lookup.Gateways)
|
||||
orchestratorFound, orchestratorEndpoint := a.resolvePaymentOrchestratorAddress(lookup.Services)
|
||||
a.resolveLedgerAddress(lookup.Services)
|
||||
a.resolvePaymentQuotationAddress(lookup.Services, orchestratorFound, orchestratorEndpoint)
|
||||
a.resolvePaymentMethodsAddress(lookup.Services)
|
||||
}
|
||||
|
||||
func (a *APIImp) resolveChainGatewayAddress(gateways []discovery.GatewaySummary) {
|
||||
cfg := a.config.ChainGateway
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, selected, ok := selectGatewayEndpoint(
|
||||
gateways,
|
||||
cfg.DefaultAsset.Chain,
|
||||
[]string{discovery.OperationBalanceRead},
|
||||
)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
cfg.Address = endpoint.address
|
||||
cfg.Insecure = endpoint.insecure
|
||||
ensureTimeoutsChainGateway(cfg)
|
||||
|
||||
a.logger.Info("Resolved chain gateway address from discovery",
|
||||
zap.String("rail", selected.Rail),
|
||||
zap.String("gateway_id", selected.ID),
|
||||
zap.String("network", selected.Network),
|
||||
zap.String("invoke_uri", endpoint.raw),
|
||||
zap.String("address", endpoint.address),
|
||||
zap.Bool("insecure", endpoint.insecure))
|
||||
}
|
||||
|
||||
func (a *APIImp) resolveLedgerAddress(services []discovery.ServiceSummary) {
|
||||
endpoint, selected, ok := selectServiceEndpoint(
|
||||
services,
|
||||
ledgerDiscoveryServiceNames,
|
||||
discovery.LedgerServiceOperations(),
|
||||
)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := ensureLedgerConfig(a.config)
|
||||
cfg.Address = endpoint.address
|
||||
cfg.Insecure = endpoint.insecure
|
||||
ensureTimeoutsLedger(cfg)
|
||||
|
||||
a.logger.Info("Resolved ledger address from discovery",
|
||||
zap.String("service", selected.Service),
|
||||
zap.String("service_id", selected.ID),
|
||||
zap.String("instance_id", selected.InstanceID),
|
||||
zap.String("invoke_uri", endpoint.raw),
|
||||
zap.String("address", endpoint.address),
|
||||
zap.Bool("insecure", endpoint.insecure))
|
||||
}
|
||||
|
||||
func (a *APIImp) resolvePaymentOrchestratorAddress(services []discovery.ServiceSummary) (bool, discoveryEndpoint) {
|
||||
endpoint, selected, ok := selectServiceEndpoint(
|
||||
services,
|
||||
paymentOrchestratorDiscoveryServiceNames,
|
||||
[]string{discovery.OperationPaymentInitiate},
|
||||
)
|
||||
if !ok {
|
||||
return false, discoveryEndpoint{}
|
||||
}
|
||||
|
||||
cfg := ensurePaymentOrchestratorConfig(a.config)
|
||||
cfg.Address = endpoint.address
|
||||
cfg.Insecure = endpoint.insecure
|
||||
ensureTimeoutsPayment(cfg)
|
||||
|
||||
a.logger.Info("Resolved payment orchestrator address from discovery",
|
||||
zap.String("service", selected.Service),
|
||||
zap.String("service_id", selected.ID),
|
||||
zap.String("instance_id", selected.InstanceID),
|
||||
zap.String("invoke_uri", endpoint.raw),
|
||||
zap.String("address", endpoint.address),
|
||||
zap.Bool("insecure", endpoint.insecure))
|
||||
|
||||
return true, endpoint
|
||||
}
|
||||
|
||||
func (a *APIImp) resolvePaymentQuotationAddress(services []discovery.ServiceSummary, orchestratorFound bool, orchestratorEndpoint discoveryEndpoint) {
|
||||
endpoint, selected, ok := selectServiceEndpoint(
|
||||
services,
|
||||
paymentQuotationDiscoveryServiceNames,
|
||||
[]string{discovery.OperationPaymentQuote},
|
||||
)
|
||||
if !ok {
|
||||
cfg := a.config.PaymentQuotation
|
||||
if cfg != nil && strings.TrimSpace(cfg.Address) != "" {
|
||||
return
|
||||
}
|
||||
if !orchestratorFound {
|
||||
return
|
||||
}
|
||||
// Fall back to orchestrator endpoint when quotation service is not announced.
|
||||
endpoint = orchestratorEndpoint
|
||||
selected = discovery.ServiceSummary{Service: "PAYMENTS_ORCHESTRATOR"}
|
||||
}
|
||||
|
||||
cfg := ensurePaymentQuotationConfig(a.config)
|
||||
cfg.Address = endpoint.address
|
||||
cfg.Insecure = endpoint.insecure
|
||||
ensureTimeoutsPayment(cfg)
|
||||
|
||||
a.logger.Info("Resolved payment quotation address from discovery",
|
||||
zap.String("service", selected.Service),
|
||||
zap.String("service_id", selected.ID),
|
||||
zap.String("instance_id", selected.InstanceID),
|
||||
zap.String("invoke_uri", endpoint.raw),
|
||||
zap.String("address", endpoint.address),
|
||||
zap.Bool("insecure", endpoint.insecure))
|
||||
}
|
||||
|
||||
func (a *APIImp) resolvePaymentMethodsAddress(services []discovery.ServiceSummary) {
|
||||
endpoint, selected, ok := selectServiceEndpoint(
|
||||
services,
|
||||
paymentMethodsDiscoveryServiceNames,
|
||||
[]string{discovery.OperationPaymentMethodsRead},
|
||||
)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := ensurePaymentMethodsConfig(a.config)
|
||||
cfg.Address = endpoint.address
|
||||
cfg.Insecure = endpoint.insecure
|
||||
ensureTimeoutsPayment(cfg)
|
||||
|
||||
a.logger.Info("Resolved payment methods address from discovery",
|
||||
zap.String("service", selected.Service),
|
||||
zap.String("service_id", selected.ID),
|
||||
zap.String("instance_id", selected.InstanceID),
|
||||
zap.String("invoke_uri", endpoint.raw),
|
||||
zap.String("address", endpoint.address),
|
||||
zap.Bool("insecure", endpoint.insecure))
|
||||
}
|
||||
|
||||
func selectServiceEndpoint(services []discovery.ServiceSummary, serviceNames []string, requiredOps []string) (discoveryEndpoint, discovery.ServiceSummary, bool) {
|
||||
selections := make([]serviceSelection, 0)
|
||||
for _, svc := range services {
|
||||
if !svc.Healthy {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(svc.InvokeURI) == "" {
|
||||
continue
|
||||
}
|
||||
nameRank, ok := serviceRank(svc.Service, serviceNames)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
endpoint, err := parseDiscoveryInvokeURI(svc.InvokeURI)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
selections = append(selections, serviceSelection{
|
||||
service: svc,
|
||||
endpoint: endpoint,
|
||||
opMatch: discovery.HasAnyOperation(svc.Ops, requiredOps),
|
||||
nameRank: nameRank,
|
||||
})
|
||||
}
|
||||
if len(selections) == 0 {
|
||||
return discoveryEndpoint{}, discovery.ServiceSummary{}, false
|
||||
}
|
||||
|
||||
sort.Slice(selections, func(i, j int) bool {
|
||||
if selections[i].opMatch != selections[j].opMatch {
|
||||
return selections[i].opMatch
|
||||
}
|
||||
if selections[i].nameRank != selections[j].nameRank {
|
||||
return selections[i].nameRank < selections[j].nameRank
|
||||
}
|
||||
if selections[i].service.ID != selections[j].service.ID {
|
||||
return selections[i].service.ID < selections[j].service.ID
|
||||
}
|
||||
return selections[i].service.InstanceID < selections[j].service.InstanceID
|
||||
})
|
||||
|
||||
selected := selections[0]
|
||||
return selected.endpoint, selected.service, true
|
||||
}
|
||||
|
||||
func selectGatewayEndpoint(gateways []discovery.GatewaySummary, preferredNetwork string, requiredOps []string) (discoveryEndpoint, discovery.GatewaySummary, bool) {
|
||||
preferredNetwork = strings.TrimSpace(preferredNetwork)
|
||||
selections := make([]gatewaySelection, 0)
|
||||
|
||||
for _, gateway := range gateways {
|
||||
if !gateway.Healthy {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(gateway.Rail), discovery.RailCrypto) {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(gateway.InvokeURI) == "" {
|
||||
continue
|
||||
}
|
||||
endpoint, err := parseDiscoveryInvokeURI(gateway.InvokeURI)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
selections = append(selections, gatewaySelection{
|
||||
gateway: gateway,
|
||||
endpoint: endpoint,
|
||||
networkMatch: preferredNetwork != "" && strings.EqualFold(strings.TrimSpace(gateway.Network), preferredNetwork),
|
||||
opMatch: discovery.HasAnyOperation(gateway.Ops, requiredOps),
|
||||
})
|
||||
}
|
||||
if len(selections) == 0 {
|
||||
return discoveryEndpoint{}, discovery.GatewaySummary{}, false
|
||||
}
|
||||
|
||||
sort.Slice(selections, func(i, j int) bool {
|
||||
if selections[i].networkMatch != selections[j].networkMatch {
|
||||
return selections[i].networkMatch
|
||||
}
|
||||
if selections[i].opMatch != selections[j].opMatch {
|
||||
return selections[i].opMatch
|
||||
}
|
||||
if selections[i].gateway.RoutingPriority != selections[j].gateway.RoutingPriority {
|
||||
return selections[i].gateway.RoutingPriority > selections[j].gateway.RoutingPriority
|
||||
}
|
||||
if selections[i].gateway.ID != selections[j].gateway.ID {
|
||||
return selections[i].gateway.ID < selections[j].gateway.ID
|
||||
}
|
||||
return selections[i].gateway.InstanceID < selections[j].gateway.InstanceID
|
||||
})
|
||||
|
||||
selected := selections[0]
|
||||
return selected.endpoint, selected.gateway, true
|
||||
}
|
||||
|
||||
func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return discoveryEndpoint{}, fmt.Errorf("Invoke uri is empty")
|
||||
}
|
||||
|
||||
// Without a scheme we expect a plain host:port target.
|
||||
if !strings.Contains(raw, "://") {
|
||||
if _, _, err := net.SplitHostPort(raw); err != nil {
|
||||
return discoveryEndpoint{}, fmt.Errorf("Invoke uri must include host:port: %w", err)
|
||||
}
|
||||
return discoveryEndpoint{
|
||||
address: raw,
|
||||
insecure: true,
|
||||
raw: raw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return discoveryEndpoint{}, err
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
|
||||
case "grpc":
|
||||
address := strings.TrimSpace(parsed.Host)
|
||||
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
|
||||
return discoveryEndpoint{}, fmt.Errorf("Grpc invoke uri must include host:port: %w", splitErr)
|
||||
}
|
||||
return discoveryEndpoint{
|
||||
address: address,
|
||||
insecure: true,
|
||||
raw: raw,
|
||||
}, nil
|
||||
case "grpcs":
|
||||
address := strings.TrimSpace(parsed.Host)
|
||||
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
|
||||
return discoveryEndpoint{}, fmt.Errorf("Grpcs invoke uri must include host:port: %w", splitErr)
|
||||
}
|
||||
return discoveryEndpoint{
|
||||
address: address,
|
||||
insecure: false,
|
||||
raw: raw,
|
||||
}, nil
|
||||
case "dns", "passthrough":
|
||||
// gRPC resolver targets such as dns:///service:port.
|
||||
return discoveryEndpoint{
|
||||
address: raw,
|
||||
insecure: true,
|
||||
raw: raw,
|
||||
}, nil
|
||||
default:
|
||||
return discoveryEndpoint{}, fmt.Errorf("Unsupported invoke uri scheme: %s", parsed.Scheme)
|
||||
}
|
||||
}
|
||||
|
||||
func serviceRank(service string, names []string) (int, bool) {
|
||||
service = strings.TrimSpace(service)
|
||||
if service == "" {
|
||||
return 0, false
|
||||
}
|
||||
for i, name := range names {
|
||||
if strings.EqualFold(service, strings.TrimSpace(name)) {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func ensureLedgerConfig(cfg *eapi.Config) *eapi.LedgerConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
if cfg.Ledger == nil {
|
||||
cfg.Ledger = &eapi.LedgerConfig{}
|
||||
}
|
||||
return cfg.Ledger
|
||||
}
|
||||
|
||||
func ensurePaymentOrchestratorConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
if cfg.PaymentOrchestrator == nil {
|
||||
cfg.PaymentOrchestrator = &eapi.PaymentOrchestratorConfig{}
|
||||
}
|
||||
return cfg.PaymentOrchestrator
|
||||
}
|
||||
|
||||
func ensurePaymentQuotationConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
if cfg.PaymentQuotation == nil {
|
||||
cfg.PaymentQuotation = &eapi.PaymentOrchestratorConfig{}
|
||||
}
|
||||
return cfg.PaymentQuotation
|
||||
}
|
||||
|
||||
func ensurePaymentMethodsConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
if cfg.PaymentMethods == nil {
|
||||
cfg.PaymentMethods = &eapi.PaymentOrchestratorConfig{}
|
||||
}
|
||||
return cfg.PaymentMethods
|
||||
}
|
||||
|
||||
func ensureTimeoutsLedger(cfg *eapi.LedgerConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
if cfg.DialTimeoutSeconds <= 0 {
|
||||
cfg.DialTimeoutSeconds = defaultClientDialTimeoutSecs
|
||||
}
|
||||
if cfg.CallTimeoutSeconds <= 0 {
|
||||
cfg.CallTimeoutSeconds = defaultClientCallTimeoutSecs
|
||||
}
|
||||
}
|
||||
|
||||
func ensureTimeoutsChainGateway(cfg *eapi.ChainGatewayConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
if cfg.DialTimeoutSeconds <= 0 {
|
||||
cfg.DialTimeoutSeconds = defaultClientDialTimeoutSecs
|
||||
}
|
||||
if cfg.CallTimeoutSeconds <= 0 {
|
||||
cfg.CallTimeoutSeconds = defaultClientCallTimeoutSecs
|
||||
}
|
||||
}
|
||||
|
||||
func ensureTimeoutsPayment(cfg *eapi.PaymentOrchestratorConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
if cfg.DialTimeoutSeconds <= 0 {
|
||||
cfg.DialTimeoutSeconds = defaultClientDialTimeoutSecs
|
||||
}
|
||||
if cfg.CallTimeoutSeconds <= 0 {
|
||||
cfg.CallTimeoutSeconds = defaultClientCallTimeoutSecs
|
||||
}
|
||||
}
|
||||
140
api/edge/bff/internal/api/discovery_resolver_test.go
Normal file
140
api/edge/bff/internal/api/discovery_resolver_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package apiimp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
)
|
||||
|
||||
func TestParseDiscoveryInvokeURI(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
raw string
|
||||
address string
|
||||
insecure bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "host_port",
|
||||
raw: "ledger:50052",
|
||||
address: "ledger:50052",
|
||||
insecure: true,
|
||||
},
|
||||
{
|
||||
name: "grpc_scheme",
|
||||
raw: "grpc://payments-orchestrator:50062",
|
||||
address: "payments-orchestrator:50062",
|
||||
insecure: true,
|
||||
},
|
||||
{
|
||||
name: "grpcs_scheme",
|
||||
raw: "grpcs://payments-orchestrator:50062",
|
||||
address: "payments-orchestrator:50062",
|
||||
insecure: false,
|
||||
},
|
||||
{
|
||||
name: "dns_scheme",
|
||||
raw: "dns:///ledger:50052",
|
||||
address: "dns:///ledger:50052",
|
||||
insecure: true,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
raw: "ledger",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
endpoint, err := parseDiscoveryInvokeURI(tc.raw)
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for %q", tc.raw)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("parseDiscoveryInvokeURI(%q) failed: %v", tc.raw, err)
|
||||
}
|
||||
if endpoint.address != tc.address {
|
||||
t.Fatalf("expected address %q, got %q", tc.address, endpoint.address)
|
||||
}
|
||||
if endpoint.insecure != tc.insecure {
|
||||
t.Fatalf("expected insecure %t, got %t", tc.insecure, endpoint.insecure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectServiceEndpointPrefersRequiredOperation(t *testing.T) {
|
||||
services := []discovery.ServiceSummary{
|
||||
{
|
||||
ID: "candidate-without-op",
|
||||
Service: "LEDGER",
|
||||
Healthy: true,
|
||||
InvokeURI: "ledger-2:50052",
|
||||
Ops: []string{"balance.read"},
|
||||
},
|
||||
{
|
||||
ID: "candidate-with-op",
|
||||
Service: "LEDGER",
|
||||
Healthy: true,
|
||||
InvokeURI: "ledger-1:50052",
|
||||
Ops: []string{"ledger.debit"},
|
||||
},
|
||||
}
|
||||
|
||||
endpoint, selected, ok := selectServiceEndpoint(services, []string{"LEDGER"}, []string{"ledger.debit"})
|
||||
if !ok {
|
||||
t.Fatal("expected service endpoint to be selected")
|
||||
}
|
||||
if selected.ID != "candidate-with-op" {
|
||||
t.Fatalf("expected candidate-with-op, got %s", selected.ID)
|
||||
}
|
||||
if endpoint.address != "ledger-1:50052" {
|
||||
t.Fatalf("expected address ledger-1:50052, got %s", endpoint.address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectGatewayEndpointPrefersNetworkAndOperation(t *testing.T) {
|
||||
gateways := []discovery.GatewaySummary{
|
||||
{
|
||||
ID: "high-priority-no-op",
|
||||
Rail: "CRYPTO",
|
||||
Network: "TRON_NILE",
|
||||
Healthy: true,
|
||||
InvokeURI: "gw-high:50053",
|
||||
RoutingPriority: 10,
|
||||
},
|
||||
{
|
||||
ID: "low-priority-with-op",
|
||||
Rail: "CRYPTO",
|
||||
Network: "TRON_NILE",
|
||||
Healthy: true,
|
||||
InvokeURI: "gw-low:50053",
|
||||
Ops: []string{"balance.read"},
|
||||
RoutingPriority: 1,
|
||||
},
|
||||
{
|
||||
ID: "different-network",
|
||||
Rail: "CRYPTO",
|
||||
Network: "ARBITRUM_ONE",
|
||||
Healthy: true,
|
||||
InvokeURI: "gw-other:50053",
|
||||
Ops: []string{"balance.read"},
|
||||
RoutingPriority: 100,
|
||||
},
|
||||
}
|
||||
|
||||
endpoint, selected, ok := selectGatewayEndpoint(gateways, "TRON_NILE", []string{"balance.read"})
|
||||
if !ok {
|
||||
t.Fatal("expected gateway endpoint to be selected")
|
||||
}
|
||||
if selected.ID != "low-priority-with-op" {
|
||||
t.Fatalf("expected low-priority-with-op, got %s", selected.ID)
|
||||
}
|
||||
if endpoint.address != "gw-low:50053" {
|
||||
t.Fatalf("expected address gw-low:50053, got %s", endpoint.address)
|
||||
}
|
||||
}
|
||||
149
api/edge/bff/internal/api/middleware.go
Normal file
149
api/edge/bff/internal/api/middleware.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package apiimp
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
cm "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/go-chi/metrics"
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
amr "github.com/tech/sendico/pkg/api/routers"
|
||||
"github.com/tech/sendico/pkg/api/routers/health"
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/messaging"
|
||||
notifications "github.com/tech/sendico/pkg/messaging/notifications/processor"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
wsh "github.com/tech/sendico/server/interface/api/ws"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
"github.com/tech/sendico/server/internal/api/routers"
|
||||
mr "github.com/tech/sendico/server/internal/api/routers/metrics"
|
||||
"github.com/tech/sendico/server/internal/api/ws"
|
||||
"go.uber.org/zap"
|
||||
"moul.io/chizap"
|
||||
)
|
||||
|
||||
type Middleware struct {
|
||||
logger mlogger.Logger
|
||||
router *chi.Mux
|
||||
apiEndpoint string
|
||||
health amr.Health
|
||||
metrics mr.Metrics
|
||||
wshandler ws.Router
|
||||
messaging amr.Messaging
|
||||
epdispatcher *routers.Dispatcher
|
||||
}
|
||||
|
||||
func (mw *Middleware) Handler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) {
|
||||
mw.epdispatcher.Handler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func (mw *Middleware) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) {
|
||||
mw.epdispatcher.AccountHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func (mw *Middleware) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) {
|
||||
mw.epdispatcher.PendingAccountHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func (mw *Middleware) WSHandler(messageType string, handler wsh.HandlerFunc) {
|
||||
mw.wshandler.InstallHandler(messageType, handler)
|
||||
}
|
||||
|
||||
func (mw *Middleware) Consumer(processor notifications.EnvelopeProcessor) error {
|
||||
return mw.messaging.Consumer(processor)
|
||||
}
|
||||
|
||||
func (mw *Middleware) Producer() messaging.Producer {
|
||||
return mw.messaging.Producer()
|
||||
}
|
||||
|
||||
func (mw *Middleware) Messaging() messaging.Register {
|
||||
return mw
|
||||
}
|
||||
|
||||
func (mw *Middleware) Finish() {
|
||||
mw.messaging.Finish()
|
||||
mw.health.Finish()
|
||||
}
|
||||
|
||||
func (mw *Middleware) SetStatus(status health.ServiceStatus) {
|
||||
mw.health.SetStatus(status)
|
||||
}
|
||||
|
||||
func (mw *Middleware) installMiddleware(config *middleware.Config, debug bool) {
|
||||
mw.logger.Debug("Installing middleware stack...")
|
||||
// Collect metrics for all incoming HTTP requests
|
||||
mw.router.Use(metrics.Collector(metrics.CollectorOpts{
|
||||
Host: false, // avoid high-cardinality "host" label
|
||||
Proto: true, // include HTTP protocol label
|
||||
}))
|
||||
mw.router.Use(cm.RequestID)
|
||||
mw.router.Use(cm.RealIP)
|
||||
if debug {
|
||||
mw.router.Use(chizap.New(mw.logger.Named("http_trace"), &chizap.Opts{
|
||||
WithReferer: true,
|
||||
WithUserAgent: true,
|
||||
}))
|
||||
}
|
||||
mw.router.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: config.CORS.AllowedOrigins,
|
||||
AllowedMethods: config.CORS.AllowedMethods,
|
||||
AllowedHeaders: config.CORS.AllowedHeaders,
|
||||
ExposedHeaders: config.CORS.ExposedHeaders,
|
||||
AllowCredentials: config.CORS.AllowCredentials,
|
||||
MaxAge: config.CORS.MaxAge,
|
||||
OptionsPassthrough: false,
|
||||
Debug: debug,
|
||||
}))
|
||||
mw.router.Use(cm.Recoverer)
|
||||
mw.router.Handle("/metrics", metrics.Handler())
|
||||
mw.logger.Info("Middleware stack installation complete")
|
||||
}
|
||||
|
||||
func CreateMiddleware(logger mlogger.Logger, db db.Factory, enforcer auth.Enforcer, router *chi.Mux, config *middleware.Config, debug bool) (*Middleware, error) {
|
||||
p := &Middleware{
|
||||
logger: logger.Named("middleware"),
|
||||
router: router,
|
||||
apiEndpoint: os.Getenv(config.EndPointEnv),
|
||||
}
|
||||
p.logger.Info("Set endpoint", zap.String("endpoint", p.apiEndpoint))
|
||||
p.installMiddleware(config, debug)
|
||||
var err error
|
||||
if p.messaging, err = amr.NewMessagingRouter(p.logger, &config.Messaging); err != nil {
|
||||
p.logger.Error("Failed to create messaging router", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if p.health, err = amr.NewHealthRouter(p.logger, p.router, p.apiEndpoint); err != nil {
|
||||
p.logger.Error("Failed to create healthcheck router", zap.Error(err), zap.String("api_endpoint", p.apiEndpoint))
|
||||
return nil, err
|
||||
}
|
||||
if p.metrics, err = mr.NewMetricsRouter(p.logger, p.router, p.apiEndpoint); err != nil {
|
||||
p.logger.Error("Failed to create metrics router", zap.Error(err), zap.String("api_endpoint", p.apiEndpoint))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
adb, err := db.NewAccountDB()
|
||||
if err != nil {
|
||||
p.logger.Error("Faild to create account database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
rtdb, err := db.NewRefreshTokensDB()
|
||||
if err != nil {
|
||||
p.logger.Error("Faild to create refresh token management database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cdb, err := db.NewVerificationsDB()
|
||||
if err != nil {
|
||||
p.logger.Error("Failed to create confirmations database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.epdispatcher = routers.NewDispatcher(p.logger, p.router, adb, cdb, rtdb, enforcer, config)
|
||||
p.wshandler = ws.NewRouter(p.logger, p.router, &config.WebSocket, p.apiEndpoint)
|
||||
return p, nil
|
||||
}
|
||||
74
api/edge/bff/internal/api/routers/authorized/handler.go
Normal file
74
api/edge/bff/internal/api/routers/authorized/handler.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
emodel "github.com/tech/sendico/server/interface/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type tokenHandlerFunc = func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc
|
||||
|
||||
func (ar *AuthorizedRouter) tokenHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler tokenHandlerFunc) {
|
||||
hndlr := func(r *http.Request) http.HandlerFunc {
|
||||
_, claims, err := jwtauth.FromContext(r.Context())
|
||||
if err != nil {
|
||||
ar.logger.Debug("Authorization failed", zap.Error(err), zap.String("request", r.URL.Path))
|
||||
return response.Unauthorized(ar.logger, ar.service, "credentials required")
|
||||
}
|
||||
t, err := emodel.Claims2Token(claims)
|
||||
if err != nil {
|
||||
ar.logger.Debug("Failed to decode account token", zap.Error(err))
|
||||
return response.BadRequest(ar.logger, ar.service, "credentials_unreadable", "faild to parse credentials")
|
||||
}
|
||||
return handler(r, t)
|
||||
}
|
||||
ar.imp.InstallHandler(service, endpoint, method, hndlr)
|
||||
}
|
||||
|
||||
func (ar *AuthorizedRouter) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) {
|
||||
hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc {
|
||||
if t.Pending {
|
||||
return response.Unauthorized(ar.logger, ar.service, "additional verification required")
|
||||
}
|
||||
var a model.Account
|
||||
if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.AccRef(t.AccountRef))
|
||||
return response.NotFound(ar.logger, ar.service, err.Error())
|
||||
}
|
||||
return response.Internal(ar.logger, ar.service, err)
|
||||
}
|
||||
accessToken, err := ar.imp.CreateAccessToken(&a)
|
||||
if err != nil {
|
||||
ar.logger.Warn("Failed to generate access token", zap.Error(err))
|
||||
return response.Internal(ar.logger, ar.service, err)
|
||||
}
|
||||
return handler(r, &a, &accessToken)
|
||||
}
|
||||
ar.tokenHandler(service, endpoint, method, hndlr)
|
||||
}
|
||||
|
||||
func (ar *AuthorizedRouter) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) {
|
||||
hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc {
|
||||
var a model.Account
|
||||
if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.AccRef(t.AccountRef))
|
||||
return response.NotFound(ar.logger, ar.service, err.Error())
|
||||
}
|
||||
return response.Internal(ar.logger, ar.service, err)
|
||||
}
|
||||
return handler(r, &a, t)
|
||||
}
|
||||
ar.tokenHandler(service, endpoint, method, hndlr)
|
||||
}
|
||||
34
api/edge/bff/internal/api/routers/authorized/router.go
Normal file
34
api/edge/bff/internal/api/routers/authorized/router.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db/account"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
re "github.com/tech/sendico/server/internal/api/routers/endpoint"
|
||||
)
|
||||
|
||||
type AuthorizedRouter struct {
|
||||
logger mlogger.Logger
|
||||
db account.DB
|
||||
imp *re.HttpEndpointRouter
|
||||
service mservice.Type
|
||||
}
|
||||
|
||||
func NewRouter(logger mlogger.Logger, apiEndpoint string, router chi.Router, db account.DB, enforcer auth.Enforcer, config *middleware.TokenConfig, signature *middleware.Signature) *AuthorizedRouter {
|
||||
ja := jwtauth.New(signature.Algorithm, signature.PrivateKey, signature.PublicKey)
|
||||
router.Use(jwtauth.Verifier(ja))
|
||||
router.Use(jwtauth.Authenticator(ja))
|
||||
l := logger.Named("authorized")
|
||||
ar := AuthorizedRouter{
|
||||
logger: l,
|
||||
db: db,
|
||||
imp: re.NewHttpEndpointRouter(l, apiEndpoint, router, config, signature),
|
||||
service: mservice.Accounts,
|
||||
}
|
||||
|
||||
return &ar
|
||||
}
|
||||
55
api/edge/bff/internal/api/routers/dispatcher.go
Normal file
55
api/edge/bff/internal/api/routers/dispatcher.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db/account"
|
||||
"github.com/tech/sendico/pkg/db/refreshtokens"
|
||||
"github.com/tech/sendico/pkg/db/verification"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
rauthorized "github.com/tech/sendico/server/internal/api/routers/authorized"
|
||||
rpublic "github.com/tech/sendico/server/internal/api/routers/public"
|
||||
)
|
||||
|
||||
type Dispatcher struct {
|
||||
logger mlogger.Logger
|
||||
public APIRouter
|
||||
protected ProtectedAPIRouter
|
||||
}
|
||||
|
||||
func (d *Dispatcher) Handler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) {
|
||||
d.public.InstallHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func (d *Dispatcher) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) {
|
||||
d.protected.AccountHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func (d *Dispatcher) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) {
|
||||
d.protected.PendingAccountHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, vdb verification.DB, rtdb refreshtokens.DB, enforcer auth.Enforcer, config *middleware.Config) *Dispatcher {
|
||||
d := &Dispatcher{
|
||||
logger: logger.Named("api_dispatcher"),
|
||||
}
|
||||
|
||||
d.logger.Debug("Installing endpoints middleware...")
|
||||
endpoint := os.Getenv(config.EndPointEnv)
|
||||
signature := middleware.SignatureConf(config)
|
||||
router.Group(func(r chi.Router) {
|
||||
d.public = rpublic.NewRouter(d.logger, endpoint, db, vdb, rtdb, r, &config.Token, &signature)
|
||||
})
|
||||
router.Group(func(r chi.Router) {
|
||||
d.protected = rauthorized.NewRouter(d.logger, endpoint, r, db, enforcer, &config.Token, &signature)
|
||||
})
|
||||
|
||||
return d
|
||||
}
|
||||
36
api/edge/bff/internal/api/routers/endpoint/endpoint.go
Normal file
36
api/edge/bff/internal/api/routers/endpoint/endpoint.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
)
|
||||
|
||||
type (
|
||||
RegistratorT = func(chi.Router, string, http.HandlerFunc)
|
||||
ResponderFunc = func(ctx context.Context, r *http.Request, session *model.SessionIdentifier, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc
|
||||
)
|
||||
|
||||
type HttpEndpointRouter struct {
|
||||
logger mlogger.Logger
|
||||
apiEndpoint string
|
||||
router chi.Router
|
||||
config middleware.TokenConfig
|
||||
signature middleware.Signature
|
||||
}
|
||||
|
||||
func NewHttpEndpointRouter(logger mlogger.Logger, apiEndpoint string, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *HttpEndpointRouter {
|
||||
er := HttpEndpointRouter{
|
||||
logger: logger.Named("http"),
|
||||
apiEndpoint: apiEndpoint,
|
||||
router: router,
|
||||
signature: *signature,
|
||||
config: *config,
|
||||
}
|
||||
return &er
|
||||
}
|
||||
50
api/edge/bff/internal/api/routers/endpoint/install.go
Normal file
50
api/edge/bff/internal/api/routers/endpoint/install.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (er *HttpEndpointRouter) chooseMethod(method api.HTTPMethod) RegistratorT {
|
||||
switch method {
|
||||
case api.Get:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Get(p, h) }
|
||||
case api.Post:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Post(p, h) }
|
||||
case api.Put:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Put(p, h) }
|
||||
case api.Delete:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Delete(p, h) }
|
||||
case api.Patch:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Patch(p, h) }
|
||||
case api.Options:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Options(p, h) }
|
||||
case api.Head:
|
||||
return func(r chi.Router, p string, h http.HandlerFunc) { r.Head(p, h) }
|
||||
default:
|
||||
}
|
||||
er.logger.Error("Unknown method provided", zap.String("method", api.HTTPMethod2String(method)))
|
||||
panic(fmt.Sprintf("Unknown method provided: %d", method))
|
||||
}
|
||||
|
||||
func (er *HttpEndpointRouter) endpoint(service mservice.Type, handler string) string {
|
||||
return path.Join(er.apiEndpoint, service, handler)
|
||||
}
|
||||
|
||||
func (er *HttpEndpointRouter) InstallHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) {
|
||||
ep := er.endpoint(service, endpoint)
|
||||
hm := er.chooseMethod(method)
|
||||
hndlr := func(w http.ResponseWriter, r *http.Request) {
|
||||
res := handler(r)
|
||||
res(w, r)
|
||||
}
|
||||
hm(er.router, ep, hndlr)
|
||||
er.logger.Info("Handler installed", zap.String("endpoint", ep), zap.String("method", api.HTTPMethod2String(method)))
|
||||
}
|
||||
30
api/edge/bff/internal/api/routers/endpoint/token.go
Normal file
30
api/edge/bff/internal/api/routers/endpoint/token.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
emodel "github.com/tech/sendico/server/interface/model"
|
||||
)
|
||||
|
||||
func (er *HttpEndpointRouter) CreateAccessToken(user *model.Account) (sresponse.TokenData, error) {
|
||||
ja := jwtauth.New(er.signature.Algorithm, er.signature.PrivateKey, er.signature.PublicKey)
|
||||
_, res, err := ja.Encode(emodel.Account2Claims(user, er.config.Expiration.Account))
|
||||
token := sresponse.TokenData{
|
||||
Token: res,
|
||||
Expiration: time.Now().Add(time.Duration(er.config.Expiration.Account) * time.Hour),
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (er *HttpEndpointRouter) CreatePendingToken(user *model.Account, ttlMinutes int) (sresponse.TokenData, error) {
|
||||
ja := jwtauth.New(er.signature.Algorithm, er.signature.PrivateKey, er.signature.PublicKey)
|
||||
_, res, err := ja.Encode(emodel.PendingAccount2Claims(user, ttlMinutes))
|
||||
token := sresponse.TokenData{
|
||||
Token: res,
|
||||
Expiration: time.Now().Add(time.Duration(ttlMinutes) * time.Minute),
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
40
api/edge/bff/internal/api/routers/metrics/handler.go
Normal file
40
api/edge/bff/internal/api/routers/metrics/handler.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/metrics"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type metricsRouter struct {
|
||||
logger mlogger.Logger
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (mr *metricsRouter) Finish() {
|
||||
mr.logger.Debug("Stopped")
|
||||
}
|
||||
|
||||
func (mr *metricsRouter) handle(w http.ResponseWriter, r *http.Request) {
|
||||
mr.logger.Debug("Serving metrics request...")
|
||||
mr.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func newMetricsRouter(logger mlogger.Logger, router chi.Router, endpoint string) *metricsRouter {
|
||||
mr := metricsRouter{
|
||||
logger: logger.Named("metrics"),
|
||||
handler: metrics.Handler(),
|
||||
}
|
||||
|
||||
logger.Debug("Installing Prometheus middleware...")
|
||||
router.Group(func(r chi.Router) {
|
||||
ep := endpoint + "/metrics"
|
||||
r.Get(ep, mr.handle)
|
||||
logger.Info("Prometheus handler installed", zap.String("endpoint", ep))
|
||||
})
|
||||
|
||||
return &mr
|
||||
}
|
||||
14
api/edge/bff/internal/api/routers/metrics/router.go
Normal file
14
api/edge/bff/internal/api/routers/metrics/router.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
)
|
||||
|
||||
type Metrics interface {
|
||||
Finish()
|
||||
}
|
||||
|
||||
func NewMetricsRouter(logger mlogger.Logger, router chi.Router, endpoint string) (Metrics, error) {
|
||||
return newMetricsRouter(logger, router, endpoint), nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user