diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index cd92a4c..0000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.gitignore b/.gitignore
index 7ee1fdd..d73c05b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
.env.version
*.pb.go
*.pb.gw.go
+pubspec.lock
+.DS_Store
diff --git a/.woodpecker/bump_version.yml b/.woodpecker/bump_version.yml
index 49e5703..9c13308 100644
--- a/.woodpecker/bump_version.yml
+++ b/.woodpecker/bump_version.yml
@@ -12,8 +12,8 @@ depends_on:
- payments_orchestrator
when:
- - event: push
- branch: main
+ event: push
+ branch: main
steps:
- name: bump-version
@@ -21,7 +21,13 @@ steps:
environment:
GIT_AUTHOR_NAME: woodpecker
GIT_AUTHOR_EMAIL: ci@sendico.io
+ GIT_COMMITTER_NAME: woodpecker
+ GIT_COMMITTER_EMAIL: ci@sendico.io
commands:
- set -euo pipefail
- apk add --no-cache git
+ # make sure git knows who commits
+ - git config user.name "$GIT_AUTHOR_NAME"
+ - git config user.email "$GIT_AUTHOR_EMAIL"
+ # run your script (must do commit + push)
- sh ci/scripts/common/bump_version.sh
diff --git a/.woodpecker/notification.yml b/.woodpecker/notification.yml
index db4133c..1a586f2 100644
--- a/.woodpecker/notification.yml
+++ b/.woodpecker/notification.yml
@@ -5,6 +5,7 @@ matrix:
NOTIFICATION_MONGO_SECRET_PATH: sendico/db
NOTIFICATION_MAIL_SECRET_PATH: sendico/notification/mail
NOTIFICATION_API_SECRET_PATH: sendico/api/endpoint
+ NOTIFICATION_TELEGRAM_SECRET_PATH: sendico/notification/telegram
NOTIFICATION_ENV: prod
when:
diff --git a/api/.DS_Store b/api/.DS_Store
deleted file mode 100644
index eace314..0000000
Binary files a/api/.DS_Store and /dev/null differ
diff --git a/api/billing/fees/go.mod b/api/billing/fees/go.mod
index 1c5aea6..91255a8 100644
--- a/api/billing/fees/go.mod
+++ b/api/billing/fees/go.mod
@@ -10,8 +10,8 @@ require (
github.com/tech/sendico/fx/oracle v0.0.0
github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver v1.17.6
- go.uber.org/zap v1.27.0
- google.golang.org/grpc v1.76.0
+ go.uber.org/zap v1.27.1
+ google.golang.org/grpc v1.77.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -36,7 +36,7 @@ require (
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2 // indirect
- github.com/prometheus/common v0.67.2 // indirect
+ github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
@@ -44,7 +44,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
- golang.org/x/crypto v0.44.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
diff --git a/api/billing/fees/go.sum b/api/billing/fees/go.sum
index b00a4ba..91ffdb4 100644
--- a/api/billing/fees/go.sum
+++ b/api/billing/fees/go.sum
@@ -9,8 +9,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
-github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk=
-github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
@@ -117,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
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.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
-github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
+github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
+github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -154,32 +152,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
-go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
-go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
-go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
-go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
-go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
-go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
-go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
-go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
-golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -216,8 +214,8 @@ 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/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
-google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
-google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
+google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/api/chain/gateway/config.yml b/api/chain/gateway/config.yml
index dbd48ce..05dada2 100644
--- a/api/chain/gateway/config.yml
+++ b/api/chain/gateway/config.yml
@@ -50,8 +50,8 @@ service_wallet:
key_management:
driver: vault
settings:
- address: "https://vault.sendico.io:8200"
- token_env: CHAIN_GATEWAY_VAULT_TOKEN
+ address: "https://vault.sendico.io"
+ token_env: VAULT_TOKEN
namespace: ""
- mount_path: secret
+ mount_path: kv
key_prefix: chain/gateway/wallets
diff --git a/api/chain/gateway/entrypoint.sh b/api/chain/gateway/entrypoint.sh
new file mode 100644
index 0000000..965b66d
--- /dev/null
+++ b/api/chain/gateway/entrypoint.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+set -eu
+
+if [ -n "${VAULT_TOKEN_FILE:-}" ] && [ -f "${VAULT_TOKEN_FILE}" ]; then
+ token="$(cat "${VAULT_TOKEN_FILE}" 2>/dev/null | tr -d '[:space:]')"
+ if [ -n "${token}" ]; then
+ export VAULT_TOKEN="${token}"
+ fi
+fi
+
+if [ -z "${VAULT_TOKEN:-}" ]; then
+ echo "[entrypoint] VAULT_TOKEN is not set; expected Vault Agent sink to write a token to ${VAULT_TOKEN_FILE:-/run/vault/token}" >&2
+fi
+
+exec /app/chain-gateway "$@"
diff --git a/api/chain/gateway/go.mod b/api/chain/gateway/go.mod
index 0aa5d97..be00771 100644
--- a/api/chain/gateway/go.mod
+++ b/api/chain/gateway/go.mod
@@ -14,17 +14,17 @@ require (
github.com/stretchr/testify v1.11.1
github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver v1.17.6
- go.uber.org/zap v1.27.0
- google.golang.org/grpc v1.76.0
+ go.uber.org/zap v1.27.1
+ google.golang.org/grpc v1.77.0
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
- github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f // indirect
+ github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7 // indirect
github.com/beorn7/perks v1.0.1 // indirect
- github.com/bits-and-blooms/bitset v1.24.3 // indirect
+ github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.134.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
@@ -65,7 +65,7 @@ require (
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
- github.com/prometheus/common v0.67.2 // indirect
+ github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
@@ -79,7 +79,7 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
- golang.org/x/crypto v0.44.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
diff --git a/api/chain/gateway/go.sum b/api/chain/gateway/go.sum
index 5654a7d..d7bb493 100644
--- a/api/chain/gateway/go.sum
+++ b/api/chain/gateway/go.sum
@@ -6,19 +6,17 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
-github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f h1:B/TfTw73mVqWKDzJZhU9Qi9wQyYfmiCz9FnmpQsyv5M=
-github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
+github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7 h1:uups37roJCTtR/BrJa0WoMrxt3rzgV+Qrj+TxYyJoAo=
+github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
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/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y=
-github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
+github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
+github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
-github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk=
-github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
@@ -241,8 +239,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
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.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
-github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
+github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
+github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
@@ -277,12 +275,8 @@ github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5
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.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
-github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
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.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
-github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
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/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
@@ -302,32 +296,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
-go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
-go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
-go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
-go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
-go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
-go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
-go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
-go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
-golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -370,8 +364,8 @@ 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/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
-google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
-google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
+google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/api/chain/gateway/internal/keymanager/vault/manager.go b/api/chain/gateway/internal/keymanager/vault/manager.go
index 85f9b37..9e7db59 100644
--- a/api/chain/gateway/internal/keymanager/vault/manager.go
+++ b/api/chain/gateway/internal/keymanager/vault/manager.go
@@ -55,8 +55,8 @@ func New(logger mlogger.Logger, cfg Config) (*Manager, error) {
}
token := strings.TrimSpace(os.Getenv(tokenEnv))
if token == "" {
- logger.Error("vault token env not set", zap.String("env", tokenEnv))
- return nil, merrors.InvalidArgument("vault key manager: token env " + tokenEnv + " is not set")
+ logger.Error("vault token missing; expected Vault Agent to export token", zap.String("env", tokenEnv))
+ return nil, merrors.InvalidArgument("vault key manager: token env " + tokenEnv + " is not set (expected Vault Agent sink to populate it)")
}
mountPath := strings.Trim(strings.TrimSpace(cfg.MountPath), "/")
if mountPath == "" {
diff --git a/api/fx/ingestor/.DS_Store b/api/fx/ingestor/.DS_Store
deleted file mode 100644
index ffdd749..0000000
Binary files a/api/fx/ingestor/.DS_Store and /dev/null differ
diff --git a/api/fx/ingestor/go.mod b/api/fx/ingestor/go.mod
index 21a9959..50ced22 100644
--- a/api/fx/ingestor/go.mod
+++ b/api/fx/ingestor/go.mod
@@ -12,7 +12,7 @@ require (
github.com/prometheus/client_golang v1.23.2
github.com/tech/sendico/fx/storage v0.0.0
github.com/tech/sendico/pkg v0.1.0
- go.uber.org/zap v1.27.0
+ go.uber.org/zap v1.27.1
gopkg.in/yaml.v3 v3.0.1
)
@@ -35,7 +35,7 @@ require (
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
- github.com/prometheus/common v0.67.2 // indirect
+ github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
@@ -44,12 +44,12 @@ require (
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
- golang.org/x/crypto v0.44.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
- google.golang.org/grpc v1.76.0 // indirect
+ google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
diff --git a/api/fx/ingestor/go.sum b/api/fx/ingestor/go.sum
index b1ed329..91ffdb4 100644
--- a/api/fx/ingestor/go.sum
+++ b/api/fx/ingestor/go.sum
@@ -9,8 +9,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
-github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk=
-github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
@@ -117,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
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.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
-github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
+github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
+github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -154,47 +152,41 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
-go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
-go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
-go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
-go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
-go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
-go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
-go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
-go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
-golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
-golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
-golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
-golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
-golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -203,8 +195,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
-golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -213,8 +203,6 @@ 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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
-golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -224,12 +212,10 @@ 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=
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/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
-google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
-google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
+google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/api/fx/oracle/client/client.go b/api/fx/oracle/client/client.go
index 82ab2d2..ca8b6fd 100644
--- a/api/fx/oracle/client/client.go
+++ b/api/fx/oracle/client/client.go
@@ -3,11 +3,11 @@ package client
import (
"context"
"crypto/tls"
- "errors"
"fmt"
"strings"
"time"
+ "github.com/tech/sendico/pkg/merrors"
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
@@ -88,7 +88,7 @@ type oracleClient struct {
func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) {
cfg.setDefaults()
if strings.TrimSpace(cfg.Address) == "" {
- return nil, errors.New("oracle: address is required")
+ return nil, merrors.InvalidArgument("oracle: address is required")
}
dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
@@ -105,7 +105,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...)
if err != nil {
- return nil, fmt.Errorf("oracle: dial %s: %w", cfg.Address, err)
+ return nil, merrors.InternalWrap(err, fmt.Sprintf("oracle: dial %s", cfg.Address))
}
return &oracleClient{
@@ -133,7 +133,7 @@ func (c *oracleClient) Close() error {
func (c *oracleClient) LatestRate(ctx context.Context, req LatestRateParams) (*RateSnapshot, error) {
if req.Pair == nil {
- return nil, errors.New("oracle: pair is required")
+ return nil, merrors.InvalidArgument("oracle: pair is required")
}
callCtx, cancel := c.callContext(ctx)
@@ -145,26 +145,26 @@ func (c *oracleClient) LatestRate(ctx context.Context, req LatestRateParams) (*R
Provider: req.Provider,
})
if err != nil {
- return nil, fmt.Errorf("oracle: latest rate: %w", err)
+ return nil, merrors.InternalWrap(err, "oracle: latest rate")
}
if resp.GetRate() == nil {
- return nil, errors.New("oracle: latest rate: empty payload")
+ return nil, merrors.Internal("oracle: latest rate: empty payload")
}
return fromProtoRate(resp.GetRate()), nil
}
func (c *oracleClient) GetQuote(ctx context.Context, req GetQuoteParams) (*Quote, error) {
if req.Pair == nil {
- return nil, errors.New("oracle: pair is required")
+ return nil, merrors.InvalidArgument("oracle: pair is required")
}
if req.Side == fxv1.Side_SIDE_UNSPECIFIED {
- return nil, errors.New("oracle: side is required")
+ return nil, merrors.InvalidArgument("oracle: side is required")
}
baseSupplied := req.BaseAmount != nil
quoteSupplied := req.QuoteAmount != nil
if baseSupplied == quoteSupplied {
- return nil, errors.New("oracle: exactly one of base_amount or quote_amount must be set")
+ return nil, merrors.InvalidArgument("oracle: exactly one of base_amount or quote_amount must be set")
}
callCtx, cancel := c.callContext(ctx)
@@ -191,10 +191,10 @@ func (c *oracleClient) GetQuote(ctx context.Context, req GetQuoteParams) (*Quote
resp, err := c.client.GetQuote(callCtx, protoReq)
if err != nil {
- return nil, fmt.Errorf("oracle: get quote: %w", err)
+ return nil, merrors.InternalWrap(err, "oracle: get quote")
}
if resp.GetQuote() == nil {
- return nil, errors.New("oracle: get quote: empty payload")
+ return nil, merrors.Internal("oracle: get quote: empty payload")
}
return fromProtoQuote(resp.GetQuote()), nil
}
diff --git a/api/fx/oracle/go.mod b/api/fx/oracle/go.mod
index 5b6e650..b1b2718 100644
--- a/api/fx/oracle/go.mod
+++ b/api/fx/oracle/go.mod
@@ -12,8 +12,8 @@ require (
github.com/tech/sendico/fx/storage v0.0.0
github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver v1.17.6
- go.uber.org/zap v1.27.0
- google.golang.org/grpc v1.76.0
+ go.uber.org/zap v1.27.1
+ google.golang.org/grpc v1.77.0
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
)
@@ -37,7 +37,7 @@ require (
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
- github.com/prometheus/common v0.67.2 // indirect
+ github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
@@ -45,7 +45,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
- golang.org/x/crypto v0.44.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
diff --git a/api/fx/oracle/go.sum b/api/fx/oracle/go.sum
index b00a4ba..91ffdb4 100644
--- a/api/fx/oracle/go.sum
+++ b/api/fx/oracle/go.sum
@@ -9,8 +9,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
-github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk=
-github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
@@ -117,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
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.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
-github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
+github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
+github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -154,32 +152,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
-go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
-go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
-go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
-go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
-go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
-go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
-go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
-go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
-golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -216,8 +214,8 @@ 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/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
-google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
-google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
+google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/api/fx/storage/go.mod b/api/fx/storage/go.mod
index 8dd247c..7b1bf9b 100644
--- a/api/fx/storage/go.mod
+++ b/api/fx/storage/go.mod
@@ -7,7 +7,7 @@ replace github.com/tech/sendico/pkg => ../../pkg
require (
github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver v1.17.6
- go.uber.org/zap v1.27.0
+ go.uber.org/zap v1.27.1
)
require (
@@ -25,7 +25,7 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
- golang.org/x/crypto v0.44.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
diff --git a/api/fx/storage/go.sum b/api/fx/storage/go.sum
index bab4975..170b262 100644
--- a/api/fx/storage/go.sum
+++ b/api/fx/storage/go.sum
@@ -7,8 +7,6 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
-github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk=
-github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
@@ -122,28 +120,26 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
-go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
-go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
-go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
-go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
+go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
-golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
-golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
-golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -151,8 +147,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
-golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -160,16 +154,14 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
-golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.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/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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
-golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/api/ledger/client/client.go b/api/ledger/client/client.go
index f71927c..3b0671a 100644
--- a/api/ledger/client/client.go
+++ b/api/ledger/client/client.go
@@ -3,11 +3,11 @@ package client
import (
"context"
"crypto/tls"
- "errors"
"fmt"
"strings"
"time"
+ "github.com/tech/sendico/pkg/merrors"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
@@ -48,7 +48,7 @@ type ledgerClient struct {
func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) {
cfg.setDefaults()
if strings.TrimSpace(cfg.Address) == "" {
- return nil, errors.New("ledger: address is required")
+ return nil, merrors.InvalidArgument("ledger: address is required")
}
dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
@@ -65,7 +65,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...)
if err != nil {
- return nil, fmt.Errorf("ledger: dial %s: %w", cfg.Address, err)
+ return nil, merrors.InternalWrap(err, fmt.Sprintf("ledger: dial %s", cfg.Address))
}
return &ledgerClient{
diff --git a/api/ledger/go.mod b/api/ledger/go.mod
index c015888..9bf4afe 100644
--- a/api/ledger/go.mod
+++ b/api/ledger/go.mod
@@ -10,8 +10,8 @@ require (
github.com/stretchr/testify v1.11.1
github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver v1.17.6
- go.uber.org/zap v1.27.0
- google.golang.org/grpc v1.76.0
+ go.uber.org/zap v1.27.1
+ google.golang.org/grpc v1.77.0
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
)
@@ -38,7 +38,7 @@ require (
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
- github.com/prometheus/common v0.67.2 // indirect
+ github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
@@ -46,7 +46,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
- golang.org/x/crypto v0.44.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
diff --git a/api/ledger/go.sum b/api/ledger/go.sum
index 7fa6056..63f1b50 100644
--- a/api/ledger/go.sum
+++ b/api/ledger/go.sum
@@ -9,8 +9,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
-github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk=
-github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
@@ -117,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
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.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
-github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
+github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
+github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -156,32 +154,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
-go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
-go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
-go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
-go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
-go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
-go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
-go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
-go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
-golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -218,8 +216,8 @@ 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/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
-google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
-google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
+google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/api/ledger/internal/service/ledger/helpers.go b/api/ledger/internal/service/ledger/helpers.go
index 63baf19..de0b9b8 100644
--- a/api/ledger/internal/service/ledger/helpers.go
+++ b/api/ledger/internal/service/ledger/helpers.go
@@ -146,7 +146,7 @@ func calculateBalance(lines []*model.PostingLine) (decimal.Decimal, error) {
for _, line := range lines {
amount, err := parseDecimal(line.Amount)
if err != nil {
- return decimal.Zero, fmt.Errorf("invalid line amount: %w", err)
+ return decimal.Zero, merrors.InvalidArgumentWrap(err, "invalid line amount")
}
balance = balance.Add(amount)
}
diff --git a/api/ledger/internal/service/ledger/outbox_publisher.go b/api/ledger/internal/service/ledger/outbox_publisher.go
index 30a3a3d..bad4baf 100644
--- a/api/ledger/internal/service/ledger/outbox_publisher.go
+++ b/api/ledger/internal/service/ledger/outbox_publisher.go
@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/ledger/storage"
ledgerModel "github.com/tech/sendico/ledger/storage/model"
+ "github.com/tech/sendico/pkg/merrors"
pmessaging "github.com/tech/sendico/pkg/messaging"
me "github.com/tech/sendico/pkg/messaging/envelope"
"github.com/tech/sendico/pkg/mlogger"
@@ -126,7 +127,7 @@ func (p *outboxPublisher) dispatchPending(ctx context.Context) (int, error) {
func (p *outboxPublisher) publishEvent(_ context.Context, event *ledgerModel.OutboxEvent) error {
docID := event.GetID()
if docID == nil || docID.IsZero() {
- return errors.New("outbox event missing identifier")
+ return merrors.InvalidArgument("outbox event missing identifier")
}
payload, err := p.wrapPayload(event)
@@ -157,7 +158,7 @@ func (p *outboxPublisher) wrapPayload(event *ledgerModel.OutboxEvent) ([]byte, e
func (p *outboxPublisher) markSent(ctx context.Context, event *ledgerModel.OutboxEvent) error {
eventRef := event.GetID()
if eventRef == nil || eventRef.IsZero() {
- return errors.New("outbox event missing identifier")
+ return merrors.InvalidArgument("outbox event missing identifier")
}
return p.store.MarkSent(ctx, *eventRef, time.Now().UTC())
diff --git a/api/ledger/internal/service/ledger/queries.go b/api/ledger/internal/service/ledger/queries.go
index e59e8dd..a61ea9f 100644
--- a/api/ledger/internal/service/ledger/queries.go
+++ b/api/ledger/internal/service/ledger/queries.go
@@ -249,15 +249,15 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat
func parseCursor(cursor string) (int, error) {
decoded, err := base64.StdEncoding.DecodeString(cursor)
if err != nil {
- return 0, fmt.Errorf("invalid base64: %w", err)
+ return 0, merrors.InvalidArgumentWrap(err, "invalid cursor base64 encoding")
}
parts := strings.Split(string(decoded), ":")
if len(parts) != 2 || parts[0] != "offset" {
- return 0, fmt.Errorf("invalid cursor format")
+ return 0, merrors.InvalidArgument("invalid cursor format")
}
offset, err := strconv.Atoi(parts[1])
if err != nil {
- return 0, fmt.Errorf("invalid offset: %w", err)
+ return 0, merrors.InvalidArgumentWrap(err, "invalid cursor offset")
}
return offset, nil
}
diff --git a/api/ledger/internal/service/ledger/service.go b/api/ledger/internal/service/ledger/service.go
index ce9c4cb..3763481 100644
--- a/api/ledger/internal/service/ledger/service.go
+++ b/api/ledger/internal/service/ledger/service.go
@@ -18,6 +18,7 @@ import (
"github.com/tech/sendico/ledger/storage"
"github.com/tech/sendico/pkg/api/routers"
+ "github.com/tech/sendico/pkg/merrors"
pmessaging "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
@@ -241,10 +242,10 @@ func (s *Service) quoteFees(ctx context.Context, trigger feesv1.Trigger, organiz
return nil, nil
}
if strings.TrimSpace(organizationRef) == "" {
- return nil, fmt.Errorf("organization reference is required to quote fees")
+ return nil, merrors.InvalidArgument("organization reference is required to quote fees")
}
if baseAmount == nil {
- return nil, fmt.Errorf("base amount is required to quote fees")
+ return nil, merrors.InvalidArgument("base amount is required to quote fees")
}
amountCopy := &moneyv1.Money{Amount: baseAmount.GetAmount(), Currency: baseAmount.GetCurrency()}
@@ -309,11 +310,11 @@ func convertFeeDerivedLines(lines []*feesv1.DerivedPostingLine) ([]*ledgerv1.Pos
continue
}
if line.GetMoney() == nil {
- return nil, fmt.Errorf("fee line %d missing money", idx)
+ return nil, merrors.Internal(fmt.Sprintf("fee line %d missing money", idx))
}
dec, err := decimal.NewFromString(line.GetMoney().GetAmount())
if err != nil {
- return nil, fmt.Errorf("fee line %d invalid amount: %w", idx, err)
+ return nil, merrors.InternalWrap(err, fmt.Sprintf("fee line %d invalid amount", idx))
}
dec = ensureAmountForSide(dec, line.GetSide())
posting := &ledgerv1.PostingLine{
diff --git a/api/notification/.DS_Store b/api/notification/.DS_Store
deleted file mode 100644
index c82e4d9..0000000
Binary files a/api/notification/.DS_Store and /dev/null differ
diff --git a/api/notification/config.yml b/api/notification/config.yml
index bbbd57b..deae90f 100755
--- a/api/notification/config.yml
+++ b/api/notification/config.yml
@@ -53,14 +53,21 @@ api:
password_env: MAIL_SECRET
host: "smtp.mail.ru"
port: 465
- from: "MeetX Tech"
+ from: "Sendico Tech"
network_timeout: 10
+ telegram:
+ bot_token_env: TELEGRAM_BOT_TOKEN
+ chat_id_env: TELEGRAM_CHAT_ID
+ thread_id_env: TELEGRAM_THREAD_ID
+ api_url: "https://api.telegram.org"
+ timeout_seconds: 10
+ parse_mode: markdown
localizer:
path: "./i18n"
languages: ["en", "ru", "uk"]
service_name: "Sendico"
- support: "support@meetx.space"
+ support: "support@sendico.io"
app:
@@ -82,4 +89,4 @@ database:
collection_name_env: PERMISSION_COLLECTION
database_name_env: MONGO_DATABASE
timeout_seconds_env: PERMISSION_TIMEOUT
- is_filtered_env: PERMISSION_IS_FILTERED
\ No newline at end of file
+ is_filtered_env: PERMISSION_IS_FILTERED
diff --git a/api/notification/go.mod b/api/notification/go.mod
index 89a3e9f..7d313cb 100644
--- a/api/notification/go.mod
+++ b/api/notification/go.mod
@@ -13,7 +13,7 @@ require (
github.com/tech/sendico/pkg v0.1.0
github.com/xhit/go-simple-mail/v2 v2.16.0
go.mongodb.org/mongo-driver v1.17.6
- go.uber.org/zap v1.27.0
+ go.uber.org/zap v1.27.1
golang.org/x/text v0.31.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -38,7 +38,7 @@ require (
github.com/nats-io/nuid v1.0.1 // 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.2 // indirect
+ github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
@@ -48,11 +48,11 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
- golang.org/x/crypto v0.44.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
- google.golang.org/grpc v1.76.0 // indirect
+ google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
diff --git a/api/notification/go.sum b/api/notification/go.sum
index ff04c50..5ae62e8 100644
--- a/api/notification/go.sum
+++ b/api/notification/go.sum
@@ -13,8 +13,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
-github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk=
-github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
@@ -123,8 +121,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
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.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
-github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
+github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
+github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -169,32 +167,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
-go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
-go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
-go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
-go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
-go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
-go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
-go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
-go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
-golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -231,8 +229,8 @@ 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/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
-google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
-google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
+google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/api/notification/i18n/en.json b/api/notification/i18n/en.json
index 0cc34ad..24afb54 100644
--- a/api/notification/i18n/en.json
+++ b/api/notification/i18n/en.json
@@ -20,6 +20,10 @@
"mail.reset-password.greeting": "Hi, {{.Name}}",
"mail.reset-password.body": "It looks like you requested a new password.
If that sounds right, you can enter a new password by clicking the followinglink<\/a>.
If you have not requested a passowrd change please contact us under {{.SupportMail}}<\/a>.",
+ "mail.confirmation-code.subj": "{{.ServiceName}}: your confirmation code",
+ "mail.confirmation-code.greeting": "Hi, {{.Name}}",
+ "mail.confirmation-code.body": "Use this code to complete your {{.Target}} request: {{.Code}}<\/b>. This code expires soon.",
+
"mail.email-verification.subj": "{{.ServiceName}}: verify your email address",
"mail.email-verification.greeting": "Hi, {{.Name}}",
"mail.email-verification.body": "It looks like you have changed your email address.
If that sounds right, verify your email address by clicking the following link<\/a>.
If you have not changed your email address please contact us under {{.SupportMail}}<\/a>.",
diff --git a/api/notification/i18n/ru.json b/api/notification/i18n/ru.json
index b7f28a6..0d42683 100644
--- a/api/notification/i18n/ru.json
+++ b/api/notification/i18n/ru.json
@@ -21,6 +21,10 @@
"mail.reset-password.greeting": "День добрый, {{.Name}}",
"mail.reset-password.body": "От вашего имени пришел запрос на сброс пароля.
Если этот запрос отправили вы, то подтвердите сброс переходом по ссылке<\/a>.
Если этот запрос отправили не вы, то дайте нам знать по адресу электронной почты {{.SupportMail}}<\/a>",
+ "mail.confirmation-code.subj": "{{.ServiceName}}: ваш код подтверждения",
+ "mail.confirmation-code.greeting": "День добрый, {{.Name}}",
+ "mail.confirmation-code.body": "Используйте этот код, чтобы завершить запрос {{.Target}}: {{.Code}}<\/b>. Код скоро истекает.",
+
"mail.email-verification.subj": "{{.ServiceName}}: подтвердите ваш адрес электронной почты",
"mail.email-verification.greeting": "День добрый, {{.Name}}",
"mail.email-verification.body": "От вашего имени пришел запрос на смену адреса электронной почты.
Если этот запрос отправили вы, то подтвердите смену переходом по ссылке <\/a>.
Если этот запрос отправили не вы, то дайте нам знать по адресу электронной почты {{.SupportMail}}<\/a>",
@@ -58,4 +62,4 @@
"___file_trailer": ""
-}
\ No newline at end of file
+}
diff --git a/api/notification/i18n/uk.json b/api/notification/i18n/uk.json
index 8771068..e19651c 100644
--- a/api/notification/i18n/uk.json
+++ b/api/notification/i18n/uk.json
@@ -20,6 +20,10 @@
"mail.reset-password.greeting": "День добрий, {{.Name}}",
"mail.reset-password.body": "Від вашого імені прийшов запит на скидання пароля.
Якщо цей запит відправили ви, то підтвердіть скидання переходом по посилання<\/a>.
>
Якщо цей запит надіслали не ви, дайте нам знати за адресою електронної пошти {{.SupportMail}}<\/a>",
+ "mail.confirmation-code.subj": "{{.ServiceName}}: ваш код підтвердження",
+ "mail.confirmation-code.greeting": "День добрий, {{.Name}}",
+ "mail.confirmation-code.body": "Використайте цей код, щоб завершити запит {{.Target}}: {{.Code}}<\/b>. Код скоро спливає.",
+
"mail.email-verification.subj": "{{.ServiceName}}: Перевірте свою адресу електронної пошти",
"mail.email-verification.greeting": "День добрий, {{.Name}}",
"mail.email-verification.body": "Від вашого імені надійшов запит на адресу електронної пошти.
Якщо цей запит надіслали, то підтвердьте зміну переходом за адресою<\/a>.
Якщо цей запит відправили не ви, то дайте нам знати на адресу електронної пошти {{.SupportMail}}<\/a>",
diff --git a/api/notification/interface/services/notification/config/config.go b/api/notification/interface/services/notification/config/config.go
index 2e34fd9..61d576c 100644
--- a/api/notification/interface/services/notification/config/config.go
+++ b/api/notification/interface/services/notification/config/config.go
@@ -1,6 +1,16 @@
package notificationimp
type Config struct {
- Driver string `yaml:"driver"`
- Settings map[string]any `yaml:"settings,omitempty"`
+ Driver string `yaml:"driver"`
+ Settings map[string]any `yaml:"settings,omitempty"`
+ Telegram *TelegramConfig `yaml:"telegram"`
+}
+
+type TelegramConfig struct {
+ BotTokenEnv string `yaml:"bot_token_env"`
+ ChatIDEnv string `yaml:"chat_id_env"`
+ ThreadIDEnv string `yaml:"thread_id_env,omitempty"`
+ APIURL string `yaml:"api_url,omitempty"`
+ ParseMode string `yaml:"parse_mode,omitempty"`
+ TimeoutSeconds int `yaml:"timeout_seconds"`
}
diff --git a/api/notification/internal/server/notificationimp/confcode.go b/api/notification/internal/server/notificationimp/confcode.go
new file mode 100644
index 0000000..bccf5ee
--- /dev/null
+++ b/api/notification/internal/server/notificationimp/confcode.go
@@ -0,0 +1,27 @@
+package notificationimp
+
+import (
+ "context"
+ "strings"
+
+ "github.com/tech/sendico/pkg/model"
+ "go.uber.org/zap"
+)
+
+func (a *NotificationAPI) onConfirmationCode(ctx context.Context, account *model.Account, destination string, target model.ConfirmationTarget, code string) error {
+ builder := a.client.MailBuilder().
+ AddRecipient(account.Name, strings.TrimSpace(destination)).
+ SetAccountID(account.ID.Hex()).
+ SetLocale(account.Locale).
+ SetTemplateID("confirmation-code").
+ AddData("Name", account.Name).
+ AddData("Code", code).
+ AddData("Target", string(target))
+
+ if err := a.client.Send(builder); err != nil {
+ a.logger.Warn("Failed to send confirmation code email", zap.Error(err), zap.String("login", account.Login))
+ return err
+ }
+ a.logger.Info("Confirmation code email sent", zap.String("login", account.Login), zap.String("destination", destination), zap.String("target", string(target)))
+ return nil
+}
diff --git a/api/notification/internal/server/notificationimp/mail/internal/builder/builder.go b/api/notification/internal/server/notificationimp/mail/internal/builder/builder.go
index a729130..b486c22 100644
--- a/api/notification/internal/server/notificationimp/mail/internal/builder/builder.go
+++ b/api/notification/internal/server/notificationimp/mail/internal/builder/builder.go
@@ -42,7 +42,7 @@ func (mb *MessageBuilderImp) AddData(key, value string) mmail.MailBuilder {
func (mb *MessageBuilderImp) Build() (mmail.Message, error) {
if len(mb.message.recipients) == 0 {
- return nil, merrors.InvalidArgument("Recipient not set")
+ return nil, merrors.InvalidArgument("Recipient not set", "recipients")
}
return mb.message, nil
}
diff --git a/api/notification/internal/server/notificationimp/mail/internal/mailimp.go b/api/notification/internal/server/notificationimp/mail/internal/mailimp.go
index 87f2d32..65a6df4 100755
--- a/api/notification/internal/server/notificationimp/mail/internal/mailimp.go
+++ b/api/notification/internal/server/notificationimp/mail/internal/mailimp.go
@@ -69,7 +69,7 @@ func (c *Client) Send(r mmail.MailBuilder) error {
c.logger.Warn("Malformed messge", zap.String("template_id", m.TemplateID()),
zap.String("locale", m.Locale()), zap.Strings("recipients", m.Recipients()),
zap.Int("body_size", len(body)))
- return merrors.InvalidArgument("malformed message")
+ return merrors.InvalidArgument("malformed message", "message.body", "message.recipients")
}
subj, err := mailkey.Subject(c.l, m.Parameters(), m.TemplateID(), m.Locale())
if err != nil {
diff --git a/api/notification/internal/server/notificationimp/mail/mail.go b/api/notification/internal/server/notificationimp/mail/mail.go
index 6cacb60..12f616d 100644
--- a/api/notification/internal/server/notificationimp/mail/mail.go
+++ b/api/notification/internal/server/notificationimp/mail/mail.go
@@ -1,6 +1,7 @@
package mail
import (
+ "github.com/mitchellh/mapstructure"
"github.com/tech/sendico/notification/interface/api/localizer"
notification "github.com/tech/sendico/notification/interface/services/notification/config"
mi "github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal"
@@ -9,7 +10,6 @@ import (
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
- "github.com/mitchellh/mapstructure"
"go.uber.org/zap"
)
@@ -22,7 +22,7 @@ type Config = notification.Config
func createMailClient(logger mlogger.Logger, producer messaging.Producer, l localizer.Localizer, dp domainprovider.DomainProvider, config *Config) (Client, error) {
if len(config.Driver) == 0 {
- return nil, merrors.InvalidArgument("Mail driver name must be provided")
+ return nil, merrors.InvalidArgument("Mail driver name must be provided", "config.driver")
}
logger.Info("Connecting mail client...", zap.String("driver", config.Driver))
if config.Driver == "dummy" {
@@ -45,7 +45,7 @@ func createMailClient(logger mlogger.Logger, producer messaging.Producer, l loca
return mi.NewClient(logger, l, dp, &gsmconfing), nil
}
- return nil, merrors.InvalidArgument("Unkwnown mail driver: " + config.Driver)
+ return nil, merrors.InvalidArgument("Unkwnown mail driver: "+config.Driver, "config.driver")
}
func CreateMailClient(logger mlogger.Logger, sender string, producer messaging.Producer, l localizer.Localizer, dp domainprovider.DomainProvider, config *Config) (Client, error) {
diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go
index e6d8f5b..4f81929 100644
--- a/api/notification/internal/server/notificationimp/notification.go
+++ b/api/notification/internal/server/notificationimp/notification.go
@@ -5,10 +5,15 @@ import (
"github.com/tech/sendico/notification/interface/api"
mmail "github.com/tech/sendico/notification/internal/server/notificationimp/mail"
+ "github.com/tech/sendico/notification/internal/server/notificationimp/telegram"
"github.com/tech/sendico/pkg/domainprovider"
+ "github.com/tech/sendico/pkg/merrors"
na "github.com/tech/sendico/pkg/messaging/notifications/account"
+ cnotifications "github.com/tech/sendico/pkg/messaging/notifications/confirmation"
ni "github.com/tech/sendico/pkg/messaging/notifications/invitation"
+ snotifications "github.com/tech/sendico/pkg/messaging/notifications/site"
"github.com/tech/sendico/pkg/mlogger"
+ "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"go.uber.org/zap"
)
@@ -17,6 +22,7 @@ type NotificationAPI struct {
logger mlogger.Logger
client mmail.Client
dp domainprovider.DomainProvider
+ tg telegram.Client
}
func (a *NotificationAPI) Name() mservice.Type {
@@ -33,11 +39,22 @@ func CreateAPI(a api.API) (*NotificationAPI, error) {
}
p.logger = a.Logger().Named(p.Name())
+ if a.Config().Notification == nil {
+ return nil, merrors.InvalidArgument("notification configuration is missing", "config.notification")
+ }
+ if a.Config().Notification.Telegram == nil {
+ return nil, merrors.InvalidArgument("telegram configuration is missing", "config.notification.telegram")
+ }
+
var err error
if p.client, err = mmail.CreateMailClient(p.logger.Named("mailer"), p.Name(), a.Register().Producer(), a.Localizer(), a.DomainProvider(), a.Config().Notification); err != nil {
p.logger.Error("Failed to create mail connection", zap.Error(err), zap.String("driver", a.Config().Notification.Driver))
return nil, err
}
+ if p.tg, err = telegram.NewClient(p.logger.Named("telegram"), a.Config().Notification.Telegram); err != nil {
+ p.logger.Error("Failed to create telegram client", zap.Error(err))
+ return nil, err
+ }
db, err := a.DBFactory().NewAccountDB()
if err != nil {
@@ -54,6 +71,11 @@ func CreateAPI(a api.API) (*NotificationAPI, error) {
return nil, err
}
+ if err := a.Register().Consumer(cnotifications.NewConfirmationCodeProcessor(p.logger, db, p.onConfirmationCode)); err != nil {
+ p.logger.Error("Failed to create confirmation code handler", zap.Error(err))
+ return nil, err
+ }
+
idb, err := a.DBFactory().NewInvitationsDB()
if err != nil {
p.logger.Error("Failed to create invitation db connection", zap.Error(err))
@@ -64,5 +86,46 @@ func CreateAPI(a api.API) (*NotificationAPI, error) {
return nil, err
}
+ if err := a.Register().Consumer(snotifications.NewSiteRequestProcessor(p.logger, p.onDemoRequest, p.onContactRequest, p.onCallRequest)); err != nil {
+ p.logger.Error("Failed to register site request handler", zap.Error(err))
+ return nil, err
+ }
+
return p, nil
}
+
+func (a *NotificationAPI) onDemoRequest(ctx context.Context, request *model.DemoRequest) error {
+ if a.tg == nil {
+ return merrors.Internal("telegram client is not configured")
+ }
+ if err := a.tg.SendDemoRequest(ctx, request); err != nil {
+ a.logger.Warn("Failed to send demo request via telegram", zap.Error(err))
+ return err
+ }
+ a.logger.Info("Demo request sent via Telegram", zap.String("name", request.Name), zap.String("organization", request.OrganizationName))
+ return nil
+}
+
+func (a *NotificationAPI) onContactRequest(ctx context.Context, request *model.ContactRequest) error {
+ if a.tg == nil {
+ return merrors.Internal("telegram client is not configured")
+ }
+ if err := a.tg.SendContactRequest(ctx, request); err != nil {
+ a.logger.Warn("Failed to send contact request via telegram", zap.Error(err))
+ return err
+ }
+ a.logger.Info("Contact request sent via Telegram", zap.String("name", request.Name), zap.String("topic", request.Topic))
+ return nil
+}
+
+func (a *NotificationAPI) onCallRequest(ctx context.Context, request *model.CallRequest) error {
+ if a.tg == nil {
+ return merrors.Internal("telegram client is not configured")
+ }
+ if err := a.tg.SendCallRequest(ctx, request); err != nil {
+ a.logger.Warn("Failed to send call request via telegram", zap.Error(err))
+ return err
+ }
+ a.logger.Info("Call request sent via Telegram", zap.String("phone", request.Phone))
+ return nil
+}
diff --git a/api/notification/internal/server/notificationimp/telegram/call.go b/api/notification/internal/server/notificationimp/telegram/call.go
new file mode 100644
index 0000000..7a445c4
--- /dev/null
+++ b/api/notification/internal/server/notificationimp/telegram/call.go
@@ -0,0 +1,18 @@
+package telegram
+
+import "github.com/tech/sendico/pkg/model"
+
+func newCallRequestTemplate(request *model.CallRequest) messageTemplate {
+ return messageTemplate{
+ title: "New call request received",
+ emphasize: []string{"call request"},
+ fields: []messageField{
+ {label: "Name", value: request.Name},
+ {label: "Phone", value: request.Phone},
+ {label: "Email", value: request.Email},
+ {label: "Company", value: request.Company},
+ {label: "Preferred time", value: request.PreferredTime},
+ {label: "Message", value: request.Message},
+ },
+ }
+}
diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go
new file mode 100644
index 0000000..aabc0eb
--- /dev/null
+++ b/api/notification/internal/server/notificationimp/telegram/client.go
@@ -0,0 +1,182 @@
+package telegram
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ notconfig "github.com/tech/sendico/notification/interface/services/notification/config"
+ "github.com/tech/sendico/pkg/merrors"
+ "github.com/tech/sendico/pkg/mlogger"
+ "github.com/tech/sendico/pkg/model"
+ "go.uber.org/zap"
+)
+
+const defaultAPIURL = "https://api.telegram.org"
+
+type Client interface {
+ SendDemoRequest(ctx context.Context, request *model.DemoRequest) error
+ SendContactRequest(ctx context.Context, request *model.ContactRequest) error
+ SendCallRequest(ctx context.Context, request *model.CallRequest) error
+}
+
+type client struct {
+ logger mlogger.Logger
+ httpClient *http.Client
+ apiURL string
+ botToken string
+ chatID string
+ threadID *int64
+ parseMode parseMode
+}
+
+type sendMessagePayload struct {
+ ChatID string `json:"chat_id"`
+ Text string `json:"text"`
+ ParseMode string `json:"parse_mode,omitempty"`
+ ThreadID *int64 `json:"message_thread_id,omitempty"`
+ DisablePreview bool `json:"disable_web_page_preview,omitempty"`
+ DisableNotify bool `json:"disable_notification,omitempty"`
+ ProtectContent bool `json:"protect_content,omitempty"`
+}
+
+func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, error) {
+ if cfg == nil {
+ return nil, merrors.InvalidArgument("telegram configuration is not provided", "config.notification.telegram")
+ }
+ token := strings.TrimSpace(os.Getenv(cfg.BotTokenEnv))
+ if token == "" {
+ return nil, merrors.InvalidArgument(fmt.Sprintf("telegram bot token env %s is empty", cfg.BotTokenEnv), cfg.BotTokenEnv)
+ }
+ chatID := strings.TrimSpace(os.Getenv(cfg.ChatIDEnv))
+ if chatID == "" {
+ return nil, merrors.InvalidArgument(fmt.Sprintf("telegram chat id env %s is empty", cfg.ChatIDEnv), cfg.ChatIDEnv)
+ }
+
+ var threadID *int64
+ if env := strings.TrimSpace(cfg.ThreadIDEnv); env != "" {
+ raw := strings.TrimSpace(os.Getenv(env))
+ if raw != "" {
+ val, err := strconv.ParseInt(raw, 10, 64)
+ if err != nil {
+ return nil, merrors.InvalidArgumentWrap(err, fmt.Sprintf("telegram thread id env %s is invalid", env), env)
+ }
+ threadID = &val
+ }
+ }
+
+ timeout := time.Duration(cfg.TimeoutSeconds) * time.Second
+ if timeout <= 0 {
+ timeout = 10 * time.Second
+ }
+
+ apiURL := strings.TrimSpace(cfg.APIURL)
+ if apiURL == "" {
+ apiURL = defaultAPIURL
+ }
+ mode := normalizeParseMode(cfg.ParseMode)
+ if mode == parseModeUnset {
+ mode = parseModeMarkdown
+ }
+
+ return &client{
+ logger: logger.Named("telegram"),
+ httpClient: &http.Client{
+ Timeout: timeout,
+ },
+ apiURL: strings.TrimRight(apiURL, "/"),
+ botToken: token,
+ chatID: chatID,
+ threadID: threadID,
+ parseMode: mode,
+ }, nil
+}
+
+func (c *client) SendDemoRequest(ctx context.Context, request *model.DemoRequest) error {
+ if request == nil {
+ return merrors.InvalidArgument("demo request payload is nil", "request")
+ }
+ return c.sendForm(ctx, newDemoRequestTemplate(request))
+}
+
+func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) error {
+ body, err := json.Marshal(&payload)
+ if err != nil {
+ c.logger.Warn("Failed to marshal telegram payload", zap.Error(err))
+ return err
+ }
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint(), bytes.NewReader(body))
+ if err != nil {
+ c.logger.Warn("Failed to create telegram request", zap.Error(err))
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ fields := []zap.Field{
+ zap.String("chat_id", payload.ChatID),
+ zap.Int("payload_size_bytes", len(body)),
+ zap.Bool("disable_preview", payload.DisablePreview),
+ zap.Bool("disable_notification", payload.DisableNotify),
+ zap.Bool("protect_content", payload.ProtectContent),
+ }
+ if payload.ThreadID != nil {
+ fields = append(fields, zap.Int64("thread_id", *payload.ThreadID))
+ }
+ c.logger.Debug("Sending Telegram message", fields...)
+ start := time.Now()
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ c.logger.Warn("Telegram request failed", zap.Error(err))
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
+ c.logger.Debug("Telegram message sent", zap.Int("status_code", resp.StatusCode), zap.Duration("latency", time.Since(start)))
+ return nil
+ }
+ respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
+ c.logger.Warn("Telegram API returned non-success status",
+ zap.Int("status_code", resp.StatusCode),
+ zap.ByteString("response_body", respBody),
+ zap.String("chat_id", c.chatID))
+ return merrors.Internal(fmt.Sprintf("telegram sendMessage failed with status %d: %s", resp.StatusCode, string(respBody)))
+}
+
+func (c *client) endpoint() string {
+ return fmt.Sprintf("%s/bot%s/sendMessage", c.apiURL, c.botToken)
+}
+
+func (c *client) SendContactRequest(ctx context.Context, request *model.ContactRequest) error {
+ if request == nil {
+ return merrors.InvalidArgument("contact request payload is nil", "request")
+ }
+ return c.sendForm(ctx, newContactRequestTemplate(request))
+}
+
+func (c *client) SendCallRequest(ctx context.Context, request *model.CallRequest) error {
+ if request == nil {
+ return merrors.InvalidArgument("call request payload is nil", "request")
+ }
+ return c.sendForm(ctx, newCallRequestTemplate(request))
+}
+
+func (c *client) sendForm(ctx context.Context, template messageTemplate) error {
+ message := template.Format(c.parseMode)
+ payload := sendMessagePayload{
+ ChatID: c.chatID,
+ Text: message,
+ ParseMode: c.parseMode.String(),
+ ThreadID: c.threadID,
+ DisablePreview: true,
+ }
+ return c.sendMessage(ctx, payload)
+}
diff --git a/api/notification/internal/server/notificationimp/telegram/contact.go b/api/notification/internal/server/notificationimp/telegram/contact.go
new file mode 100644
index 0000000..fc73d22
--- /dev/null
+++ b/api/notification/internal/server/notificationimp/telegram/contact.go
@@ -0,0 +1,18 @@
+package telegram
+
+import "github.com/tech/sendico/pkg/model"
+
+func newContactRequestTemplate(request *model.ContactRequest) messageTemplate {
+ return messageTemplate{
+ title: "New site request received",
+ emphasize: []string{"site request"},
+ fields: []messageField{
+ {label: "Name", value: request.Name},
+ {label: "Email", value: request.Email},
+ {label: "Phone", value: request.Phone},
+ {label: "Company", value: request.Company},
+ {label: "Topic", value: request.Topic},
+ {label: "Message", value: request.Message},
+ },
+ }
+}
diff --git a/api/notification/internal/server/notificationimp/telegram/demo.go b/api/notification/internal/server/notificationimp/telegram/demo.go
new file mode 100644
index 0000000..3965f98
--- /dev/null
+++ b/api/notification/internal/server/notificationimp/telegram/demo.go
@@ -0,0 +1,25 @@
+package telegram
+
+import (
+ "strings"
+
+ "github.com/tech/sendico/pkg/model"
+)
+
+func newDemoRequestTemplate(request *model.DemoRequest) messageTemplate {
+ fields := []messageField{
+ {label: "Name", value: request.Name},
+ {label: "Organization", value: request.OrganizationName},
+ {label: "Phone", value: request.Phone},
+ {label: "Work email", value: request.WorkEmail},
+ {label: "Payout volume", value: request.PayoutVolume},
+ }
+ if strings.TrimSpace(request.Comment) != "" {
+ fields = append(fields, messageField{label: "Comment", value: request.Comment})
+ }
+ return messageTemplate{
+ title: "New demo request received",
+ fields: fields,
+ emphasize: []string{"demo request"},
+ }
+}
diff --git a/api/notification/internal/server/notificationimp/telegram/message.go b/api/notification/internal/server/notificationimp/telegram/message.go
new file mode 100644
index 0000000..8ec7ae9
--- /dev/null
+++ b/api/notification/internal/server/notificationimp/telegram/message.go
@@ -0,0 +1,174 @@
+package telegram
+
+import (
+ "fmt"
+ "html"
+ "strings"
+)
+
+type parseMode string
+
+const (
+ parseModeUnset parseMode = ""
+ parseModeMarkdown parseMode = "Markdown"
+ parseModeMarkdownV2 parseMode = "MarkdownV2"
+ parseModeHTML parseMode = "HTML"
+)
+
+func normalizeParseMode(value string) parseMode {
+ switch strings.ToLower(strings.TrimSpace(value)) {
+ case "markdown":
+ return parseModeMarkdown
+ case "markdownv2":
+ return parseModeMarkdownV2
+ case "html":
+ return parseModeHTML
+ default:
+ return parseModeUnset
+ }
+}
+
+func (pm parseMode) String() string {
+ return string(pm)
+}
+
+type messageField struct {
+ label string
+ value string
+}
+
+type messageTemplate struct {
+ title string
+ fields []messageField
+ emphasize []string
+}
+
+func (mt messageTemplate) Format(mode parseMode) string {
+ var builder strings.Builder
+ builder.WriteString(formatTitle(mode, mt.title, mt.emphasize))
+ builder.WriteString("\n")
+ builder.WriteString("-----------------------------\n")
+
+ formatter := selectValueFormatter(mode)
+ for _, field := range mt.fields {
+ appendMessageField(&builder, field.label, field.value, formatter)
+ }
+ return builder.String()
+}
+
+type valueFormatter func(string) string
+
+func formatTitle(mode parseMode, title string, emphasize []string) string {
+ switch mode {
+ case parseModeMarkdown:
+ return highlightMarkdown(title, emphasize, escapeMarkdown)
+ case parseModeMarkdownV2:
+ return highlightMarkdown(title, emphasize, escapeMarkdownV2)
+ case parseModeHTML:
+ return highlightHTML(title, emphasize)
+ default:
+ return title
+ }
+}
+
+func highlightMarkdown(title string, emphasize []string, esc func(string) string) string {
+ if len(emphasize) == 0 {
+ return title
+ }
+ result := title
+ for _, word := range emphasize {
+ word = strings.TrimSpace(word)
+ if word == "" {
+ continue
+ }
+ escaped := esc(word)
+ replacement := fmt.Sprintf("*%s*", escaped)
+ result = strings.ReplaceAll(result, word, replacement)
+ }
+ return result
+}
+
+func highlightHTML(title string, emphasize []string) string {
+ if len(emphasize) == 0 {
+ return html.EscapeString(title)
+ }
+ result := html.EscapeString(title)
+ for _, word := range emphasize {
+ word = strings.TrimSpace(word)
+ if word == "" {
+ continue
+ }
+ escaped := html.EscapeString(word)
+ replacement := fmt.Sprintf("%s", escaped)
+ result = strings.ReplaceAll(result, escaped, replacement)
+ }
+ return result
+}
+
+func appendMessageField(builder *strings.Builder, label, value string, formatter valueFormatter) {
+ value = strings.TrimSpace(value)
+ if value == "" {
+ value = "—"
+ } else if formatter != nil {
+ value = formatter(value)
+ }
+ fmt.Fprintf(builder, "• %s: %s\n", label, value)
+}
+
+func selectValueFormatter(mode parseMode) valueFormatter {
+ switch mode {
+ case parseModeMarkdown:
+ return func(value string) string {
+ return fmt.Sprintf("*%s*", escapeMarkdown(value))
+ }
+ case parseModeMarkdownV2:
+ return func(value string) string {
+ return fmt.Sprintf("*%s*", escapeMarkdownV2(value))
+ }
+ case parseModeHTML:
+ return func(value string) string {
+ return fmt.Sprintf("%s", html.EscapeString(value))
+ }
+ default:
+ return nil
+ }
+}
+
+var markdownEscaper = strings.NewReplacer(
+ "*", "\\*",
+ "_", "\\_",
+ "[", "\\[",
+ "]", "\\]",
+ "(", "\\(",
+ ")", "\\)",
+ "`", "\\`",
+)
+
+var markdownV2Escaper = strings.NewReplacer(
+ "_", "\\_",
+ "*", "\\*",
+ "[", "\\[",
+ "]", "\\]",
+ "(", "\\(",
+ ")", "\\)",
+ "~", "\\~",
+ "`", "\\`",
+ ">", "\\>",
+ "#", "\\#",
+ "+", "\\+",
+ "-", "\\-",
+ "=", "\\=",
+ "|", "\\|",
+ "{", "\\{",
+ "}", "\\}",
+ ".", "\\.",
+ "!", "\\!",
+)
+
+func escapeMarkdown(value string) string {
+ return markdownEscaper.Replace(value)
+}
+
+func escapeMarkdownV2(value string) string {
+ return markdownV2Escaper.Replace(value)
+}
diff --git a/api/payments/orchestrator/client/client.go b/api/payments/orchestrator/client/client.go
index e98b3ee..c571933 100644
--- a/api/payments/orchestrator/client/client.go
+++ b/api/payments/orchestrator/client/client.go
@@ -3,11 +3,11 @@ package client
import (
"context"
"crypto/tls"
- "errors"
"fmt"
"strings"
"time"
+ "github.com/tech/sendico/pkg/merrors"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
@@ -48,7 +48,7 @@ type orchestratorClient struct {
func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) {
cfg.setDefaults()
if strings.TrimSpace(cfg.Address) == "" {
- return nil, errors.New("payment-orchestrator: address is required")
+ return nil, merrors.InvalidArgument("payment-orchestrator: address is required")
}
dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
@@ -65,7 +65,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...)
if err != nil {
- return nil, fmt.Errorf("payment-orchestrator: dial %s: %w", cfg.Address, err)
+ return nil, merrors.InternalWrap(err, fmt.Sprintf("payment-orchestrator: dial %s", cfg.Address))
}
return &orchestratorClient{
diff --git a/api/payments/orchestrator/config.yml b/api/payments/orchestrator/config.yml
index 037c328..645d3cc 100644
--- a/api/payments/orchestrator/config.yml
+++ b/api/payments/orchestrator/config.yml
@@ -46,7 +46,7 @@ ledger:
insecure: true
gateway:
- address: "sendico_chain_gateway:50054"
+ address: "sendico_chain_gateway:50070"
dial_timeout_seconds: 5
call_timeout_seconds: 3
insecure: true
diff --git a/api/payments/orchestrator/go.mod b/api/payments/orchestrator/go.mod
index 6ac7d6d..f87cb3f 100644
--- a/api/payments/orchestrator/go.mod
+++ b/api/payments/orchestrator/go.mod
@@ -20,8 +20,8 @@ require (
github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000
github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver v1.17.6
- go.uber.org/zap v1.27.0
- google.golang.org/grpc v1.76.0
+ go.uber.org/zap v1.27.1
+ google.golang.org/grpc v1.77.0
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
)
@@ -46,7 +46,7 @@ require (
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
- github.com/prometheus/common v0.67.2 // indirect
+ github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
@@ -54,7 +54,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
- golang.org/x/crypto v0.44.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
diff --git a/api/payments/orchestrator/go.sum b/api/payments/orchestrator/go.sum
index 0468119..44a7f0b 100644
--- a/api/payments/orchestrator/go.sum
+++ b/api/payments/orchestrator/go.sum
@@ -9,8 +9,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
-github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk=
-github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
@@ -117,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
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.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
-github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
+github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
+github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -140,10 +138,10 @@ github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5
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.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
-github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
-github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
-github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
+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/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.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
@@ -157,32 +155,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
-go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
-go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
-go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
-go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
-go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
-go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
-go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
-go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
-golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -219,8 +217,8 @@ 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/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
-google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
-google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
+google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/api/pkg/.DS_Store b/api/pkg/.DS_Store
deleted file mode 100644
index 1aa90dd..0000000
Binary files a/api/pkg/.DS_Store and /dev/null differ
diff --git a/api/pkg/auth/factory.go b/api/pkg/auth/factory.go
index b4b6a55..c054e56 100644
--- a/api/pkg/auth/factory.go
+++ b/api/pkg/auth/factory.go
@@ -48,5 +48,5 @@ func CreateAuth(
}
return enforcer, manager, nil
}
- return nil, nil, merrors.InvalidArgument("Unknown enforcer type: " + string(config.Driver))
+ return nil, nil, merrors.InvalidArgument("Unknown enforcer type: "+string(config.Driver), "config.driver")
}
diff --git a/api/pkg/auth/internal/casbin/action.go b/api/pkg/auth/internal/casbin/action.go
index 8e25dad..e19efb1 100644
--- a/api/pkg/auth/internal/casbin/action.go
+++ b/api/pkg/auth/internal/casbin/action.go
@@ -18,6 +18,6 @@ func stringToAction(actionStr string) (model.Action, error) {
case string(model.ActionDelete):
return model.ActionDelete, nil
default:
- return "", merrors.InvalidArgument(fmt.Sprintf("invalid action: %s", actionStr))
+ return "", merrors.InvalidArgument(fmt.Sprintf("invalid action: %s", actionStr), "action")
}
}
diff --git a/api/pkg/auth/internal/casbin/config/config.go b/api/pkg/auth/internal/casbin/config/config.go
index 17d1f47..4984fef 100644
--- a/api/pkg/auth/internal/casbin/config/config.go
+++ b/api/pkg/auth/internal/casbin/config/config.go
@@ -109,7 +109,7 @@ func PrepareConfig(logger mlogger.Logger, config *Config) (*EnforcerConfig, erro
if len(adapter.DatabaseName) == 0 {
logger.Error("Database name is not set")
- return nil, merrors.InvalidArgument("database name must be provided")
+ return nil, merrors.InvalidArgument("database name must be provided", "adapter.databaseName")
}
path := getEnvValue(logger, "model_path", "model_path_env", config.ModelPath, config.ModelPathEnv)
diff --git a/api/pkg/auth/internal/casbin/role.go b/api/pkg/auth/internal/casbin/role.go
index cc42979..7c0811e 100644
--- a/api/pkg/auth/internal/casbin/role.go
+++ b/api/pkg/auth/internal/casbin/role.go
@@ -35,7 +35,7 @@ func NewRoleManager(logger mlogger.Logger, enforcer *CasbinEnforcer, rolePermiss
func (rm *RoleManager) validateObjectIDs(ids ...primitive.ObjectID) error {
for _, id := range ids {
if id.IsZero() {
- return merrors.InvalidArgument("Object references cannot be zero")
+ return merrors.InvalidArgument("Object references cannot be zero", "objectRef")
}
}
return nil
diff --git a/api/pkg/auth/internal/native/role.go b/api/pkg/auth/internal/native/role.go
index 2515b2c..f50eaa6 100644
--- a/api/pkg/auth/internal/native/role.go
+++ b/api/pkg/auth/internal/native/role.go
@@ -36,7 +36,7 @@ func NewRoleManager(logger mlogger.Logger, enforcer *Enforcer, rolePermissionRef
func (rm *RoleManager) validateObjectIDs(ids ...primitive.ObjectID) error {
for _, id := range ids {
if id.IsZero() {
- return merrors.InvalidArgument("Object references cannot be zero")
+ return merrors.InvalidArgument("Object references cannot be zero", "objectRef")
}
}
return nil
diff --git a/api/pkg/db/confirmation/confirmation.go b/api/pkg/db/confirmation/confirmation.go
new file mode 100644
index 0000000..7f5d510
--- /dev/null
+++ b/api/pkg/db/confirmation/confirmation.go
@@ -0,0 +1,16 @@
+package confirmation
+
+import (
+ "context"
+
+ "github.com/tech/sendico/pkg/db/template"
+ "github.com/tech/sendico/pkg/model"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type DB interface {
+ template.DB[*model.ConfirmationCode]
+
+ FindActive(ctx context.Context, accountRef primitive.ObjectID, destination string, target model.ConfirmationTarget, now int64) (*model.ConfirmationCode, error)
+ DeleteTuple(ctx context.Context, accountRef primitive.ObjectID, destination string, target model.ConfirmationTarget) error
+}
diff --git a/api/pkg/db/connection.go b/api/pkg/db/connection.go
index da45fca..de00fbf 100644
--- a/api/pkg/db/connection.go
+++ b/api/pkg/db/connection.go
@@ -47,10 +47,10 @@ func (c *MongoConnection) Ping(ctx context.Context) error {
// ConnectMongo returns a low-level MongoDB connection without constructing repositories.
func ConnectMongo(logger mlogger.Logger, config *Config) (*MongoConnection, error) {
if config == nil {
- return nil, merrors.InvalidArgument("database configuration is nil")
+ return nil, merrors.InvalidArgument("database configuration is nil", "config")
}
if config.Driver != Mongo {
- return nil, merrors.InvalidArgument("unsupported database driver: " + string(config.Driver))
+ return nil, merrors.InvalidArgument("unsupported database driver: "+string(config.Driver), "config.driver")
}
client, _, settings, err := mongoimpl.ConnectClient(logger, config.Settings)
diff --git a/api/pkg/db/factory.go b/api/pkg/db/factory.go
index f43697d..3c37aac 100644
--- a/api/pkg/db/factory.go
+++ b/api/pkg/db/factory.go
@@ -3,6 +3,7 @@ package db
import (
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/account"
+ "github.com/tech/sendico/pkg/db/confirmation"
mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo"
"github.com/tech/sendico/pkg/db/invitation"
"github.com/tech/sendico/pkg/db/organization"
@@ -17,6 +18,7 @@ import (
// Factory exposes high-level repositories used by application services.
type Factory interface {
NewRefreshTokensDB() (refreshtokens.DB, error)
+ NewConfirmationsDB() (confirmation.DB, error)
NewAccountDB() (account.DB, error)
NewOrganizationDB() (organization.DB, error)
@@ -37,5 +39,5 @@ func NewConnection(logger mlogger.Logger, config *Config) (Factory, error) {
if config.Driver == Mongo {
return mongoimpl.NewConnection(logger, config.Settings)
}
- return nil, merrors.InvalidArgument("unknown database driver: " + string(config.Driver))
+ return nil, merrors.InvalidArgument("unknown database driver: "+string(config.Driver), "config.driver")
}
diff --git a/api/pkg/db/internal/mongo/confirmationdb/db.go b/api/pkg/db/internal/mongo/confirmationdb/db.go
new file mode 100644
index 0000000..27cf571
--- /dev/null
+++ b/api/pkg/db/internal/mongo/confirmationdb/db.go
@@ -0,0 +1,67 @@
+package confirmationdb
+
+import (
+ "github.com/tech/sendico/pkg/db/confirmation"
+ ri "github.com/tech/sendico/pkg/db/repository/index"
+ "github.com/tech/sendico/pkg/db/template"
+ "github.com/tech/sendico/pkg/mlogger"
+ "github.com/tech/sendico/pkg/model"
+ "github.com/tech/sendico/pkg/mservice"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.uber.org/zap"
+)
+
+const (
+ fieldAccountRef = "accountRef"
+ fieldDestination = "destination"
+ fieldTarget = "target"
+ fieldExpiresAt = "expiresAt"
+ fieldUsed = "used"
+)
+
+type ConfirmationDB struct {
+ template.DBImp[*model.ConfirmationCode]
+}
+
+func Create(logger mlogger.Logger, db *mongo.Database) (confirmation.DB, error) {
+ p := &ConfirmationDB{
+ DBImp: *template.Create[*model.ConfirmationCode](logger, mservice.Confirmations, db),
+ }
+
+ // Ensure one active code per account/destination/target.
+ if err := p.Repository.CreateIndex(&ri.Definition{
+ Keys: []ri.Key{
+ {Field: fieldAccountRef, Sort: ri.Asc},
+ {Field: fieldDestination, Sort: ri.Asc},
+ {Field: fieldTarget, Sort: ri.Asc},
+ },
+ Unique: true,
+ }); err != nil {
+ p.Logger.Error("Failed to create confirmation unique index", zap.Error(err))
+ return nil, err
+ }
+
+ // TTL on expiry.
+ ttl := int32(0)
+ if err := p.Repository.CreateIndex(&ri.Definition{
+ Keys: []ri.Key{
+ {Field: fieldExpiresAt, Sort: ri.Asc},
+ },
+ TTL: &ttl,
+ }); err != nil {
+ p.Logger.Error("Failed to create confirmation TTL index", zap.Error(err))
+ return nil, err
+ }
+
+ // Query helper indexes.
+ if err := p.Repository.CreateIndex(&ri.Definition{
+ Keys: []ri.Key{
+ {Field: fieldUsed, Sort: ri.Asc},
+ },
+ }); err != nil {
+ p.Logger.Error("Failed to create confirmation used index", zap.Error(err))
+ return nil, err
+ }
+
+ return p, nil
+}
diff --git a/api/pkg/db/internal/mongo/confirmationdb/delete.go b/api/pkg/db/internal/mongo/confirmationdb/delete.go
new file mode 100644
index 0000000..c174435
--- /dev/null
+++ b/api/pkg/db/internal/mongo/confirmationdb/delete.go
@@ -0,0 +1,17 @@
+package confirmationdb
+
+import (
+ "context"
+
+ "github.com/tech/sendico/pkg/db/repository"
+ "github.com/tech/sendico/pkg/model"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+func (db *ConfirmationDB) DeleteTuple(ctx context.Context, accountRef primitive.ObjectID, destination string, target model.ConfirmationTarget) error {
+ query := repository.Query().
+ Filter(repository.Field(fieldAccountRef), accountRef).
+ Filter(repository.Field(fieldDestination), destination).
+ Filter(repository.Field(fieldTarget), target)
+ return db.DeleteMany(ctx, query)
+}
diff --git a/api/pkg/db/internal/mongo/confirmationdb/find.go b/api/pkg/db/internal/mongo/confirmationdb/find.go
new file mode 100644
index 0000000..3be2f63
--- /dev/null
+++ b/api/pkg/db/internal/mongo/confirmationdb/find.go
@@ -0,0 +1,26 @@
+package confirmationdb
+
+import (
+ "context"
+ "time"
+
+ "github.com/tech/sendico/pkg/db/repository"
+ "github.com/tech/sendico/pkg/db/repository/builder"
+ "github.com/tech/sendico/pkg/model"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+func (db *ConfirmationDB) FindActive(ctx context.Context, accountRef primitive.ObjectID, destination string, target model.ConfirmationTarget, now int64) (*model.ConfirmationCode, error) {
+ var res model.ConfirmationCode
+ query := repository.Query().
+ Filter(repository.Field(fieldAccountRef), accountRef).
+ Filter(repository.Field(fieldDestination), destination).
+ Filter(repository.Field(fieldTarget), target).
+ Filter(repository.Field(fieldUsed), false).
+ Comparison(repository.Field(fieldExpiresAt), builder.Gt, time.Unix(now, 0))
+
+ if err := db.FindOne(ctx, query, &res); err != nil {
+ return nil, err
+ }
+ return &res, nil
+}
diff --git a/api/pkg/db/internal/mongo/db.go b/api/pkg/db/internal/mongo/db.go
index 6083f15..02cc9ff 100755
--- a/api/pkg/db/internal/mongo/db.go
+++ b/api/pkg/db/internal/mongo/db.go
@@ -7,7 +7,9 @@ import (
"github.com/mitchellh/mapstructure"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/account"
+ "github.com/tech/sendico/pkg/db/confirmation"
"github.com/tech/sendico/pkg/db/internal/mongo/accountdb"
+ "github.com/tech/sendico/pkg/db/internal/mongo/confirmationdb"
"github.com/tech/sendico/pkg/db/internal/mongo/invitationdb"
"github.com/tech/sendico/pkg/db/internal/mongo/organizationdb"
"github.com/tech/sendico/pkg/db/internal/mongo/policiesdb"
@@ -156,6 +158,10 @@ func (db *DB) NewAccountDB() (account.DB, error) {
return accountdb.Create(db.logger, db.db())
}
+func (db *DB) NewConfirmationsDB() (confirmation.DB, error) {
+ return confirmationdb.Create(db.logger, db.db())
+}
+
func (db *DB) NewOrganizationDB() (organization.DB, error) {
pdb, err := db.NewPoliciesDB()
if err != nil {
diff --git a/api/pkg/db/internal/mongo/organizationdb/create.go b/api/pkg/db/internal/mongo/organizationdb/create.go
index 9f69ae4..2be8cd2 100644
--- a/api/pkg/db/internal/mongo/organizationdb/create.go
+++ b/api/pkg/db/internal/mongo/organizationdb/create.go
@@ -10,7 +10,7 @@ import (
func (db *OrganizationDB) Create(ctx context.Context, _, _ primitive.ObjectID, org *model.Organization) error {
if org == nil {
- return merrors.InvalidArgument("Organization object is nil")
+ return merrors.InvalidArgument("Organization object is nil", "organization")
}
org.SetID(primitive.NewObjectID())
// Organizaiton reference must be set to the same value as own organization reference
diff --git a/api/pkg/db/internal/mongo/refreshtokensdb/crud.go b/api/pkg/db/internal/mongo/refreshtokensdb/crud.go
index 7365b88..92699f3 100644
--- a/api/pkg/db/internal/mongo/refreshtokensdb/crud.go
+++ b/api/pkg/db/internal/mongo/refreshtokensdb/crud.go
@@ -18,7 +18,7 @@ func (db *RefreshTokenDB) Create(ctx context.Context, rt *model.RefreshToken) er
// First, try to find an existing token for this account/client/device combination
var existing model.RefreshToken
if rt.AccountRef == nil {
- return merrors.InvalidArgument("Account reference must have a vaild value")
+ return merrors.InvalidArgument("Account reference must have a vaild value", "refreshToken.accountRef")
}
if err := db.FindOne(ctx, filterByAccount(*rt.AccountRef, &rt.SessionIdentifier), &existing); err != nil {
if errors.Is(err, merrors.ErrNoData) {
diff --git a/api/pkg/db/internal/mongo/repositoryimp/builderimp/pipeline_test.go b/api/pkg/db/internal/mongo/repositoryimp/builderimp/pipeline_test.go
index 750b0e7..129f34a 100644
--- a/api/pkg/db/internal/mongo/repositoryimp/builderimp/pipeline_test.go
+++ b/api/pkg/db/internal/mongo/repositoryimp/builderimp/pipeline_test.go
@@ -3,9 +3,9 @@ package builderimp
import (
"testing"
+ "github.com/stretchr/testify/assert"
"github.com/tech/sendico/pkg/db/repository/builder"
"github.com/tech/sendico/pkg/mservice"
- "github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
@@ -45,7 +45,7 @@ func TestPipelineImp_Lookup(t *testing.T) {
mockForeignField := &MockField{build: "foreignField"}
mockAsField := &MockField{build: "asField"}
- result := pipeline.Lookup(mservice.Projects, mockLocalField, mockForeignField, mockAsField)
+ result := pipeline.Lookup(mservice.Site, mockLocalField, mockForeignField, mockAsField)
// Should return self for chaining
assert.Same(t, pipeline, result)
@@ -54,7 +54,7 @@ func TestPipelineImp_Lookup(t *testing.T) {
assert.Len(t, built, 1)
expected := bson.D{{Key: string(builder.Lookup), Value: bson.D{
- {Key: string(builder.MKFrom), Value: mservice.Projects},
+ {Key: string(builder.MKFrom), Value: mservice.Site},
{Key: string(builder.MKLocalField), Value: "localField"},
{Key: string(builder.MKForeignField), Value: "foreignField"},
{Key: string(builder.MKAs), Value: "asField"},
@@ -70,7 +70,7 @@ func TestPipelineImp_LookupWithPipeline_WithoutLet(t *testing.T) {
}
mockAsField := &MockField{build: "asField"}
- result := pipeline.LookupWithPipeline(mservice.Tasks, mockNestedPipeline, mockAsField, nil)
+ result := pipeline.LookupWithPipeline(mservice.Site, mockNestedPipeline, mockAsField, nil)
// Should return self for chaining
assert.Same(t, pipeline, result)
@@ -79,7 +79,7 @@ func TestPipelineImp_LookupWithPipeline_WithoutLet(t *testing.T) {
assert.Len(t, built, 1)
expected := bson.D{{Key: string(builder.Lookup), Value: bson.D{
- {Key: string(builder.MKFrom), Value: mservice.Tasks},
+ {Key: string(builder.MKFrom), Value: mservice.Site},
{Key: string(builder.MKPipeline), Value: mockNestedPipeline.build},
{Key: string(builder.MKAs), Value: "asField"},
}}}
@@ -99,7 +99,7 @@ func TestPipelineImp_LookupWithPipeline_WithLet(t *testing.T) {
"projRef": mockLetField,
}
- result := pipeline.LookupWithPipeline(mservice.Tasks, mockNestedPipeline, mockAsField, &letVars)
+ result := pipeline.LookupWithPipeline(mservice.Site, mockNestedPipeline, mockAsField, &letVars)
// Should return self for chaining
assert.Same(t, pipeline, result)
@@ -108,7 +108,7 @@ func TestPipelineImp_LookupWithPipeline_WithLet(t *testing.T) {
assert.Len(t, built, 1)
expected := bson.D{{Key: string(builder.Lookup), Value: bson.D{
- {Key: string(builder.MKFrom), Value: mservice.Tasks},
+ {Key: string(builder.MKFrom), Value: mservice.Site},
{Key: string(builder.MKPipeline), Value: mockNestedPipeline.build},
{Key: string(builder.MKAs), Value: "asField"},
{Key: string(builder.MKLet), Value: bson.D{{Key: "projRef", Value: "$_id"}}},
@@ -126,14 +126,14 @@ func TestPipelineImp_LookupWithPipeline_WithEmptyLet(t *testing.T) {
emptyLetVars := map[string]builder.Field{}
- pipeline.LookupWithPipeline(mservice.Tasks, mockNestedPipeline, mockAsField, &emptyLetVars)
+ pipeline.LookupWithPipeline(mservice.Site, mockNestedPipeline, mockAsField, &emptyLetVars)
built := pipeline.Build()
assert.Len(t, built, 1)
// Should not include let field when empty
expected := bson.D{{Key: string(builder.Lookup), Value: bson.D{
- {Key: string(builder.MKFrom), Value: mservice.Tasks},
+ {Key: string(builder.MKFrom), Value: mservice.Site},
{Key: string(builder.MKPipeline), Value: mockNestedPipeline.build},
{Key: string(builder.MKAs), Value: "asField"},
}}}
diff --git a/api/pkg/db/internal/mongo/repositoryimp/index.go b/api/pkg/db/internal/mongo/repositoryimp/index.go
index 9a3a31b..7313aa7 100644
--- a/api/pkg/db/internal/mongo/repositoryimp/index.go
+++ b/api/pkg/db/internal/mongo/repositoryimp/index.go
@@ -15,7 +15,7 @@ func (r *MongoRepository) CreateIndex(def *ri.Definition) error {
return merrors.NoData("data collection is not set")
}
if len(def.Keys) == 0 {
- return merrors.InvalidArgument("Index definition has no keys")
+ return merrors.InvalidArgument("Index definition has no keys", "index.keys")
}
// ----- build BSON keys --------------------------------------------------
diff --git a/api/pkg/db/internal/mongo/repositoryimp/repository.go b/api/pkg/db/internal/mongo/repositoryimp/repository.go
index adac397..81e2cc4 100644
--- a/api/pkg/db/internal/mongo/repositoryimp/repository.go
+++ b/api/pkg/db/internal/mongo/repositoryimp/repository.go
@@ -83,7 +83,7 @@ func (r *MongoRepository) findOneByFilterImp(ctx context.Context, filter bson.D,
func (r *MongoRepository) Get(ctx context.Context, id primitive.ObjectID, result storable.Storable) error {
if id.IsZero() {
- return merrors.InvalidArgument("zero id provided while fetching " + result.Collection())
+ return merrors.InvalidArgument("zero id provided while fetching "+result.Collection(), "id")
}
return r.findOneByFilterImp(ctx, idFilter(id), fmt.Sprintf("%s with ID = %s not found", result.Collection(), id.Hex()), result)
}
@@ -134,7 +134,7 @@ func (r *MongoRepository) Update(ctx context.Context, obj storable.Storable) err
func (r *MongoRepository) Patch(ctx context.Context, id primitive.ObjectID, patch builder.Patch) error {
if id.IsZero() {
- return merrors.InvalidArgument("zero id provided while patching")
+ return merrors.InvalidArgument("zero id provided while patching", "id")
}
_, err := r.collection.UpdateByID(ctx, id, patch.Build())
return err
diff --git a/api/pkg/db/internal/mongo/tseriesimp/tseries.go b/api/pkg/db/internal/mongo/tseriesimp/tseries.go
index f7713c0..ed1a5ba 100644
--- a/api/pkg/db/internal/mongo/tseriesimp/tseries.go
+++ b/api/pkg/db/internal/mongo/tseriesimp/tseries.go
@@ -22,7 +22,7 @@ type TimeSeries struct {
func NewMongoTimeSeriesCollection(ctx context.Context, db *mongo.Database, tsOpts *tsoptions.Options) (*TimeSeries, error) {
if tsOpts == nil {
- return nil, merrors.InvalidArgument("nil time-series options provided")
+ return nil, merrors.InvalidArgument("nil time-series options provided", "options")
}
// Configure time-series options
granularity := tsOpts.Granularity.String()
diff --git a/api/pkg/go.mod b/api/pkg/go.mod
index 0147599..be42262 100644
--- a/api/pkg/go.mod
+++ b/api/pkg/go.mod
@@ -15,9 +15,9 @@ require (
github.com/testcontainers/testcontainers-go v0.33.0
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0
go.mongodb.org/mongo-driver v1.17.6
- go.uber.org/zap v1.27.0
- golang.org/x/crypto v0.44.0
- google.golang.org/grpc v1.76.0
+ go.uber.org/zap v1.27.1
+ golang.org/x/crypto v0.45.0
+ google.golang.org/grpc v1.77.0
google.golang.org/protobuf v1.36.10
)
@@ -67,7 +67,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
- github.com/prometheus/common v0.67.2 // indirect
+ github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
@@ -80,12 +80,12 @@ require (
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.1.0 // 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.37.0 // indirect
+ go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
- go.opentelemetry.io/otel/metric v1.37.0 // indirect
- go.opentelemetry.io/otel/trace v1.37.0 // indirect
+ go.opentelemetry.io/otel/metric v1.38.0 // indirect
+ go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/net v0.47.0 // indirect
diff --git a/api/pkg/go.sum b/api/pkg/go.sum
index daeed6a..6c0ebad 100644
--- a/api/pkg/go.sum
+++ b/api/pkg/go.sum
@@ -11,8 +11,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
-github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk=
-github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
@@ -128,12 +126,12 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
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.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
-github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
+github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
+github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
-github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+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/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=
@@ -172,40 +170,40 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
-go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
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.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
-go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
-go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
-go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
-go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
-go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
-go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+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.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
-golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -269,12 +267,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
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-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
-google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
+google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
+google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
-google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
-google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
+google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/api/pkg/merrors/errors.go b/api/pkg/merrors/errors.go
index cb558c0..754db56 100644
--- a/api/pkg/merrors/errors.go
+++ b/api/pkg/merrors/errors.go
@@ -3,6 +3,7 @@ package merrors
import (
"errors"
"fmt"
+ "strings"
"go.mongodb.org/mongo-driver/bson/primitive"
)
@@ -27,8 +28,8 @@ func Internal(msg string) error {
var ErrInvalidArg = errors.New("invalidArgError")
-func InvalidArgument(msg string) error {
- return fmt.Errorf("%w: %s", ErrInvalidArg, msg)
+func InvalidArgument(msg string, argumentNames ...string) error {
+ return fmt.Errorf("%w: %s", ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...))
}
var ErrDataConflict = errors.New("DataConflict")
@@ -49,13 +50,13 @@ func AccessDenied(object, action string, objectRef primitive.ObjectID) error {
var ErrInvalidDataType = errors.New("invalidDataType")
func InvalidDataType(msg string) error {
- return fmt.Errorf("%w: %s", ErrDataConflict, msg)
+ return fmt.Errorf("%w: %s", ErrInvalidDataType, msg)
}
var ErrUnauthorized = errors.New("unathorized")
func Unauthorized(msg string) error {
- return fmt.Errorf("%w: %s", ErrDataConflict, msg)
+ return fmt.Errorf("%w: %s", ErrUnauthorized, msg)
}
var ErrNoMessagingTopic = errors.New("messagingTopicError")
@@ -63,3 +64,39 @@ var ErrNoMessagingTopic = errors.New("messagingTopicError")
func NoMessagingTopic(topic string) error {
return fmt.Errorf("%w: messaging topic '%s' not found", ErrNoMessagingTopic, topic)
}
+
+func InvalidArgumentWrap(err error, msg string, argumentNames ...string) error {
+ return wrapError(ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...), err)
+}
+
+func InternalWrap(err error, msg string) error {
+ return wrapError(ErrInternal, msg, err)
+}
+
+func wrapError(base error, msg string, err error) error {
+ baseErr := fmt.Errorf("%w: %s", base, msg)
+ if err == nil {
+ return baseErr
+ }
+ return errors.Join(baseErr, err)
+}
+
+func invalidArgumentMessage(msg string, argumentNames ...string) string {
+ names := make([]string, 0, len(argumentNames))
+ for _, name := range argumentNames {
+ name = strings.TrimSpace(name)
+ if name == "" {
+ continue
+ }
+ names = append(names, fmt.Sprintf("%q", name))
+ }
+ if len(names) == 0 {
+ return msg
+ }
+
+ prefix := "broken argument"
+ if len(names) > 1 {
+ prefix = "broken arguments"
+ }
+ return fmt.Sprintf("%s %s: %s", prefix, strings.Join(names, ", "), msg)
+}
diff --git a/api/pkg/merrors/errors_test.go b/api/pkg/merrors/errors_test.go
new file mode 100644
index 0000000..bb6cedd
--- /dev/null
+++ b/api/pkg/merrors/errors_test.go
@@ -0,0 +1,47 @@
+package merrors
+
+import (
+ "errors"
+ "strings"
+ "testing"
+)
+
+func TestInvalidArgumentSupportsBrokenArgumentName(t *testing.T) {
+ t.Run("without argument name keeps old behavior", func(t *testing.T) {
+ err := InvalidArgument("value is missing")
+ expected := "invalidArgError: value is missing"
+ if err.Error() != expected {
+ t.Fatalf("unexpected error message: %s", err)
+ }
+ if !errors.Is(err, ErrInvalidArg) {
+ t.Fatalf("error should wrap ErrInvalidArg")
+ }
+ })
+
+ t.Run("single argument name", func(t *testing.T) {
+ err := InvalidArgument("value is missing", "bot_token_env")
+ expected := `invalidArgError: broken argument "bot_token_env": value is missing`
+ if err.Error() != expected {
+ t.Fatalf("unexpected error message: %s", err)
+ }
+ })
+
+ t.Run("multiple argument names", func(t *testing.T) {
+ err := InvalidArgument("value mismatch", "bot_token_env", "chat_id_env", " ")
+ expected := `invalidArgError: broken arguments "bot_token_env", "chat_id_env": value mismatch`
+ if err.Error() != expected {
+ t.Fatalf("unexpected error message: %s", err)
+ }
+ })
+}
+
+func TestInvalidArgumentWrapSupportsBrokenArgumentName(t *testing.T) {
+ base := errors.New("root cause")
+ err := InvalidArgumentWrap(base, "value is missing", "bot_token_env")
+ if !strings.Contains(err.Error(), `invalidArgError: broken argument "bot_token_env": value is missing`) {
+ t.Fatalf("wrapped error should include broken argument name: %s", err)
+ }
+ if !errors.Is(err, ErrInvalidArg) || !errors.Is(err, base) {
+ t.Fatalf("wrapped error should preserve all error layers")
+ }
+}
diff --git a/api/pkg/messaging/internal/inprocess/broker.go b/api/pkg/messaging/internal/inprocess/broker.go
index dc54b20..3be97aa 100644
--- a/api/pkg/messaging/internal/inprocess/broker.go
+++ b/api/pkg/messaging/internal/inprocess/broker.go
@@ -76,7 +76,7 @@ func (b *MessageBroker) Unsubscribe(event model.NotificationEvent, subChan <-cha
func NewInProcessBroker(logger mlogger.Logger, bufferSize int) (*MessageBroker, error) {
if bufferSize < 1 {
- return nil, merrors.InvalidArgument(fmt.Sprintf("Invelid buffer size %d. It must be greater than 1", bufferSize))
+ return nil, merrors.InvalidArgument(fmt.Sprintf("Invelid buffer size %d. It must be greater than 1", bufferSize), "bufferSize")
}
logger.Info("Created in-process logger", zap.Int("buffer_size", bufferSize))
return &MessageBroker{
diff --git a/api/pkg/messaging/internal/natsb/broker.go b/api/pkg/messaging/internal/natsb/broker.go
index 14cb51c..b2e2b97 100644
--- a/api/pkg/messaging/internal/natsb/broker.go
+++ b/api/pkg/messaging/internal/natsb/broker.go
@@ -39,7 +39,7 @@ func loadEnv(settings *nc.Settings, l *zap.Logger) (*envConfig, error) {
return v, nil
}
l.Error(fmt.Sprintf("NATS %s not found in environment", label), zap.String("env_var", key))
- return "", merrors.InvalidArgument(fmt.Sprintf("NATS %s not found in environment variable: %s", label, key))
+ return "", merrors.InvalidArgument(fmt.Sprintf("NATS %s not found in environment variable: %s", label, key), key)
}
user, err := get(settings.UsernameEnv, "user name")
@@ -65,7 +65,7 @@ func loadEnv(settings *nc.Settings, l *zap.Logger) (*envConfig, error) {
port, err := strconv.Atoi(portStr)
if err != nil || port <= 0 || port > 65535 {
l.Error("Invalid NATS port value", zap.String("port", portStr))
- return nil, merrors.InvalidArgument("Invalid NATS port: " + portStr)
+ return nil, merrors.InvalidArgument("Invalid NATS port: "+portStr, settings.PortEnv)
}
return &envConfig{
diff --git a/api/pkg/messaging/internal/notifications/confirmation/notification.go b/api/pkg/messaging/internal/notifications/confirmation/notification.go
new file mode 100644
index 0000000..c45f5ae
--- /dev/null
+++ b/api/pkg/messaging/internal/notifications/confirmation/notification.go
@@ -0,0 +1,47 @@
+package notifications
+
+import (
+ "encoding/json"
+
+ messaging "github.com/tech/sendico/pkg/messaging/envelope"
+ "github.com/tech/sendico/pkg/model"
+ nm "github.com/tech/sendico/pkg/model/notification"
+ "github.com/tech/sendico/pkg/mservice"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type confirmationCodePayload struct {
+ AccountRef string `json:"accountRef"`
+ Destination string `json:"destination"`
+ Target string `json:"target"`
+ Code string `json:"code"`
+}
+
+type ConfirmationCodeNotification struct {
+ messaging.Envelope
+ payload confirmationCodePayload
+}
+
+func (ccn *ConfirmationCodeNotification) Serialize() ([]byte, error) {
+ data, err := json.Marshal(ccn.payload)
+ if err != nil {
+ return nil, err
+ }
+ return ccn.Envelope.Wrap(data)
+}
+
+func newConfirmationEvent(action nm.NotificationAction) model.NotificationEvent {
+ return model.NewNotification(mservice.Confirmations, action)
+}
+
+func NewConfirmationCodeEnvelope(sender string, accountRef primitive.ObjectID, destination string, target model.ConfirmationTarget, code string) messaging.Envelope {
+ return &ConfirmationCodeNotification{
+ Envelope: messaging.CreateEnvelope(sender, newConfirmationEvent(nm.NAPending)),
+ payload: confirmationCodePayload{
+ AccountRef: accountRef.Hex(),
+ Destination: destination,
+ Target: string(target),
+ Code: code,
+ },
+ }
+}
diff --git a/api/pkg/messaging/internal/notifications/confirmation/processor.go b/api/pkg/messaging/internal/notifications/confirmation/processor.go
new file mode 100644
index 0000000..c085712
--- /dev/null
+++ b/api/pkg/messaging/internal/notifications/confirmation/processor.go
@@ -0,0 +1,70 @@
+package notifications
+
+import (
+ "context"
+ "encoding/json"
+
+ "github.com/tech/sendico/pkg/db/account"
+ "github.com/tech/sendico/pkg/merrors"
+ me "github.com/tech/sendico/pkg/messaging/envelope"
+ ch "github.com/tech/sendico/pkg/messaging/notifications/confirmation/handler"
+ np "github.com/tech/sendico/pkg/messaging/notifications/processor"
+ "github.com/tech/sendico/pkg/mlogger"
+ "github.com/tech/sendico/pkg/model"
+ nm "github.com/tech/sendico/pkg/model/notification"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.uber.org/zap"
+)
+
+type ConfirmationCodeProcessor struct {
+ logger mlogger.Logger
+ db account.DB
+ handler ch.ConfirmationCodeHandler
+ event model.NotificationEvent
+}
+
+func (ccp *ConfirmationCodeProcessor) Process(ctx context.Context, envelope me.Envelope) error {
+ var msg confirmationCodePayload
+ if err := json.Unmarshal(envelope.GetData(), &msg); err != nil {
+ ccp.logger.Warn("Failed to unmarshal confirmation code envelope", zap.Error(err), zap.String("topic", ccp.event.ToString()))
+ return err
+ }
+
+ accountRef, err := primitive.ObjectIDFromHex(msg.AccountRef)
+ if err != nil {
+ ccp.logger.Warn("Failed to restore account id from envelope", zap.Error(err), zap.String("topic", ccp.event.ToString()), zap.String("account_ref", msg.AccountRef))
+ return err
+ }
+
+ var account model.Account
+ if err := ccp.db.Get(ctx, accountRef, &account); err != nil {
+ ccp.logger.Warn("Failed to fetch account for confirmation code", zap.Error(err), zap.String("topic", ccp.event.ToString()), zap.String("account_ref", msg.AccountRef))
+ return err
+ }
+
+ target := model.ConfirmationTarget(msg.Target)
+ if target != model.ConfirmationTargetLogin && target != model.ConfirmationTargetPayout {
+ return merrors.InvalidArgument("invalid confirmation target", "target")
+ }
+ if msg.Code == "" {
+ return merrors.InvalidArgument("empty confirmation code", "code")
+ }
+ if msg.Destination == "" {
+ return merrors.InvalidArgument("empty destination", "destination")
+ }
+
+ return ccp.handler(ctx, &account, msg.Destination, target, msg.Code)
+}
+
+func (ccp *ConfirmationCodeProcessor) GetSubject() model.NotificationEvent {
+ return ccp.event
+}
+
+func NewConfirmationCodeProcessor(logger mlogger.Logger, db account.DB, handler ch.ConfirmationCodeHandler) np.EnvelopeProcessor {
+ return &ConfirmationCodeProcessor{
+ logger: logger.Named("confirmation_code_processor"),
+ db: db,
+ handler: handler,
+ event: newConfirmationEvent(nm.NAPending),
+ }
+}
diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go
new file mode 100644
index 0000000..d496272
--- /dev/null
+++ b/api/pkg/messaging/internal/notifications/site/notification.go
@@ -0,0 +1,124 @@
+package notifications
+
+import (
+ gmessaging "github.com/tech/sendico/pkg/generated/gmessaging"
+ "github.com/tech/sendico/pkg/merrors"
+ messaging "github.com/tech/sendico/pkg/messaging/envelope"
+ "github.com/tech/sendico/pkg/model"
+ nm "github.com/tech/sendico/pkg/model/notification"
+ "github.com/tech/sendico/pkg/mservice"
+ "google.golang.org/protobuf/proto"
+)
+
+type SiteRequestNotification struct {
+ messaging.Envelope
+ requestType gmessaging.SiteRequestEvent_RequestType
+ demoRequest *model.DemoRequest
+ contactRequest *model.ContactRequest
+ callRequest *model.CallRequest
+}
+
+func (srn *SiteRequestNotification) Serialize() ([]byte, error) {
+ msg := gmessaging.SiteRequestEvent{
+ Type: srn.requestType,
+ }
+
+ switch srn.requestType {
+ case gmessaging.SiteRequestEvent_REQUEST_TYPE_DEMO:
+ if srn.demoRequest == nil {
+ return nil, merrors.InvalidArgument("demo request payload is empty", "request")
+ }
+ msg.Payload = &gmessaging.SiteRequestEvent_Demo{
+ Demo: &gmessaging.SiteDemoRequest{
+ Name: srn.demoRequest.Name,
+ OrganizationName: srn.demoRequest.OrganizationName,
+ Phone: srn.demoRequest.Phone,
+ WorkEmail: srn.demoRequest.WorkEmail,
+ PayoutVolume: srn.demoRequest.PayoutVolume,
+ Comment: srn.demoRequest.Comment,
+ },
+ }
+ case gmessaging.SiteRequestEvent_REQUEST_TYPE_CONTACT:
+ if srn.contactRequest == nil {
+ return nil, merrors.InvalidArgument("contact request payload is empty", "request")
+ }
+ msg.Payload = &gmessaging.SiteRequestEvent_Contact{
+ Contact: &gmessaging.SiteContactRequest{
+ Name: srn.contactRequest.Name,
+ Email: srn.contactRequest.Email,
+ Phone: srn.contactRequest.Phone,
+ Company: srn.contactRequest.Company,
+ Topic: srn.contactRequest.Topic,
+ Message: srn.contactRequest.Message,
+ },
+ }
+ case gmessaging.SiteRequestEvent_REQUEST_TYPE_CALL:
+ if srn.callRequest == nil {
+ return nil, merrors.InvalidArgument("call request payload is empty", "request")
+ }
+ msg.Payload = &gmessaging.SiteRequestEvent_Call{
+ Call: &gmessaging.SiteCallRequest{
+ Name: srn.callRequest.Name,
+ Phone: srn.callRequest.Phone,
+ Email: srn.callRequest.Email,
+ Company: srn.callRequest.Company,
+ PreferredTime: srn.callRequest.PreferredTime,
+ Message: srn.callRequest.Message,
+ },
+ }
+ default:
+ return nil, merrors.InvalidArgument("unsupported site request type", "type")
+ }
+
+ data, err := proto.Marshal(&msg)
+ if err != nil {
+ return nil, err
+ }
+ return srn.Envelope.Wrap(data)
+}
+
+func newSiteRequestEvent() model.NotificationEvent {
+ return model.NewNotification(mservice.Site, nm.NACreated)
+}
+
+func NewDemoRequestEvent() model.NotificationEvent {
+ return newSiteRequestEvent()
+}
+
+func NewContactRequestEvent() model.NotificationEvent {
+ return newSiteRequestEvent()
+}
+
+func NewCallRequestEvent() model.NotificationEvent {
+ return newSiteRequestEvent()
+}
+
+func NewDemoRequestEnvelope(sender string, request *model.DemoRequest) messaging.Envelope {
+ return &SiteRequestNotification{
+ Envelope: messaging.CreateEnvelope(sender, newSiteRequestEvent()),
+ requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_DEMO,
+ demoRequest: request,
+ contactRequest: nil,
+ callRequest: nil,
+ }
+}
+
+func NewContactRequestEnvelope(sender string, request *model.ContactRequest) messaging.Envelope {
+ return &SiteRequestNotification{
+ Envelope: messaging.CreateEnvelope(sender, newSiteRequestEvent()),
+ requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_CONTACT,
+ contactRequest: request,
+ demoRequest: nil,
+ callRequest: nil,
+ }
+}
+
+func NewCallRequestEnvelope(sender string, request *model.CallRequest) messaging.Envelope {
+ return &SiteRequestNotification{
+ Envelope: messaging.CreateEnvelope(sender, newSiteRequestEvent()),
+ requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_CALL,
+ callRequest: request,
+ demoRequest: nil,
+ contactRequest: nil,
+ }
+}
diff --git a/api/pkg/messaging/notifications/confirmation/confirmation.go b/api/pkg/messaging/notifications/confirmation/confirmation.go
new file mode 100644
index 0000000..23ec6db
--- /dev/null
+++ b/api/pkg/messaging/notifications/confirmation/confirmation.go
@@ -0,0 +1,20 @@
+package notifications
+
+import (
+ "github.com/tech/sendico/pkg/db/account"
+ messaging "github.com/tech/sendico/pkg/messaging/envelope"
+ cinternal "github.com/tech/sendico/pkg/messaging/internal/notifications/confirmation"
+ ch "github.com/tech/sendico/pkg/messaging/notifications/confirmation/handler"
+ np "github.com/tech/sendico/pkg/messaging/notifications/processor"
+ "github.com/tech/sendico/pkg/mlogger"
+ "github.com/tech/sendico/pkg/model"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+func Code(sender string, accountRef primitive.ObjectID, destination string, target model.ConfirmationTarget, code string) messaging.Envelope {
+ return cinternal.NewConfirmationCodeEnvelope(sender, accountRef, destination, target, code)
+}
+
+func NewConfirmationCodeProcessor(logger mlogger.Logger, db account.DB, handler ch.ConfirmationCodeHandler) np.EnvelopeProcessor {
+ return cinternal.NewConfirmationCodeProcessor(logger, db, handler)
+}
diff --git a/api/pkg/messaging/notifications/confirmation/handler/interface.go b/api/pkg/messaging/notifications/confirmation/handler/interface.go
new file mode 100644
index 0000000..a7c2477
--- /dev/null
+++ b/api/pkg/messaging/notifications/confirmation/handler/interface.go
@@ -0,0 +1,9 @@
+package handler
+
+import (
+ "context"
+
+ "github.com/tech/sendico/pkg/model"
+)
+
+type ConfirmationCodeHandler = func(context.Context, *model.Account, string, model.ConfirmationTarget, string) error
diff --git a/api/pkg/messaging/notifications/site/call_request.go b/api/pkg/messaging/notifications/site/call_request.go
new file mode 100644
index 0000000..b1d92ad
--- /dev/null
+++ b/api/pkg/messaging/notifications/site/call_request.go
@@ -0,0 +1,11 @@
+package notifications
+
+import (
+ messaging "github.com/tech/sendico/pkg/messaging/envelope"
+ internalsite "github.com/tech/sendico/pkg/messaging/internal/notifications/site"
+ "github.com/tech/sendico/pkg/model"
+)
+
+func CallRequest(sender string, request *model.CallRequest) messaging.Envelope {
+ return internalsite.NewCallRequestEnvelope(sender, request)
+}
diff --git a/api/pkg/messaging/notifications/site/contact_request.go b/api/pkg/messaging/notifications/site/contact_request.go
new file mode 100644
index 0000000..fbf8c6d
--- /dev/null
+++ b/api/pkg/messaging/notifications/site/contact_request.go
@@ -0,0 +1,11 @@
+package notifications
+
+import (
+ messaging "github.com/tech/sendico/pkg/messaging/envelope"
+ internalsite "github.com/tech/sendico/pkg/messaging/internal/notifications/site"
+ "github.com/tech/sendico/pkg/model"
+)
+
+func ContactRequest(sender string, request *model.ContactRequest) messaging.Envelope {
+ return internalsite.NewContactRequestEnvelope(sender, request)
+}
diff --git a/api/pkg/messaging/notifications/site/demo_request.go b/api/pkg/messaging/notifications/site/demo_request.go
new file mode 100644
index 0000000..bb21277
--- /dev/null
+++ b/api/pkg/messaging/notifications/site/demo_request.go
@@ -0,0 +1,11 @@
+package notifications
+
+import (
+ messaging "github.com/tech/sendico/pkg/messaging/envelope"
+ internalsite "github.com/tech/sendico/pkg/messaging/internal/notifications/site"
+ "github.com/tech/sendico/pkg/model"
+)
+
+func DemoRequest(sender string, request *model.DemoRequest) messaging.Envelope {
+ return internalsite.NewDemoRequestEnvelope(sender, request)
+}
diff --git a/api/pkg/messaging/notifications/site/handler/handler.go b/api/pkg/messaging/notifications/site/handler/handler.go
new file mode 100644
index 0000000..54a369c
--- /dev/null
+++ b/api/pkg/messaging/notifications/site/handler/handler.go
@@ -0,0 +1,11 @@
+package notifications
+
+import (
+ "context"
+
+ "github.com/tech/sendico/pkg/model"
+)
+
+type DemoRequestHandler = func(context.Context, *model.DemoRequest) error
+type ContactRequestHandler = func(context.Context, *model.ContactRequest) error
+type CallRequestHandler = func(context.Context, *model.CallRequest) error
diff --git a/api/pkg/messaging/notifications/site/processor.go b/api/pkg/messaging/notifications/site/processor.go
new file mode 100644
index 0000000..5952340
--- /dev/null
+++ b/api/pkg/messaging/notifications/site/processor.go
@@ -0,0 +1,108 @@
+package notifications
+
+import (
+ "context"
+
+ gmessaging "github.com/tech/sendico/pkg/generated/gmessaging"
+ me "github.com/tech/sendico/pkg/messaging/envelope"
+ internalsite "github.com/tech/sendico/pkg/messaging/internal/notifications/site"
+ np "github.com/tech/sendico/pkg/messaging/notifications/processor"
+ handler "github.com/tech/sendico/pkg/messaging/notifications/site/handler"
+ "github.com/tech/sendico/pkg/mlogger"
+ "github.com/tech/sendico/pkg/model"
+ "go.uber.org/zap"
+ "google.golang.org/protobuf/proto"
+)
+
+type SiteRequestProcessor struct {
+ logger mlogger.Logger
+ demoHandler handler.DemoRequestHandler
+ contactHandler handler.ContactRequestHandler
+ callHandler handler.CallRequestHandler
+ event model.NotificationEvent
+}
+
+func (srp *SiteRequestProcessor) Process(ctx context.Context, envelope me.Envelope) error {
+ var msg gmessaging.SiteRequestEvent
+ if err := proto.Unmarshal(envelope.GetData(), &msg); err != nil {
+ srp.logger.Warn("Failed to decode site request envelope", zap.Error(err), zap.String("topic", srp.event.ToString()))
+ return err
+ }
+
+ switch msg.GetType() {
+ case gmessaging.SiteRequestEvent_REQUEST_TYPE_DEMO:
+ if srp.demoHandler == nil {
+ srp.logger.Warn("Demo request handler is not configured")
+ return nil
+ }
+ demo := msg.GetDemo()
+ if demo == nil {
+ srp.logger.Warn("Demo request payload is empty")
+ return nil
+ }
+ request := &model.DemoRequest{
+ Name: demo.GetName(),
+ OrganizationName: demo.GetOrganizationName(),
+ Phone: demo.GetPhone(),
+ WorkEmail: demo.GetWorkEmail(),
+ PayoutVolume: demo.GetPayoutVolume(),
+ Comment: demo.GetComment(),
+ }
+ return srp.demoHandler(ctx, request)
+ case gmessaging.SiteRequestEvent_REQUEST_TYPE_CONTACT:
+ if srp.contactHandler == nil {
+ srp.logger.Warn("Contact request handler is not configured")
+ return nil
+ }
+ contact := msg.GetContact()
+ if contact == nil {
+ srp.logger.Warn("Contact request payload is empty")
+ return nil
+ }
+ request := &model.ContactRequest{
+ Name: contact.GetName(),
+ Email: contact.GetEmail(),
+ Phone: contact.GetPhone(),
+ Company: contact.GetCompany(),
+ Topic: contact.GetTopic(),
+ Message: contact.GetMessage(),
+ }
+ return srp.contactHandler(ctx, request)
+ case gmessaging.SiteRequestEvent_REQUEST_TYPE_CALL:
+ if srp.callHandler == nil {
+ srp.logger.Warn("Call request handler is not configured")
+ return nil
+ }
+ call := msg.GetCall()
+ if call == nil {
+ srp.logger.Warn("Call request payload is empty")
+ return nil
+ }
+ request := &model.CallRequest{
+ Name: call.GetName(),
+ Phone: call.GetPhone(),
+ Email: call.GetEmail(),
+ Company: call.GetCompany(),
+ PreferredTime: call.GetPreferredTime(),
+ Message: call.GetMessage(),
+ }
+ return srp.callHandler(ctx, request)
+ default:
+ srp.logger.Warn("Received site request with unsupported type", zap.Any("type", msg.GetType()))
+ return nil
+ }
+}
+
+func (srp *SiteRequestProcessor) GetSubject() model.NotificationEvent {
+ return srp.event
+}
+
+func NewSiteRequestProcessor(logger mlogger.Logger, demo handler.DemoRequestHandler, contact handler.ContactRequestHandler, call handler.CallRequestHandler) np.EnvelopeProcessor {
+ return &SiteRequestProcessor{
+ logger: logger.Named("site_request_processor"),
+ demoHandler: demo,
+ contactHandler: contact,
+ callHandler: call,
+ event: internalsite.NewDemoRequestEvent(),
+ }
+}
diff --git a/api/pkg/model/account.go b/api/pkg/model/account.go
index 09cae4f..271f843 100755
--- a/api/pkg/model/account.go
+++ b/api/pkg/model/account.go
@@ -13,6 +13,7 @@ type AccountBase struct {
storable.Base `bson:",inline" json:",inline"`
ArchivableBase `bson:",inline" json:",inline"`
Describable `bson:",inline" json:",inline"`
+ LastName string `bson:"lastName" json:"lastName"`
AvatarURL *string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"`
}
diff --git a/api/pkg/model/callrequest.go b/api/pkg/model/callrequest.go
new file mode 100644
index 0000000..d237c42
--- /dev/null
+++ b/api/pkg/model/callrequest.go
@@ -0,0 +1,41 @@
+package model
+
+import (
+ "strings"
+
+ "github.com/tech/sendico/pkg/merrors"
+)
+
+// CallRequest represents a request to schedule a call from the marketing site.
+type CallRequest struct {
+ Name string `json:"name"`
+ Phone string `json:"phone"`
+ Email string `json:"email"`
+ Company string `json:"company"`
+ PreferredTime string `json:"preferredTime"`
+ Message string `json:"message"`
+}
+
+// Normalize trims whitespace from all string fields.
+func (cr *CallRequest) Normalize() {
+ if cr == nil {
+ return
+ }
+ cr.Name = strings.TrimSpace(cr.Name)
+ cr.Phone = strings.TrimSpace(cr.Phone)
+ cr.Email = strings.TrimSpace(cr.Email)
+ cr.Company = strings.TrimSpace(cr.Company)
+ cr.PreferredTime = strings.TrimSpace(cr.PreferredTime)
+ cr.Message = strings.TrimSpace(cr.Message)
+}
+
+// Validate ensures required call request fields are present.
+func (cr *CallRequest) Validate() error {
+ if cr == nil {
+ return merrors.InvalidArgument("request payload is empty", "request")
+ }
+ if cr.Phone == "" && cr.Email == "" {
+ return merrors.InvalidArgument("phone or email must not be empty", "request.phone", "request.email")
+ }
+ return nil
+}
diff --git a/api/pkg/model/confirmation.go b/api/pkg/model/confirmation.go
new file mode 100644
index 0000000..1fdcfc6
--- /dev/null
+++ b/api/pkg/model/confirmation.go
@@ -0,0 +1,43 @@
+package model
+
+import (
+ "time"
+
+ "github.com/tech/sendico/pkg/mservice"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type ConfirmationTarget string
+
+const (
+ ConfirmationTargetLogin ConfirmationTarget = "login"
+ ConfirmationTargetPayout ConfirmationTarget = "payout"
+)
+
+// ConfirmationCode stores verification codes for operations like login or payouts.
+type ConfirmationCode struct {
+ AccountBoundBase `bson:",inline" json:",inline"`
+
+ Destination string `bson:"destination" json:"destination"`
+ Target ConfirmationTarget `bson:"target" json:"target"`
+ CodeHash []byte `bson:"codeHash" json:"-"`
+ Salt []byte `bson:"salt" json:"-"`
+ ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
+ Attempts int `bson:"attempts" json:"attempts"`
+ MaxAttempts int `bson:"maxAttempts" json:"maxAttempts"`
+ ResendCount int `bson:"resendCount" json:"resendCount"`
+ ResendLimit int `bson:"resendLimit" json:"resendLimit"`
+ CooldownUntil time.Time `bson:"cooldownUntil" json:"cooldownUntil"`
+ Used bool `bson:"used" json:"used"`
+}
+
+func (c *ConfirmationCode) Collection() string {
+ return mservice.Confirmations
+}
+
+func NewConfirmationCode(accountRef primitive.ObjectID) *ConfirmationCode {
+ cc := &ConfirmationCode{}
+ cc.SetID(primitive.NewObjectID())
+ cc.AccountRef = &accountRef
+ return cc
+}
diff --git a/api/pkg/model/contactrequest.go b/api/pkg/model/contactrequest.go
new file mode 100644
index 0000000..3bdc513
--- /dev/null
+++ b/api/pkg/model/contactrequest.go
@@ -0,0 +1,41 @@
+package model
+
+import (
+ "strings"
+
+ "github.com/tech/sendico/pkg/merrors"
+)
+
+// ContactRequest represents a contact form submission from the marketing site.
+type ContactRequest struct {
+ Name string `json:"name"`
+ Email string `json:"email"`
+ Phone string `json:"phone"`
+ Company string `json:"company"`
+ Topic string `json:"topic"`
+ Message string `json:"message"`
+}
+
+// Normalize trims whitespace from all string fields.
+func (cr *ContactRequest) Normalize() {
+ if cr == nil {
+ return
+ }
+ cr.Name = strings.TrimSpace(cr.Name)
+ cr.Email = strings.TrimSpace(cr.Email)
+ cr.Phone = strings.TrimSpace(cr.Phone)
+ cr.Company = strings.TrimSpace(cr.Company)
+ cr.Topic = strings.TrimSpace(cr.Topic)
+ cr.Message = strings.TrimSpace(cr.Message)
+}
+
+// Validate ensures required contact request fields are present.
+func (cr *ContactRequest) Validate() error {
+ if cr == nil {
+ return merrors.InvalidArgument("request payload is empty", "request")
+ }
+ if (cr.Email == "") && (cr.Phone == "") {
+ return merrors.InvalidArgument("email or phone must not be empty", "request.email", "request.phone")
+ }
+ return nil
+}
diff --git a/api/pkg/model/contactrequest_test.go b/api/pkg/model/contactrequest_test.go
new file mode 100644
index 0000000..0dc49cc
--- /dev/null
+++ b/api/pkg/model/contactrequest_test.go
@@ -0,0 +1,31 @@
+package model
+
+import "testing"
+
+func TestContactRequestNormalizeAndValidate(t *testing.T) {
+ req := &ContactRequest{
+ Name: " Alice ",
+ Email: " alice@example.com ",
+ Phone: " +1 234 ",
+ Company: " Sendico ",
+ Topic: " General question ",
+ Message: " Hello team ",
+ }
+
+ req.Normalize()
+ if err := req.Validate(); err != nil {
+ t.Fatalf("expected request to be valid, got error: %v", err)
+ }
+
+ if req.Name != "Alice" || req.Email != "alice@example.com" || req.Phone != "+1 234" || req.Company != "Sendico" || req.Topic != "General question" || req.Message != "Hello team" {
+ t.Fatalf("normalize failed: %+v", req)
+ }
+}
+
+func TestContactRequestValidateMissing(t *testing.T) {
+ req := &ContactRequest{}
+ req.Normalize()
+ if err := req.Validate(); err == nil {
+ t.Fatalf("expected validation error for empty request")
+ }
+}
diff --git a/api/pkg/model/demorequest.go b/api/pkg/model/demorequest.go
new file mode 100644
index 0000000..6bbe605
--- /dev/null
+++ b/api/pkg/model/demorequest.go
@@ -0,0 +1,41 @@
+package model
+
+import (
+ "strings"
+
+ "github.com/tech/sendico/pkg/merrors"
+)
+
+// DemoRequest represents a request submitted from the marketing site to request a demo.
+type DemoRequest struct {
+ Name string `json:"name"`
+ OrganizationName string `json:"organizationName"`
+ Phone string `json:"phone"`
+ WorkEmail string `json:"workEmail"`
+ PayoutVolume string `json:"payoutVolume"`
+ Comment string `json:"comment,omitempty"`
+}
+
+// Normalize trims whitespace from all string fields.
+func (dr *DemoRequest) Normalize() {
+ if dr == nil {
+ return
+ }
+ dr.Name = strings.TrimSpace(dr.Name)
+ dr.OrganizationName = strings.TrimSpace(dr.OrganizationName)
+ dr.Phone = strings.TrimSpace(dr.Phone)
+ dr.WorkEmail = strings.TrimSpace(dr.WorkEmail)
+ dr.PayoutVolume = strings.TrimSpace(dr.PayoutVolume)
+ dr.Comment = strings.TrimSpace(dr.Comment)
+}
+
+// Validate ensures that all required fields are present.
+func (dr *DemoRequest) Validate() error {
+ if dr == nil {
+ return merrors.InvalidArgument("request payload is empty", "request")
+ }
+ if (dr.WorkEmail == "") && (dr.Phone == "") {
+ return merrors.InvalidArgument("work email or phone must not be empty", "request.workEmail", "request.phone")
+ }
+ return nil
+}
diff --git a/api/pkg/model/demorequest_test.go b/api/pkg/model/demorequest_test.go
new file mode 100644
index 0000000..428f386
--- /dev/null
+++ b/api/pkg/model/demorequest_test.go
@@ -0,0 +1,31 @@
+package model
+
+import "testing"
+
+func TestDemoRequestNormalizeAndValidate(t *testing.T) {
+ req := &DemoRequest{
+ Name: " Alice ",
+ OrganizationName: " Sendico ",
+ Phone: " +1 234 ",
+ WorkEmail: " demo@sendico.io ",
+ PayoutVolume: " 100k ",
+ Comment: " Excited ",
+ }
+
+ req.Normalize()
+ if err := req.Validate(); err != nil {
+ t.Fatalf("expected request to be valid, got error: %v", err)
+ }
+
+ if req.Name != "Alice" || req.OrganizationName != "Sendico" || req.Phone != "+1 234" || req.WorkEmail != "demo@sendico.io" || req.PayoutVolume != "100k" || req.Comment != "Excited" {
+ t.Fatalf("normalize failed: %+v", req)
+ }
+}
+
+func TestDemoRequestValidateMissing(t *testing.T) {
+ req := &DemoRequest{}
+ req.Normalize()
+ if err := req.Validate(); err == nil {
+ t.Fatalf("expected validation error for empty request")
+ }
+}
diff --git a/api/pkg/model/notificationevent.go b/api/pkg/model/notificationevent.go
index de52d20..d6d0024 100644
--- a/api/pkg/model/notificationevent.go
+++ b/api/pkg/model/notificationevent.go
@@ -71,7 +71,12 @@ func FromString(s string) (*NotificationEventImp, error) {
func StringToNotificationAction(s string) (nm.NotificationAction, error) {
switch nm.NotificationAction(s) {
- case nm.NACreated, nm.NAPending, nm.NAUpdated, nm.NADeleted, nm.NAAssigned, nm.NAPasswordReset:
+ case nm.NACreated,
+ nm.NAPending,
+ nm.NAUpdated,
+ nm.NADeleted,
+ nm.NAAssigned,
+ nm.NAPasswordReset:
return nm.NotificationAction(s), nil
default:
return "", merrors.DataConflict("invalid Notification action: " + s)
diff --git a/api/pkg/model/userdata.go b/api/pkg/model/userdata.go
index 892b7ad..761d5d0 100644
--- a/api/pkg/model/userdata.go
+++ b/api/pkg/model/userdata.go
@@ -11,17 +11,16 @@ type LoginData struct {
}
type AccountData struct {
- LoginData `bson:",inline" json:",inline"`
- Name string `bson:"name" json:"name"`
+ LoginData `bson:",inline" json:",inline"`
+ Describable `bson:",inline" json:",inline"`
+ LastName string `bson:"lastName" json:"lastName"`
}
func (ad *AccountData) ToAccount() *Account {
return &Account{
AccountPublic: AccountPublic{
AccountBase: AccountBase{
- Describable: Describable{
- Name: ad.Name,
- },
+ Describable: ad.Describable,
},
UserDataBase: ad.UserDataBase,
},
diff --git a/api/pkg/mservice/services.go b/api/pkg/mservice/services.go
index 9154426..51b3b47 100644
--- a/api/pkg/mservice/services.go
+++ b/api/pkg/mservice/services.go
@@ -6,11 +6,11 @@ type Type = string
const (
Accounts Type = "accounts" // Represents user accounts in the system
+ Confirmations Type = "confirmations" // Represents confirmation code flows
Amplitude Type = "amplitude" // Represents analytics integration with Amplitude
- Automations Type = "automation" // Represents automation workflows
+ Site Type = "site" // Represents public site endpoints
Changes Type = "changes" // Tracks changes made to resources
Clients Type = "clients" // Represents client information
- Comments Type = "comments" // Represents comments on tasks or other resources
ChainGateway Type = "chain_gateway" // Represents chain gateway microservice
FXOracle Type = "fx_oracle" // Represents FX oracle microservice
FeePlans Type = "fee_plans" // Represents fee plans microservice
@@ -36,37 +36,22 @@ const (
Permissions Type = "permissions" // Represents permissiosns service
Policies Type = "policies" // Represents access control policies
PolicyAssignements Type = "policy_assignments" // Represents policy assignments database
- Priorities Type = "priorities" // Represents object properties
- PriorityGroups Type = "priority_groups" // Represents task or project priorities
- Projects Type = "projects" // Represents projects managed in the system
- PropertyBindings Type = "property_bindings" // Represents properties bindings of resources
- PropertySchemas Type = "property_schemas" // Represents properties or attributes of resources
- Properties Type = "properties" // Represents property values of the propertites of specific objects
- Reactions Type = "reactions" // Represents comment reactions
RefreshTokens Type = "refresh_tokens" // Represents refresh tokens for authentication
Roles Type = "roles" // Represents roles in access control
- Statuses Type = "statuses" // Represents statuses of tasks or projects
- StatusGroups Type = "status_groups" // Represents status groups
- Steps Type = "steps" // Represents steps in workflows or processes
Storage Type = "storage" // Represents statuses of tasks or projects
- Tags Type = "tags" // Represents tags managed in the system
- Tasks Type = "tasks" // Represents tasks managed in the system
- Teams Type = "teams" // Represents teams managed in the system
Tenants Type = "tenants" // Represents tenants managed in the system
Workflows Type = "workflows" // Represents workflows for tasks or projects
- Workspaces Type = "workspaces" // Represents workspaces containing projects and teams
)
func StringToSType(s string) (Type, error) {
switch Type(s) {
- case Accounts, Amplitude, Automations, Changes, Clients, Comments, ChainGateway, ChainWallets, ChainWalletBalances,
+ case Accounts, Confirmations, Amplitude, Site, Changes, Clients, ChainGateway, ChainWallets, ChainWalletBalances,
ChainTransfers, ChainDeposits, FXOracle, FeePlans, FilterProjects, Invitations, Invoices, Logo, Ledger,
LedgerAccounts, LedgerBalances, LedgerEntries, LedgerOutbox, LedgerParties, LedgerPlines, Notifications,
- Organizations, Payments, PaymentOrchestrator, Permissions, Policies, PolicyAssignements, Priorities,
- PriorityGroups, Projects, PropertyBindings, PropertySchemas, Properties, Reactions, RefreshTokens, Roles,
- Statuses, StatusGroups, Steps, Storage, Tags, Tasks, Teams, Tenants, Workflows, Workspaces:
+ Organizations, Payments, PaymentOrchestrator, Permissions, Policies, PolicyAssignements,
+ RefreshTokens, Roles, Storage, Tenants, Workflows:
return Type(s), nil
default:
- return "", merrors.DataConflict("invalid service type: " + s)
+ return "", merrors.InvalidArgument("invalid service type", s)
}
}
diff --git a/api/pkg/mutil/reorder/reorder.go b/api/pkg/mutil/reorder/reorder.go
index 891e797..14f02d6 100644
--- a/api/pkg/mutil/reorder/reorder.go
+++ b/api/pkg/mutil/reorder/reorder.go
@@ -17,12 +17,12 @@ func IndexableRefs(items []model.IndexableRef, oldIndex, newIndex int) ([]model.
}
}
if targetIndex == -1 {
- return nil, merrors.InvalidArgument("Item not found at specified index")
+ return nil, merrors.InvalidArgument("Item not found at specified index", "oldIndex")
}
// Validate new index bounds
if newIndex < 0 || newIndex >= len(items) {
- return nil, merrors.InvalidArgument("Invalid new index for reorder")
+ return nil, merrors.InvalidArgument("Invalid new index for reorder", "newIndex")
}
// Remove the item from its current position
diff --git a/api/pkg/server/grpcapp/app.go b/api/pkg/server/grpcapp/app.go
index d2d0b6d..8a35d17 100644
--- a/api/pkg/server/grpcapp/app.go
+++ b/api/pkg/server/grpcapp/app.go
@@ -53,13 +53,13 @@ type App[T any] struct {
func NewApp[T any](logger mlogger.Logger, name string, config *Config, debug bool, repoFactory RepositoryFactory[T], serviceFactory ServiceFactory[T], opts ...Option[T]) (*App[T], error) {
if logger == nil {
- return nil, merrors.InvalidArgument("nil logger supplied")
+ return nil, merrors.InvalidArgument("nil logger supplied", "logger")
}
if config == nil {
- return nil, merrors.InvalidArgument("nil config supplied")
+ return nil, merrors.InvalidArgument("nil config supplied", "config")
}
if serviceFactory == nil {
- return nil, merrors.InvalidArgument("nil service factory supplied")
+ return nil, merrors.InvalidArgument("nil service factory supplied", "serviceFactory")
}
app := &App[T]{
diff --git a/api/proto/site_request.proto b/api/proto/site_request.proto
new file mode 100644
index 0000000..98db4e6
--- /dev/null
+++ b/api/proto/site_request.proto
@@ -0,0 +1,47 @@
+syntax = "proto3";
+
+option go_package = "github.com/tech/sendico/pkg/generated/gmessaging";
+
+message SiteRequestEvent {
+ enum RequestType {
+ REQUEST_TYPE_UNSPECIFIED = 0;
+ REQUEST_TYPE_DEMO = 1;
+ REQUEST_TYPE_CONTACT = 2;
+ REQUEST_TYPE_CALL = 3;
+ }
+
+ RequestType type = 1;
+
+ oneof payload {
+ SiteDemoRequest demo = 2;
+ SiteContactRequest contact = 3;
+ SiteCallRequest call = 4;
+ }
+}
+
+message SiteDemoRequest {
+ string name = 1;
+ string organization_name = 2;
+ string phone = 3;
+ string work_email = 4;
+ string payout_volume = 5;
+ string comment = 6;
+}
+
+message SiteContactRequest {
+ string name = 1;
+ string email = 2;
+ string phone = 3;
+ string company = 4;
+ string topic = 5;
+ string message = 6;
+}
+
+message SiteCallRequest {
+ string name = 1;
+ string phone = 2;
+ string email = 3;
+ string company = 4;
+ string preferred_time = 5;
+ string message = 6;
+}
diff --git a/api/server/config.yml b/api/server/config.yml
index 6b401da..cb4c79e 100755
--- a/api/server/config.yml
+++ b/api/server/config.yml
@@ -16,8 +16,9 @@ api:
CORS:
max_age: 300
allowed_origins:
- - "http://*"
- - "https://*"
+ - "https://sendico.io"
+ - "https://app.sendico.io"
+ - "https://www.sendico.io"
allowed_methods:
- "GET"
- "POST"
@@ -70,9 +71,20 @@ api:
# 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
+ driver: local_fs
+ settings:
+ root_path: ./storage
+
+ chain_gateway:
+ address: sendico_chain_gateway:50070
+ address_env: CHAIN_GATEWAY_ADDRESS
+ dial_timeout_seconds: 5
+ call_timeout_seconds: 5
+ insecure: true
+ default_asset:
+ chain: ARBITRUM_ONE
+ token_symbol: USDT
+ contract_address: ""
app:
@@ -94,4 +106,4 @@ database:
collection_name_env: PERMISSION_COLLECTION
database_name_env: MONGO_DATABASE
timeout_seconds_env: PERMISSION_TIMEOUT
- is_filtered_env: PERMISSION_IS_FILTERED
\ No newline at end of file
+ is_filtered_env: PERMISSION_IS_FILTERED
diff --git a/api/server/go.mod b/api/server/go.mod
index 940de09..27cea8c 100644
--- a/api/server/go.mod
+++ b/api/server/go.mod
@@ -4,22 +4,26 @@ go 1.25.3
replace github.com/tech/sendico/pkg => ../pkg
+replace github.com/tech/sendico/chain/gateway => ../chain/gateway
+
require (
- github.com/aws/aws-sdk-go-v2 v1.39.6
- github.com/aws/aws-sdk-go-v2/config v1.31.18
- github.com/aws/aws-sdk-go-v2/credentials v1.18.22
- github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
+ github.com/aws/aws-sdk-go-v2 v1.40.0
+ github.com/aws/aws-sdk-go-v2/config v1.32.0
+ github.com/aws/aws-sdk-go-v2/credentials v1.19.0
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/go-chi/jwtauth/v5 v5.3.3
github.com/go-chi/metrics v0.1.1
+ github.com/google/uuid v1.6.0
github.com/mitchellh/mapstructure v1.5.0
github.com/stretchr/testify v1.11.1
+ github.com/tech/sendico/chain/gateway v0.1.0
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 v1.17.6
- go.uber.org/zap v1.27.0
+ go.uber.org/zap v1.27.1
golang.org/x/net v0.47.0
gopkg.in/yaml.v3 v3.0.1
moul.io/chizap v1.0.3
@@ -27,7 +31,7 @@ require (
require (
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
- github.com/casbin/casbin/v2 v2.132.0 // indirect
+ github.com/casbin/casbin/v2 v2.134.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
)
@@ -36,18 +40,19 @@ require (
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.3 // indirect
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // 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.13 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
- github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect
+ github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 // indirect
github.com/aws/smithy-go v1.23.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
@@ -70,7 +75,6 @@ require (
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v1.0.0 // indirect
- github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
@@ -101,33 +105,33 @@ require (
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.2 // indirect
+ github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // 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.15 // indirect
- github.com/tklauser/numcpus v0.10.0 // indirect
+ github.com/tklauser/go-sysconf v0.3.16 // indirect
+ github.com/tklauser/numcpus v0.11.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // 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.1.0 // 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.37.0 // indirect
+ go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
- go.opentelemetry.io/otel/metric v1.37.0 // indirect
- go.opentelemetry.io/otel/trace v1.37.0 // indirect
+ go.opentelemetry.io/otel/metric v1.38.0 // indirect
+ go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
- golang.org/x/crypto v0.44.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
- google.golang.org/grpc v1.76.0 // indirect
+ google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
diff --git a/api/server/go.sum b/api/server/go.sum
index b1939fb..e2d491e 100644
--- a/api/server/go.sum
+++ b/api/server/go.sum
@@ -6,40 +6,42 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
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.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
-github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
+github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
+github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
-github.com/aws/aws-sdk-go-v2/config v1.31.18 h1:RouG3AcF2fLFhw+Z0qbnuIl9HZ0Kh4E/U9sKwTMRpMI=
-github.com/aws/aws-sdk-go-v2/config v1.31.18/go.mod h1:aXZ13mSQC8S2VEHwGfL1COMuJ1Zty6pX5xU7hyqjvCg=
-github.com/aws/aws-sdk-go-v2/credentials v1.18.22 h1:hyIVGBHhQPaNP9D4BaVRwpjLMCwMMdAkHqB3gGMiykU=
-github.com/aws/aws-sdk-go-v2/credentials v1.18.22/go.mod h1:B9E2qHs3/YGfeQZ4jrIE/nPvqxtyafZrJ5EQiZBG6pk=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
+github.com/aws/aws-sdk-go-v2/config v1.32.0 h1:T5WWJYnam9SzBLbsVYDu2HscLDe+GU1AUJtfcDAc/vA=
+github.com/aws/aws-sdk-go-v2/config v1.32.0/go.mod h1:pSRm/+D3TxBixGMXlgtX4+MPO9VNtEEtiFmNpxksoxw=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.0 h1:7zm+ez+qEqLaNsCSRaistkvJRJv8sByDOVuCnyHbP7M=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.0/go.mod h1:pHKPblrT7hqFGkNLxqoS3FlGoPrQg4hMIa+4asZzBfs=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
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.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
-github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 h1:ZGDJVmlpPFiNFCb/I42nYVKUanJAdFUiSmUo/32AqPQ=
-github.com/aws/aws-sdk-go-v2/service/sts v1.40.0/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0 h1:8FshVvnV2sr9kOSAbOnc/vwVmmAwMjOedKH6JW2ddPM=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 h1:U//SlnkE1wOQiIImxzdY5PXat4Wq+8rlfVEw4Y7J8as=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.4/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8 h1:MvlNs/f+9eM0mOjD9JzBUbf5jghyTk3p+O9yHMXX94Y=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 h1:GdGmKtG+/Krag7VfyOXV17xjTCz0i9NT+JnqLTOI5nA=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.1/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -48,8 +50,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
-github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk=
-github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
+github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
+github.com/casbin/casbin/v2 v2.134.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=
@@ -196,12 +198,12 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
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.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
-github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
+github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
+github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
-github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+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=
@@ -228,10 +230,10 @@ github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5
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.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
-github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
-github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
-github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
+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/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.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
@@ -248,24 +250,24 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
-go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
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.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
-go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
-go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
-go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
-go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
-go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
-go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@@ -278,16 +280,16 @@ go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95a
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.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+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.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
-golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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=
@@ -338,8 +340,8 @@ 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+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=
@@ -355,12 +357,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
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-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
-google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
+google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
+google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
-google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
-google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
+google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/api/server/interface/api/config.go b/api/server/interface/api/config.go
index bee2ca9..f21e3af 100644
--- a/api/server/interface/api/config.go
+++ b/api/server/interface/api/config.go
@@ -6,6 +6,22 @@ import (
)
type Config struct {
- Mw *mwa.Config `yaml:"middleware"`
- Storage *fsc.Config `yaml:"storage"`
+ Mw *mwa.Config `yaml:"middleware"`
+ Storage *fsc.Config `yaml:"storage"`
+ ChainGateway *ChainGatewayConfig `yaml:"chain_gateway"`
+}
+
+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"`
}
diff --git a/api/server/interface/api/register.go b/api/server/interface/api/register.go
index 4a46514..63683bd 100644
--- a/api/server/interface/api/register.go
+++ b/api/server/interface/api/register.go
@@ -11,6 +11,7 @@ import (
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
diff --git a/api/server/interface/api/srequest/signup.go b/api/server/interface/api/srequest/signup.go
index 2f96c04..208666d 100644
--- a/api/server/interface/api/srequest/signup.go
+++ b/api/server/interface/api/srequest/signup.go
@@ -4,9 +4,7 @@ import "github.com/tech/sendico/pkg/model"
type Signup struct {
Account model.AccountData `json:"account"`
- OrganizationName string `json:"organizationName"`
+ Organization model.Describable `json:"organization"`
OrganizationTimeZone string `json:"organizationTimeZone"`
- AnonymousUser model.Describable `json:"anonymousUser"`
OwnerRole model.Describable `json:"ownerRole"`
- AnonymousRole model.Describable `json:"anonymousRole"`
}
diff --git a/api/server/interface/api/srequest/signup_test.go b/api/server/interface/api/srequest/signup_test.go
index 0b284ef..f24d66a 100644
--- a/api/server/interface/api/srequest/signup_test.go
+++ b/api/server/interface/api/srequest/signup_test.go
@@ -20,19 +20,17 @@ func TestSignupRequest_JSONSerialization(t *testing.T) {
},
Password: "TestPassword123!",
},
- Name: "Test User",
+ Describable: model.Describable{
+ Name: "Test User",
+ },
+ },
+ Organization: model.Describable{
+ Name: "Test Organization",
},
- OrganizationName: "Test Organization",
OrganizationTimeZone: "UTC",
- AnonymousUser: model.Describable{
- Name: "Anonymous User",
- },
OwnerRole: model.Describable{
Name: "Owner",
},
- AnonymousRole: model.Describable{
- Name: "Anonymous",
- },
}
// Test JSON marshaling
@@ -49,11 +47,9 @@ func TestSignupRequest_JSONSerialization(t *testing.T) {
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.OrganizationName, unmarshaled.OrganizationName)
+ assert.Equal(t, signup.Organization.Name, unmarshaled.Organization.Name)
assert.Equal(t, signup.OrganizationTimeZone, unmarshaled.OrganizationTimeZone)
- assert.Equal(t, signup.AnonymousUser.Name, unmarshaled.AnonymousUser.Name)
assert.Equal(t, signup.OwnerRole.Name, unmarshaled.OwnerRole.Name)
- assert.Equal(t, signup.AnonymousRole.Name, unmarshaled.AnonymousRole.Name)
}
func TestSignupRequest_MinimalValidRequest(t *testing.T) {
@@ -65,19 +61,17 @@ func TestSignupRequest_MinimalValidRequest(t *testing.T) {
},
Password: "TestPassword123!",
},
- Name: "Test User",
+ Describable: model.Describable{
+ Name: "Test User",
+ },
+ },
+ Organization: model.Describable{
+ Name: "Test Organization",
},
- OrganizationName: "Test Organization",
OrganizationTimeZone: "UTC",
- AnonymousUser: model.Describable{
- Name: "Anonymous",
- },
OwnerRole: model.Describable{
Name: "Owner",
},
- AnonymousRole: model.Describable{
- Name: "Anonymous",
- },
}
// Test JSON marshaling
@@ -93,13 +87,13 @@ func TestSignupRequest_MinimalValidRequest(t *testing.T) {
// 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.OrganizationName, unmarshaled.OrganizationName)
+ assert.Equal(t, signup.Organization.Name, unmarshaled.Organization.Name)
}
func TestSignupRequest_InvalidJSON(t *testing.T) {
invalidJSONs := []string{
`{"account": invalid}`,
- `{"organizationName": 123}`,
+ `{"organization": 123}`,
`{"organizationTimeZone": true}`,
`{"defaultPriorityGroup": "not_an_object"}`,
`{"anonymousUser": []}`,
@@ -125,19 +119,17 @@ func TestSignupRequest_UnicodeCharacters(t *testing.T) {
},
Password: "TestPassword123!",
},
- Name: "Test 用户 Üser",
+ Describable: model.Describable{
+ Name: "Test 用户 Üser",
+ },
+ },
+ Organization: model.Describable{
+ Name: "测试 Organization",
},
- OrganizationName: "测试 Organization",
OrganizationTimeZone: "UTC",
- AnonymousUser: model.Describable{
- Name: "匿名 User",
- },
OwnerRole: model.Describable{
Name: "所有者",
},
- AnonymousRole: model.Describable{
- Name: "匿名",
- },
}
// Test JSON marshaling
@@ -153,8 +145,6 @@ func TestSignupRequest_UnicodeCharacters(t *testing.T) {
// 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.OrganizationName)
- assert.Equal(t, "匿名 User", unmarshaled.AnonymousUser.Name)
+ assert.Equal(t, "测试 Organization", unmarshaled.Organization.Name)
assert.Equal(t, "所有者", unmarshaled.OwnerRole.Name)
- assert.Equal(t, "匿名", unmarshaled.AnonymousRole.Name)
}
diff --git a/api/server/interface/api/sresponse/login_pending.go b/api/server/interface/api/sresponse/login_pending.go
new file mode 100644
index 0000000..5ef2d4a
--- /dev/null
+++ b/api/server/interface/api/sresponse/login_pending.go
@@ -0,0 +1,31 @@
+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"`
+ Destination string `json:"destination"`
+ TTLSeconds int `json:"ttlSeconds"`
+}
+
+func LoginPending(logger mlogger.Logger, account *model.Account, pendingToken *TokenData, destination string, ttlSeconds int) http.HandlerFunc {
+ return response.Accepted(
+ logger,
+ &pendingLoginResponse{
+ Account: accountResponse{
+ Account: *_createAccount(account, false),
+ authResponse: authResponse{},
+ },
+ PendingToken: *pendingToken,
+ Destination: destination,
+ TTLSeconds: ttlSeconds,
+ },
+ )
+}
diff --git a/api/server/interface/api/sresponse/response.go b/api/server/interface/api/sresponse/response.go
index 04a6cf8..8bd7211 100644
--- a/api/server/interface/api/sresponse/response.go
+++ b/api/server/interface/api/sresponse/response.go
@@ -4,9 +4,11 @@ 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
+ 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
)
diff --git a/api/server/interface/api/sresponse/signupavailability.go b/api/server/interface/api/sresponse/signupavailability.go
new file mode 100644
index 0000000..f5ca161
--- /dev/null
+++ b/api/server/interface/api/sresponse/signupavailability.go
@@ -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,
+ },
+ )
+}
diff --git a/api/server/interface/model/token.go b/api/server/interface/model/token.go
index 7f51ba6..9390ab6 100644
--- a/api/server/interface/model/token.go
+++ b/api/server/interface/model/token.go
@@ -17,6 +17,7 @@ type AccountToken struct {
Name string
Locale string
Expiration time.Time
+ Pending bool
}
func createAccountToken(a *model.Account, expiration int) AccountToken {
@@ -26,6 +27,7 @@ func createAccountToken(a *model.Account, expiration int) AccountToken {
Name: a.Name,
Locale: a.Locale,
Expiration: time.Now().Add(mduration.Param2Duration(expiration, time.Hour)),
+ Pending: false,
}
}
@@ -44,6 +46,7 @@ const (
paramNameLocale = "locale"
paramNameLogin = "login"
paramNameExpiration = "exp"
+ paramNamePending = "pending"
)
func Claims2Token(claims middleware.MapClaims) (*AccountToken, error) {
@@ -65,6 +68,11 @@ func Claims2Token(claims middleware.MapClaims) (*AccountToken, error) {
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:
@@ -90,5 +98,20 @@ func Account2Claims(a *model.Account, expiration int) middleware.MapClaims {
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,
}
}
diff --git a/api/server/interface/services/confirmation/confirmation.go b/api/server/interface/services/confirmation/confirmation.go
new file mode 100644
index 0000000..39cc975
--- /dev/null
+++ b/api/server/interface/services/confirmation/confirmation.go
@@ -0,0 +1,11 @@
+package confirmation
+
+import (
+ "github.com/tech/sendico/pkg/mservice"
+ "github.com/tech/sendico/server/interface/api"
+ "github.com/tech/sendico/server/internal/server/confirmationimp"
+)
+
+func Create(a api.API) (mservice.MicroService, error) {
+ return confirmationimp.CreateAPI(a)
+}
diff --git a/api/server/interface/services/site/site.go b/api/server/interface/services/site/site.go
new file mode 100644
index 0000000..5b1ff53
--- /dev/null
+++ b/api/server/interface/services/site/site.go
@@ -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)
+}
diff --git a/api/server/internal/api/api.go b/api/server/internal/api/api.go
index 6a255ec..d37506f 100644
--- a/api/server/internal/api/api.go
+++ b/api/server/internal/api/api.go
@@ -12,10 +12,12 @@ import (
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/interface/services/account"
+ "github.com/tech/sendico/server/interface/services/confirmation"
"github.com/tech/sendico/server/interface/services/invitation"
"github.com/tech/sendico/server/interface/services/logo"
"github.com/tech/sendico/server/interface/services/organization"
"github.com/tech/sendico/server/interface/services/permission"
+ "github.com/tech/sendico/server/interface/services/site"
"go.uber.org/zap"
)
@@ -75,10 +77,12 @@ func (a *APIImp) installServices() error {
srvf := make([]api.MicroServiceFactoryT, 0)
srvf = append(srvf, account.Create)
+ srvf = append(srvf, confirmation.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)
for _, v := range srvf {
if err := a.addMicroservice(v); err != nil {
diff --git a/api/server/internal/api/middleware.go b/api/server/internal/api/middleware.go
index 3642cca..c024582 100644
--- a/api/server/internal/api/middleware.go
+++ b/api/server/internal/api/middleware.go
@@ -45,6 +45,10 @@ func (mw *Middleware) AccountHandler(service mservice.Type, endpoint string, met
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)
}
@@ -97,6 +101,7 @@ func (mw *Middleware) installMiddleware(config *middleware.Config, debug bool) {
}))
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) {
@@ -132,7 +137,13 @@ func CreateMiddleware(logger mlogger.Logger, db db.Factory, enforcer auth.Enforc
return nil, err
}
- p.epdispatcher = routers.NewDispatcher(p.logger, p.router, adb, rtdb, enforcer, config)
+ cdb, err := db.NewConfirmationsDB()
+ 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
}
diff --git a/api/server/internal/api/routers/authorized/handler.go b/api/server/internal/api/routers/authorized/handler.go
index b0a3580..29118ce 100644
--- a/api/server/internal/api/routers/authorized/handler.go
+++ b/api/server/internal/api/routers/authorized/handler.go
@@ -37,6 +37,9 @@ func (ar *AuthorizedRouter) tokenHandler(service mservice.Type, endpoint string,
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.Forbidden(ar.logger, ar.service, "confirmation_required", "pending token requires confirmation")
+ }
var a model.Account
if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil {
if errors.Is(err, merrors.ErrNoData) {
@@ -54,3 +57,18 @@ func (ar *AuthorizedRouter) AccountHandler(service mservice.Type, endpoint strin
}
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.ObjRef("account_ref", 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)
+}
diff --git a/api/server/internal/api/routers/dispatcher.go b/api/server/internal/api/routers/dispatcher.go
index 6f6ebb0..3ec2929 100644
--- a/api/server/internal/api/routers/dispatcher.go
+++ b/api/server/internal/api/routers/dispatcher.go
@@ -8,6 +8,7 @@ import (
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/confirmation"
"github.com/tech/sendico/pkg/db/refreshtokens"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
@@ -31,7 +32,11 @@ func (d *Dispatcher) AccountHandler(service mservice.Type, endpoint string, meth
d.protected.AccountHandler(service, endpoint, method, handler)
}
-func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, rtdb refreshtokens.DB, enforcer auth.Enforcer, config *middleware.Config) *Dispatcher {
+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, cdb confirmation.DB, rtdb refreshtokens.DB, enforcer auth.Enforcer, config *middleware.Config) *Dispatcher {
d := &Dispatcher{
logger: logger.Named("api_dispatcher"),
}
@@ -40,7 +45,7 @@ func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, rtdb
endpoint := os.Getenv(config.EndPointEnv)
signature := middleware.SignatureConf(config)
router.Group(func(r chi.Router) {
- d.public = rpublic.NewRouter(d.logger, endpoint, db, rtdb, r, &config.Token, &signature)
+ d.public = rpublic.NewRouter(d.logger, endpoint, db, cdb, rtdb, r, &config.Token, &signature)
})
router.Group(func(r chi.Router) {
d.protected = rauthorized.NewRouter(d.logger, endpoint, r, db, enforcer, &config.Token, &signature)
diff --git a/api/server/internal/api/routers/endpoint/token.go b/api/server/internal/api/routers/endpoint/token.go
index bb01eb2..be85a2a 100644
--- a/api/server/internal/api/routers/endpoint/token.go
+++ b/api/server/internal/api/routers/endpoint/token.go
@@ -18,3 +18,13 @@ func (er *HttpEndpointRouter) CreateAccessToken(user *model.Account) (sresponse.
}
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
+}
diff --git a/api/server/internal/api/routers/public/login.go b/api/server/internal/api/routers/public/login.go
index b668b9e..c8c90c1 100644
--- a/api/server/internal/api/routers/public/login.go
+++ b/api/server/internal/api/routers/public/login.go
@@ -6,14 +6,20 @@ import (
"errors"
"net/http"
"strings"
+ "time"
"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/server/interface/api/srequest"
+ "github.com/tech/sendico/server/interface/api/sresponse"
+ "github.com/tech/sendico/server/internal/server/confirmationimp"
"go.uber.org/zap"
)
+const pendingLoginTTLMinutes = 10
+
func (pr *PublicRouter) logUserIn(ctx context.Context, r *http.Request, req *srequest.Login) http.HandlerFunc {
// Get the account database entry
trimmedLogin := strings.TrimSpace(req.Login)
@@ -35,13 +41,23 @@ func (pr *PublicRouter) logUserIn(ctx context.Context, r *http.Request, req *sre
return response.Unauthorized(pr.logger, pr.service, "password does not match")
}
- accessToken, err := pr.imp.CreateAccessToken(account)
+ pendingToken, err := pr.imp.CreatePendingToken(account, pendingLoginTTLMinutes)
if err != nil {
- pr.logger.Warn("Failed to generate access token", zap.Error(err))
+ pr.logger.Warn("Failed to generate pending token", zap.Error(err))
return response.Internal(pr.logger, pr.service, err)
}
- return pr.refreshAndRespondLogin(ctx, r, &req.SessionIdentifier, account, &accessToken)
+ cfg := confirmationimp.DefaultConfig()
+ _, rec, err := pr.cstore.Create(ctx, account.ID, account.Login, model.ConfirmationTargetLogin, cfg, pr.generateCode)
+ if err != nil {
+ pr.logger.Warn("Failed to create login confirmation code", zap.Error(err))
+ return response.Internal(pr.logger, pr.service, err)
+ }
+ pr.logger.Info("Login confirmation code issued",
+ zap.String("destination", pr.maskEmail(account.Login)),
+ zap.String("account", account.Login))
+
+ return sresponse.LoginPending(pr.logger, account, &pendingToken, pr.maskEmail(account.Login), int(time.Until(rec.ExpiresAt).Seconds()))
}
func (a *PublicRouter) login(r *http.Request) http.HandlerFunc {
diff --git a/api/server/internal/api/routers/public/respond.go b/api/server/internal/api/routers/public/respond.go
index 002c51f..71dff09 100644
--- a/api/server/internal/api/routers/public/respond.go
+++ b/api/server/internal/api/routers/public/respond.go
@@ -2,59 +2,16 @@ package routers
import (
"context"
- "crypto/rand"
- "encoding/base64"
- "io"
"net/http"
- "time"
"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/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
+ rtokens "github.com/tech/sendico/server/internal/api/routers/tokens"
"go.uber.org/zap"
)
-func generateRefreshTokenData(length int) (string, error) {
- randomBytes := make([]byte, length)
- if _, err := io.ReadFull(rand.Reader, randomBytes); err != nil {
- return "", merrors.Internal("failed to generate secure random bytes: " + err.Error())
- }
-
- return base64.URLEncoding.EncodeToString(randomBytes), nil
-}
-
-func (er *PublicRouter) prepareRefreshToken(ctx context.Context, r *http.Request, session *model.SessionIdentifier, account *model.Account) (*model.RefreshToken, error) {
- refreshToken, err := generateRefreshTokenData(er.config.Length)
- if err != nil {
- er.logger.Warn("Failed to generate refresh token", zap.Error(err), mzap.StorableRef(account))
- return nil, err
- }
-
- token := &model.RefreshToken{
- AccountBoundBase: model.AccountBoundBase{
- AccountRef: account.GetID(),
- },
- ClientRefreshToken: model.ClientRefreshToken{
- SessionIdentifier: *session,
- RefreshToken: refreshToken,
- },
- ExpiresAt: time.Now().Add(time.Duration(er.config.Expiration.Refresh) * time.Hour),
- IsRevoked: false,
- UserAgent: r.UserAgent(),
- IPAddress: r.RemoteAddr,
- }
-
- if err = er.rtdb.Create(ctx, token); err != nil {
- er.logger.Warn("Failed to store a refresh token", zap.Error(err), mzap.StorableRef(account),
- zap.String("client_id", token.ClientID), zap.String("device_id", token.DeviceID))
- return nil, err
- }
-
- return token, nil
-}
-
func (pr *PublicRouter) refreshAndRespondLogin(
ctx context.Context,
r *http.Request,
@@ -62,7 +19,16 @@ func (pr *PublicRouter) refreshAndRespondLogin(
account *model.Account,
accessToken *sresponse.TokenData,
) http.HandlerFunc {
- refreshToken, err := pr.prepareRefreshToken(ctx, r, session, account)
+ refreshToken, err := rtokens.PrepareRefreshToken(
+ ctx,
+ r,
+ pr.rtdb,
+ pr.config.Length,
+ pr.config.Expiration.Refresh,
+ session,
+ account,
+ pr.logger,
+ )
if err != nil {
pr.logger.Warn("Failed to create refresh token", zap.Error(err), mzap.StorableRef(account),
zap.String("client_id", session.ClientID), zap.String("device_id", session.DeviceID))
diff --git a/api/server/internal/api/routers/public/router.go b/api/server/internal/api/routers/public/router.go
index a2fcae3..2d5ab0f 100644
--- a/api/server/internal/api/routers/public/router.go
+++ b/api/server/internal/api/routers/public/router.go
@@ -1,20 +1,27 @@
package routers
import (
+ "crypto/rand"
+ "strings"
+
"github.com/go-chi/chi/v5"
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/db/account"
+ "github.com/tech/sendico/pkg/db/confirmation"
"github.com/tech/sendico/pkg/db/refreshtokens"
"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"
re "github.com/tech/sendico/server/internal/api/routers/endpoint"
+ "github.com/tech/sendico/server/internal/server/confirmationimp"
)
type PublicRouter struct {
logger mlogger.Logger
db account.DB
+ cdb confirmation.DB
+ cstore *confirmationimp.ConfirmationStore
imp *re.HttpEndpointRouter
rtdb refreshtokens.DB
config middleware.TokenConfig
@@ -26,11 +33,39 @@ func (pr *PublicRouter) InstallHandler(service mservice.Type, endpoint string, m
pr.imp.InstallHandler(service, endpoint, method, handler)
}
-func NewRouter(logger mlogger.Logger, apiEndpoint string, db account.DB, rtdb refreshtokens.DB, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *PublicRouter {
+func (pr *PublicRouter) generateCode() (string, error) {
+ const digits = "0123456789"
+ b := make([]byte, confirmationimp.DefaultConfig().CodeLength)
+ if _, err := rand.Read(b); err != nil {
+ return "", err
+ }
+ for i := range b {
+ b[i] = digits[int(b[i])%len(digits)]
+ }
+ return string(b), nil
+}
+
+func (pr *PublicRouter) maskEmail(email string) string {
+ parts := strings.Split(email, "@")
+ if len(parts) != 2 {
+ return email
+ }
+ local := parts[0]
+ if len(local) > 2 {
+ local = local[:1] + "***" + local[len(local)-1:]
+ } else {
+ local = local[:1] + "***"
+ }
+ return local + "@" + parts[1]
+}
+
+func NewRouter(logger mlogger.Logger, apiEndpoint string, db account.DB, cdb confirmation.DB, rtdb refreshtokens.DB, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *PublicRouter {
l := logger.Named("public")
hr := PublicRouter{
logger: l,
db: db,
+ cdb: cdb,
+ cstore: confirmationimp.NewStore(cdb),
rtdb: rtdb,
config: *config,
signature: *signature,
diff --git a/api/server/internal/api/routers/router.go b/api/server/internal/api/routers/router.go
index ef95937..90ea96f 100644
--- a/api/server/internal/api/routers/router.go
+++ b/api/server/internal/api/routers/router.go
@@ -12,4 +12,5 @@ type APIRouter interface {
type ProtectedAPIRouter interface {
AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc)
+ PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc)
}
diff --git a/api/server/internal/api/routers/tokens/tokens.go b/api/server/internal/api/routers/tokens/tokens.go
new file mode 100644
index 0000000..e768532
--- /dev/null
+++ b/api/server/internal/api/routers/tokens/tokens.go
@@ -0,0 +1,65 @@
+package tokens
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/tech/sendico/pkg/db/refreshtokens"
+ "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"
+ "go.uber.org/zap"
+)
+
+func generateRefreshTokenData(length int) (string, error) {
+ randomBytes := make([]byte, length)
+ if _, err := io.ReadFull(rand.Reader, randomBytes); err != nil {
+ return "", merrors.Internal("failed to generate secure random bytes: " + err.Error())
+ }
+
+ return base64.URLEncoding.EncodeToString(randomBytes), nil
+}
+
+func PrepareRefreshToken(
+ ctx context.Context,
+ r *http.Request,
+ rtdb refreshtokens.DB,
+ length int,
+ refreshExpiration int,
+ session *model.SessionIdentifier,
+ account *model.Account,
+ logger mlogger.Logger,
+) (*model.RefreshToken, error) {
+ refreshToken, err := generateRefreshTokenData(length)
+ if err != nil {
+ logger.Warn("Failed to generate refresh token", zap.Error(err), mzap.StorableRef(account))
+ return nil, err
+ }
+
+ token := &model.RefreshToken{
+ AccountBoundBase: model.AccountBoundBase{
+ AccountRef: account.GetID(),
+ },
+ ClientRefreshToken: model.ClientRefreshToken{
+ SessionIdentifier: *session,
+ RefreshToken: refreshToken,
+ },
+ ExpiresAt: time.Now().Add(time.Duration(refreshExpiration) * time.Hour),
+ IsRevoked: false,
+ UserAgent: r.UserAgent(),
+ IPAddress: r.RemoteAddr,
+ }
+
+ if err = rtdb.Create(ctx, token); err != nil {
+ logger.Warn("Failed to store a refresh token", zap.Error(err), mzap.StorableRef(account),
+ zap.String("client_id", token.ClientID), zap.String("device_id", token.DeviceID))
+ return nil, err
+ }
+
+ return token, nil
+}
diff --git a/api/server/internal/server/accountapiimp/service.go b/api/server/internal/server/accountapiimp/service.go
index 7425b03..9db86cc 100644
--- a/api/server/internal/server/accountapiimp/service.go
+++ b/api/server/internal/server/accountapiimp/service.go
@@ -2,7 +2,12 @@ package accountapiimp
import (
"context"
+ "fmt"
+ "os"
+ "strings"
+ "time"
+ chaingatewayclient "github.com/tech/sendico/chain/gateway/client"
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/account"
@@ -11,9 +16,11 @@ import (
"github.com/tech/sendico/pkg/db/refreshtokens"
"github.com/tech/sendico/pkg/db/transaction"
"github.com/tech/sendico/pkg/domainprovider"
+ "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
+ gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
"github.com/tech/sendico/server/interface/accountservice"
eapi "github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/interface/services/fileservice"
@@ -39,6 +46,13 @@ type AccountAPI struct {
tph mutil.ParamHelper
accountsPermissionRef primitive.ObjectID
accService accountservice.AccountService
+ chainGateway chainWalletClient
+ chainAsset *gatewayv1.Asset
+}
+
+type chainWalletClient interface {
+ CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error)
+ Close() error
}
func (a *AccountAPI) Name() mservice.Type {
@@ -46,7 +60,15 @@ func (a *AccountAPI) Name() mservice.Type {
}
func (a *AccountAPI) Finish(ctx context.Context) error {
- return a.avatars.Finish(ctx)
+ if err := a.avatars.Finish(ctx); err != nil {
+ return err
+ }
+ if a.chainGateway != nil {
+ if err := a.chainGateway.Close(); err != nil {
+ a.logger.Warn("Failed to close chain gateway client", zap.Error(err))
+ }
+ }
+ return nil
}
func CreateAPI(a eapi.API) (*AccountAPI, error) {
@@ -86,6 +108,7 @@ func CreateAPI(a eapi.API) (*AccountAPI, error) {
// Account related api endpoints
a.Register().Handler(mservice.Accounts, "/signup", api.Post, p.signup)
+ a.Register().Handler(mservice.Accounts, "/signup/availability", api.Get, p.signupAvailability)
a.Register().AccountHandler(mservice.Accounts, "", api.Put, p.updateProfile)
a.Register().AccountHandler(mservice.Accounts, "", api.Get, p.getProfile)
@@ -120,5 +143,82 @@ func CreateAPI(a eapi.API) (*AccountAPI, error) {
}
p.accountsPermissionRef = accountsPolicy.ID
+ cfg := a.Config()
+ if cfg == nil {
+ p.logger.Error("Failed to fetch service configuration")
+ return nil, merrors.InvalidArgument("No configuration provided")
+ }
+ if err := p.initChainGateway(cfg.ChainGateway); err != nil {
+ p.logger.Error("Failed to initialize chain gateway client", zap.Error(err))
+ return nil, err
+ }
+
return p, nil
}
+
+func (a *AccountAPI) initChainGateway(cfg *eapi.ChainGatewayConfig) error {
+ if cfg == nil {
+ return merrors.InvalidArgument("chain gateway configuration is not provided")
+ }
+
+ cfg.Address = strings.TrimSpace(cfg.Address)
+ if cfg.Address == "" {
+ cfg.Address = strings.TrimSpace(os.Getenv(cfg.AddressEnv))
+ }
+ if cfg.Address == "" {
+ return merrors.InvalidArgument(fmt.Sprintf("chain gateway address is not specified and address env %s is empty", cfg.AddressEnv))
+ }
+
+ clientCfg := chaingatewayclient.Config{
+ Address: cfg.Address,
+ DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second,
+ CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second,
+ Insecure: cfg.Insecure,
+ }
+
+ client, err := chaingatewayclient.New(context.Background(), clientCfg)
+ if err != nil {
+ return err
+ }
+
+ asset, err := buildGatewayAsset(cfg.DefaultAsset)
+ if err != nil {
+ _ = client.Close()
+ return err
+ }
+
+ a.chainGateway = client
+ a.chainAsset = asset
+ return nil
+}
+
+func buildGatewayAsset(cfg eapi.ChainGatewayAssetConfig) (*gatewayv1.Asset, error) {
+ chain, err := parseChainNetwork(cfg.Chain)
+ if err != nil {
+ return nil, err
+ }
+ tokenSymbol := strings.TrimSpace(cfg.TokenSymbol)
+ if tokenSymbol == "" {
+ return nil, merrors.InvalidArgument("chain gateway token symbol is required")
+ }
+ return &gatewayv1.Asset{
+ Chain: chain,
+ TokenSymbol: strings.ToUpper(tokenSymbol),
+ ContractAddress: strings.ToLower(strings.TrimSpace(cfg.ContractAddress)),
+ }, nil
+}
+
+func parseChainNetwork(value string) (gatewayv1.ChainNetwork, error) {
+ switch strings.ToUpper(strings.TrimSpace(value)) {
+ case "ETHEREUM_MAINNET", "CHAIN_NETWORK_ETHEREUM_MAINNET":
+ return gatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, nil
+ case "ARBITRUM_ONE", "CHAIN_NETWORK_ARBITRUM_ONE":
+ return gatewayv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE, nil
+ case "OTHER_EVM", "CHAIN_NETWORK_OTHER_EVM":
+ return gatewayv1.ChainNetwork_CHAIN_NETWORK_OTHER_EVM, nil
+ case "", "CHAIN_NETWORK_UNSPECIFIED":
+ return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument("chain network must be specified")
+ default:
+ return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument(fmt.Sprintf("unsupported chain network %s", value))
+ }
+}
diff --git a/api/server/internal/server/accountapiimp/signup.go b/api/server/internal/server/accountapiimp/signup.go
index a770d12..2f1474b 100644
--- a/api/server/internal/server/accountapiimp/signup.go
+++ b/api/server/internal/server/accountapiimp/signup.go
@@ -6,41 +6,27 @@ import (
"errors"
"fmt"
"net/http"
+ "strings"
"time"
+ "github.com/google/uuid"
"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"
+ gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
"github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
-func (a *AccountAPI) createAnonymousAccount(ctx context.Context, org *model.Organization, sr *srequest.Signup) error {
- anonymousUser := &model.Account{
- AccountPublic: model.AccountPublic{
- AccountBase: model.AccountBase{
- Describable: sr.AnonymousUser,
- },
- UserDataBase: sr.Account.UserDataBase,
- },
- }
- r, err := a.pmanager.Role().Create(ctx, org.ID, &sr.AnonymousRole)
- if err != nil {
- a.logger.Warn("Failed to create anonymous role", zap.Error(err))
- return err
- }
- if err := a.accService.CreateAccount(ctx, org, anonymousUser, r.ID); err != nil {
- a.logger.Warn("Failed to create account", zap.Error(err), zap.String("login", anonymousUser.Login))
- return err
- }
- return nil
-}
-
func (a *AccountAPI) createOrg(ctx context.Context, sr *srequest.Signup, permissionRef primitive.ObjectID) (*model.Organization, error) {
+ name := strings.TrimSpace(sr.Organization.Name)
+ if name == "" {
+ return nil, merrors.InvalidArgument("organization name must not be empty")
+ }
if _, err := time.LoadLocation(sr.OrganizationTimeZone); err != nil {
return nil, merrors.DataConflict(fmt.Sprintf("invalid time zone '%s' provided, error %s", sr.OrganizationTimeZone, err.Error()))
}
@@ -51,7 +37,8 @@ func (a *AccountAPI) createOrg(ctx context.Context, sr *srequest.Signup, permiss
PermissionRef: permissionRef,
},
Describable: model.Describable{
- Name: sr.OrganizationName,
+ Name: name,
+ Description: sr.Organization.Description,
},
TimeZone: sr.OrganizationTimeZone,
},
@@ -74,6 +61,18 @@ func (a *AccountAPI) signup(r *http.Request) http.HandlerFunc {
return response.BadRequest(a.logger, a.Name(), "", err.Error())
}
+ sr.Account.Login = strings.ToLower(strings.TrimSpace(sr.Account.Login))
+ if err := a.ensureLoginAvailable(r.Context(), sr.Account.Login); err != nil {
+ if errors.Is(err, merrors.ErrDataConflict) {
+ return response.DataConflict(a.logger, "user_already_registered", "User has already been registered")
+ }
+ if errors.Is(err, merrors.ErrInvalidArg) {
+ return response.BadPayload(a.logger, a.Name(), err)
+ }
+ a.logger.Warn("Failed to validate login availability", zap.Error(err), zap.String("login", sr.Account.Login))
+ return response.Internal(a.logger, a.Name(), err)
+ }
+
newAccount := sr.Account.ToAccount()
if res := a.accService.ValidateAccount(newAccount); res != nil {
a.logger.Warn("Invalid signup account received", zap.Error(res), zap.String("account", newAccount.Login))
@@ -96,6 +95,26 @@ func (a *AccountAPI) signup(r *http.Request) http.HandlerFunc {
return sresponse.SignUp(a.logger, newAccount)
}
+func (a *AccountAPI) signupAvailability(r *http.Request) http.HandlerFunc {
+ login := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("login")))
+ if login == "" {
+ return response.BadRequest(a.logger, a.Name(), "missing_login", "login query parameter is required")
+ }
+
+ err := a.ensureLoginAvailable(r.Context(), login)
+ switch {
+ case err == nil:
+ return sresponse.SignUpAvailability(a.logger, login, true)
+ case errors.Is(err, merrors.ErrDataConflict):
+ return sresponse.SignUpAvailability(a.logger, login, false)
+ case errors.Is(err, merrors.ErrInvalidArg):
+ return response.BadPayload(a.logger, a.Name(), err)
+ default:
+ a.logger.Warn("Failed to check login availability", zap.Error(err), zap.String("login", login))
+ return response.Internal(a.logger, a.Name(), err)
+ }
+}
+
func (a *AccountAPI) executeSignupTransaction(ctxt context.Context, sr *srequest.Signup, newAccount *model.Account) error {
_, err := a.tf.CreateTransaction().Execute(ctxt, func(ctx context.Context) (any, error) {
return a.signupTransactionBody(ctx, sr, newAccount)
@@ -116,6 +135,10 @@ func (a *AccountAPI) signupTransactionBody(ctx context.Context, sr *srequest.Sig
return nil, err
}
+ if err := a.openOrgWallet(ctx, org, sr); err != nil {
+ return nil, err
+ }
+
roleDescription, err := a.pmanager.Role().Create(ctx, org.ID, &sr.OwnerRole)
if err != nil {
a.logger.Warn("Failed to create owner role", zap.Error(err), zap.String("login", newAccount.Login))
@@ -131,10 +154,6 @@ func (a *AccountAPI) signupTransactionBody(ctx context.Context, sr *srequest.Sig
return nil, err
}
- if err := a.createAnonymousAccount(ctx, org, sr); err != nil {
- return nil, err
- }
-
return nil, nil
}
@@ -146,8 +165,21 @@ func (a *AccountAPI) grantAllPermissions(ctx context.Context, organizationRef pr
return err
}
+ required := map[mservice.Type]bool{
+ mservice.Organizations: false,
+ mservice.Accounts: false,
+ mservice.LedgerAccounts: false,
+ }
+
actions := []model.Action{model.ActionCreate, model.ActionRead, model.ActionUpdate, model.ActionDelete}
for _, policy := range policies {
+ if policy.ResourceTypes != nil {
+ for _, resource := range *policy.ResourceTypes {
+ if _, ok := required[resource]; ok {
+ required[resource] = true
+ }
+ }
+ }
for _, action := range actions {
a.logger.Debug("Adding permission", mzap.StorableRef(&policy), zap.String("action", string(action)),
mzap.ObjRef("role_ref", roleID), mzap.ObjRef("policy_ref", policy.ID), mzap.ObjRef("organization_ref", organizationRef))
@@ -172,5 +204,55 @@ func (a *AccountAPI) grantAllPermissions(ctx context.Context, organizationRef pr
return err
}
+ for resource, granted := range required {
+ if !granted {
+ a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", string(resource)))
+ }
+ }
+
+ return nil
+}
+
+func (a *AccountAPI) ensureLoginAvailable(ctx context.Context, login string) error {
+ if strings.TrimSpace(login) == "" {
+ return merrors.InvalidArgument("login must not be empty")
+ }
+ if _, err := a.db.GetByEmail(ctx, login); err != nil {
+ if errors.Is(err, merrors.ErrNoData) {
+ return nil
+ }
+ a.logger.Warn("Failed to lookup account by login", zap.Error(err), zap.String("login", login))
+ return err
+ }
+ return merrors.DataConflict("account already exists")
+}
+
+func (a *AccountAPI) openOrgWallet(ctx context.Context, org *model.Organization, sr *srequest.Signup) error {
+ if a.chainGateway == nil || a.chainAsset == nil {
+ a.logger.Warn("Chain gateway client not configured, skipping wallet creation", mzap.StorableRef(org))
+ return merrors.Internal("chain gateway client is not configured")
+ }
+ asset := *a.chainAsset
+ req := &gatewayv1.CreateManagedWalletRequest{
+ IdempotencyKey: uuid.NewString(),
+ OrganizationRef: org.ID.Hex(),
+ OwnerRef: org.ID.Hex(),
+ Asset: &asset,
+ Metadata: map[string]string{
+ "source": "signup",
+ "login": sr.Account.Login,
+ },
+ }
+
+ resp, err := a.chainGateway.CreateManagedWallet(ctx, req)
+ if err != nil {
+ a.logger.Warn("Failed to create managed wallet for organization", zap.Error(err), mzap.StorableRef(org))
+ return err
+ }
+ if resp == nil || resp.Wallet == nil || strings.TrimSpace(resp.Wallet.WalletRef) == "" {
+ return merrors.Internal("chain gateway returned empty wallet reference")
+ }
+
+ a.logger.Info("Managed wallet created for organization", mzap.StorableRef(org), zap.String("wallet_ref", resp.Wallet.WalletRef))
return nil
}
diff --git a/api/server/internal/server/accountapiimp/signup_integration_test.go b/api/server/internal/server/accountapiimp/signup_integration_test.go
index 54baae2..664162d 100644
--- a/api/server/internal/server/accountapiimp/signup_integration_test.go
+++ b/api/server/internal/server/accountapiimp/signup_integration_test.go
@@ -65,19 +65,17 @@ func TestSignupRequestSerialization(t *testing.T) {
},
Password: "TestPassword123!",
},
- Name: "Test User",
+ Describable: model.Describable{
+ Name: "Test User",
+ },
+ },
+ Organization: model.Describable{
+ Name: "Test Organization",
},
- OrganizationName: "Test Organization",
OrganizationTimeZone: "UTC",
- AnonymousUser: model.Describable{
- Name: "Anonymous User",
- },
OwnerRole: model.Describable{
Name: "Owner",
},
- AnonymousRole: model.Describable{
- Name: "Anonymous",
- },
}
// Store in MongoDB
@@ -93,7 +91,7 @@ func TestSignupRequestSerialization(t *testing.T) {
// Verify data integrity
assert.Equal(t, signupRequest.Account.Login, retrieved.Account.Login)
assert.Equal(t, signupRequest.Account.Name, retrieved.Account.Name)
- assert.Equal(t, signupRequest.OrganizationName, retrieved.OrganizationName)
+ assert.Equal(t, signupRequest.Organization.Name, retrieved.Organization.Name)
assert.Equal(t, signupRequest.OrganizationTimeZone, retrieved.OrganizationTimeZone)
})
@@ -109,19 +107,17 @@ func TestSignupHTTPSerialization(t *testing.T) {
},
Password: "TestPassword123!",
},
- Name: "Test User",
+ Describable: model.Describable{
+ Name: "Test User",
+ },
+ },
+ Organization: model.Describable{
+ Name: "Test Organization",
},
- OrganizationName: "Test Organization",
OrganizationTimeZone: "UTC",
- AnonymousUser: model.Describable{
- Name: "Anonymous User",
- },
OwnerRole: model.Describable{
Name: "Owner",
},
- AnonymousRole: model.Describable{
- Name: "Anonymous",
- },
}
t.Run("ValidJSONRequest", func(t *testing.T) {
@@ -141,13 +137,13 @@ func TestSignupHTTPSerialization(t *testing.T) {
// Verify parsing
assert.Equal(t, signupRequest.Account.Login, parsedRequest.Account.Login)
assert.Equal(t, signupRequest.Account.Name, parsedRequest.Account.Name)
- assert.Equal(t, signupRequest.OrganizationName, parsedRequest.OrganizationName)
+ assert.Equal(t, signupRequest.Organization.Name, parsedRequest.Organization.Name)
})
t.Run("UnicodeCharacters", func(t *testing.T) {
unicodeRequest := signupRequest
unicodeRequest.Account.Name = "Test 用户 Üser"
- unicodeRequest.OrganizationName = "测试 Organization"
+ unicodeRequest.Organization.Name = "测试 Organization"
// Serialize to JSON
reqBody, err := json.Marshal(unicodeRequest)
@@ -160,7 +156,7 @@ func TestSignupHTTPSerialization(t *testing.T) {
// Verify unicode characters are preserved
assert.Equal(t, "Test 用户 Üser", parsedRequest.Account.Name)
- assert.Equal(t, "测试 Organization", parsedRequest.OrganizationName)
+ assert.Equal(t, "测试 Organization", parsedRequest.Organization.Name)
})
t.Run("InvalidJSONRequest", func(t *testing.T) {
@@ -184,7 +180,9 @@ func TestAccountDataConversion(t *testing.T) {
},
Password: "TestPassword123!",
},
- Name: "Test User",
+ Describable: model.Describable{
+ Name: "Test User",
+ },
}
t.Run("ToAccount", func(t *testing.T) {
diff --git a/api/server/internal/server/accountapiimp/signup_test.go b/api/server/internal/server/accountapiimp/signup_test.go
index 276ecac..9a366c6 100644
--- a/api/server/internal/server/accountapiimp/signup_test.go
+++ b/api/server/internal/server/accountapiimp/signup_test.go
@@ -1,12 +1,18 @@
package accountapiimp
import (
+ "context"
+ "errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
+ "github.com/tech/sendico/pkg/db/repository/builder"
+ "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/server/interface/api/srequest"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.uber.org/zap"
)
// Helper function to create string pointers
@@ -60,26 +66,24 @@ func TestCreateValidSignupRequest(t *testing.T) {
},
Password: "TestPassword123!",
},
- Name: "Test User",
+ Describable: model.Describable{
+ Name: "Test User",
+ },
+ },
+ Organization: model.Describable{
+ Name: "Test Organization",
},
- OrganizationName: "Test Organization",
OrganizationTimeZone: "UTC",
- AnonymousUser: model.Describable{
- Name: "Anonymous User",
- },
OwnerRole: model.Describable{
Name: "Owner",
},
- AnonymousRole: model.Describable{
- Name: "Anonymous",
- },
}
// Validate the request structure
assert.Equal(t, "test@example.com", request.Account.Login)
assert.Equal(t, "TestPassword123!", request.Account.Password)
assert.Equal(t, "Test User", request.Account.Name)
- assert.Equal(t, "Test Organization", request.OrganizationName)
+ assert.Equal(t, "Test Organization", request.Organization.Name)
assert.Equal(t, "UTC", request.OrganizationTimeZone)
}
@@ -94,9 +98,13 @@ func TestSignupRequestValidation(t *testing.T) {
},
Password: "TestPassword123!",
},
- Name: "Test User",
+ Describable: model.Describable{
+ Name: "Test User",
+ },
+ },
+ Organization: model.Describable{
+ Name: "Test Organization",
},
- OrganizationName: "Test Organization",
OrganizationTimeZone: "UTC",
}
@@ -104,7 +112,7 @@ func TestSignupRequestValidation(t *testing.T) {
assert.NotEmpty(t, request.Account.Login)
assert.NotEmpty(t, request.Account.Password)
assert.NotEmpty(t, request.Account.Name)
- assert.NotEmpty(t, request.OrganizationName)
+ assert.NotEmpty(t, request.Organization.Name)
assert.NotEmpty(t, request.OrganizationTimeZone)
})
@@ -205,7 +213,9 @@ func TestAccountDataToAccount(t *testing.T) {
},
Password: "TestPassword123!",
},
- Name: "Test User",
+ Describable: model.Describable{
+ Name: "Test User",
+ },
}
account := accountData.ToAccount()
@@ -240,3 +250,106 @@ func TestColorValidation(t *testing.T) {
})
}
}
+
+type stubAccountDB struct {
+ result *model.Account
+ err error
+}
+
+func (s *stubAccountDB) GetByEmail(ctx context.Context, email string) (*model.Account, error) {
+ return s.result, s.err
+}
+
+func (s *stubAccountDB) GetByToken(ctx context.Context, email string) (*model.Account, error) {
+ return nil, merrors.NotImplemented("stub")
+}
+
+func (s *stubAccountDB) GetAccountsByRefs(ctx context.Context, orgRef primitive.ObjectID, refs []primitive.ObjectID) ([]model.Account, error) {
+ return nil, merrors.NotImplemented("stub")
+}
+
+func (s *stubAccountDB) Create(ctx context.Context, object *model.Account) error {
+ return merrors.NotImplemented("stub")
+}
+
+func (s *stubAccountDB) InsertMany(ctx context.Context, objects []*model.Account) error {
+ return merrors.NotImplemented("stub")
+}
+
+func (s *stubAccountDB) Get(ctx context.Context, objectRef primitive.ObjectID, result *model.Account) error {
+ return merrors.NotImplemented("stub")
+}
+
+func (s *stubAccountDB) Update(ctx context.Context, object *model.Account) error {
+ return merrors.NotImplemented("stub")
+}
+
+func (s *stubAccountDB) Patch(ctx context.Context, objectRef primitive.ObjectID, patch builder.Patch) error {
+ return merrors.NotImplemented("stub")
+}
+
+func (s *stubAccountDB) Delete(ctx context.Context, objectRef primitive.ObjectID) error {
+ return merrors.NotImplemented("stub")
+}
+
+func (s *stubAccountDB) DeleteMany(ctx context.Context, query builder.Query) error {
+ return merrors.NotImplemented("stub")
+}
+
+func (s *stubAccountDB) DeleteCascade(ctx context.Context, objectRef primitive.ObjectID) error {
+ return merrors.NotImplemented("stub")
+}
+
+func (s *stubAccountDB) FindOne(ctx context.Context, query builder.Query, result *model.Account) error {
+ return merrors.NotImplemented("stub")
+}
+
+func TestEnsureLoginAvailable(t *testing.T) {
+ ctx := context.Background()
+ logger := zap.NewNop()
+
+ t.Run("available", func(t *testing.T) {
+ api := &AccountAPI{
+ logger: logger,
+ db: &stubAccountDB{
+ err: merrors.ErrNoData,
+ },
+ }
+ assert.NoError(t, api.ensureLoginAvailable(ctx, "new@example.com"))
+ })
+
+ t.Run("taken", func(t *testing.T) {
+ api := &AccountAPI{
+ logger: logger,
+ db: &stubAccountDB{
+ result: &model.Account{},
+ },
+ }
+ err := api.ensureLoginAvailable(ctx, "used@example.com")
+ assert.Error(t, err)
+ assert.True(t, errors.Is(err, merrors.ErrDataConflict))
+ })
+
+ t.Run("invalid login", func(t *testing.T) {
+ api := &AccountAPI{
+ logger: logger,
+ db: &stubAccountDB{
+ err: merrors.ErrNoData,
+ },
+ }
+ err := api.ensureLoginAvailable(ctx, " ")
+ assert.Error(t, err)
+ assert.True(t, errors.Is(err, merrors.ErrInvalidArg))
+ })
+
+ t.Run("db error", func(t *testing.T) {
+ api := &AccountAPI{
+ logger: logger,
+ db: &stubAccountDB{
+ err: errors.New("boom"),
+ },
+ }
+ err := api.ensureLoginAvailable(ctx, "err@example.com")
+ assert.EqualError(t, err, "boom")
+ })
+}
diff --git a/api/server/internal/server/confirmationimp/request.go b/api/server/internal/server/confirmationimp/request.go
new file mode 100644
index 0000000..68e61f3
--- /dev/null
+++ b/api/server/internal/server/confirmationimp/request.go
@@ -0,0 +1,48 @@
+package confirmationimp
+
+import (
+ "encoding/json"
+ "net/http"
+ "time"
+
+ "github.com/tech/sendico/pkg/api/http/response"
+ "github.com/tech/sendico/pkg/model"
+ "github.com/tech/sendico/pkg/mutil/mzap"
+ emodel "github.com/tech/sendico/server/interface/model"
+ "go.uber.org/zap"
+)
+
+func (a *ConfirmationAPI) requestCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc {
+ var req confirmationRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ a.logger.Warn("Failed to decode confirmation request", zap.Error(err))
+ return response.BadPayload(a.logger, a.Name(), err)
+ }
+
+ target, err := a.parseTarget(req.Target)
+ if err != nil {
+ return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error())
+ }
+
+ if target == model.ConfirmationTargetLogin && (token == nil || !token.Pending) {
+ return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token")
+ }
+
+ destination := a.resolveDestination(req.Destination, account)
+ if destination == "" {
+ return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required")
+ }
+ code, rec, err := a.store.Create(r.Context(), account.ID, destination, target, a.config, a.generateCode)
+ if err != nil {
+ a.logger.Warn("Failed to create confirmation code", zap.Error(err), mzap.ObjRef("account_ref", account.ID))
+ return response.Internal(a.logger, a.Name(), err)
+ }
+
+ a.sendCode(account, target, destination, code)
+
+ return response.Accepted(a.logger, confirmationResponse{
+ TTLSeconds: int(time.Until(rec.ExpiresAt).Seconds()),
+ CooldownSeconds: int(a.config.Cooldown.Seconds()),
+ Destination: maskEmail(destination),
+ })
+}
diff --git a/api/server/internal/server/confirmationimp/resend.go b/api/server/internal/server/confirmationimp/resend.go
new file mode 100644
index 0000000..4e7e1d2
--- /dev/null
+++ b/api/server/internal/server/confirmationimp/resend.go
@@ -0,0 +1,56 @@
+package confirmationimp
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "time"
+
+ "github.com/tech/sendico/pkg/api/http/response"
+ "github.com/tech/sendico/pkg/model"
+ "github.com/tech/sendico/pkg/mutil/mzap"
+ emodel "github.com/tech/sendico/server/interface/model"
+ "go.uber.org/zap"
+)
+
+func (a *ConfirmationAPI) resendCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc {
+ var req confirmationRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ a.logger.Warn("Failed to decode confirmation resend request", zap.Error(err))
+ return response.BadPayload(a.logger, a.Name(), err)
+ }
+
+ target, err := a.parseTarget(req.Target)
+ if err != nil {
+ return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error())
+ }
+
+ if target == model.ConfirmationTargetLogin && (token == nil || !token.Pending) {
+ return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token")
+ }
+
+ destination := a.resolveDestination(req.Destination, account)
+ if destination == "" {
+ return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required")
+ }
+ code, rec, err := a.store.Resend(r.Context(), account.ID, destination, target, a.config, a.generateCode)
+ switch {
+ case errors.Is(err, errConfirmationNotFound):
+ return response.NotFound(a.logger, a.Name(), "no_active_code_for_resend")
+ case errors.Is(err, errConfirmationCooldown):
+ return response.Forbidden(a.logger, a.Name(), "cooldown_active", "please wait before requesting another code")
+ case errors.Is(err, errConfirmationResendLimit):
+ return response.Forbidden(a.logger, a.Name(), "resend_limit_reached", "too many resend attempts")
+ case err != nil:
+ a.logger.Warn("Failed to resend confirmation code", zap.Error(err), mzap.ObjRef("account_ref", account.ID))
+ return response.Internal(a.logger, a.Name(), err)
+ }
+
+ a.sendCode(account, target, destination, code)
+
+ return response.Accepted(a.logger, confirmationResponse{
+ TTLSeconds: int(time.Until(rec.ExpiresAt).Seconds()),
+ CooldownSeconds: int(a.config.Cooldown.Seconds()),
+ Destination: maskEmail(destination),
+ })
+}
diff --git a/api/server/internal/server/confirmationimp/service.go b/api/server/internal/server/confirmationimp/service.go
new file mode 100644
index 0000000..737e8c8
--- /dev/null
+++ b/api/server/internal/server/confirmationimp/service.go
@@ -0,0 +1,158 @@
+package confirmationimp
+
+import (
+ "context"
+ "crypto/rand"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/go-chi/jwtauth/v5"
+ api "github.com/tech/sendico/pkg/api/http"
+ "github.com/tech/sendico/pkg/db/refreshtokens"
+ "github.com/tech/sendico/pkg/merrors"
+ "github.com/tech/sendico/pkg/messaging"
+ cnotifications "github.com/tech/sendico/pkg/messaging/notifications/confirmation"
+ "github.com/tech/sendico/pkg/mlogger"
+ "github.com/tech/sendico/pkg/model"
+ "github.com/tech/sendico/pkg/mservice"
+ "github.com/tech/sendico/pkg/mutil/mzap"
+ eapi "github.com/tech/sendico/server/interface/api"
+ "github.com/tech/sendico/server/interface/api/sresponse"
+ "github.com/tech/sendico/server/interface/middleware"
+ emodel "github.com/tech/sendico/server/interface/model"
+ "go.uber.org/zap"
+)
+
+type Config struct {
+ CodeLength int
+ TTL time.Duration
+ MaxAttempts int
+ Cooldown time.Duration
+ ResendLimit int
+}
+
+func defaultConfig() Config {
+ return Config{
+ CodeLength: 6,
+ TTL: 10 * time.Minute,
+ MaxAttempts: 5,
+ Cooldown: time.Minute,
+ ResendLimit: 5,
+ }
+}
+
+func DefaultConfig() Config {
+ return defaultConfig()
+}
+
+type ConfirmationAPI struct {
+ logger mlogger.Logger
+ config Config
+ store *ConfirmationStore
+ rtdb refreshtokens.DB
+ producer messaging.Producer
+ tokenConfig middleware.TokenConfig
+ signature middleware.Signature
+}
+
+func (a *ConfirmationAPI) Name() mservice.Type {
+ return mservice.Confirmations
+}
+
+func (a *ConfirmationAPI) Finish(_ context.Context) error {
+ return nil
+}
+
+func CreateAPI(a eapi.API) (*ConfirmationAPI, error) {
+ cdb, err := a.DBFactory().NewConfirmationsDB()
+ if err != nil {
+ return nil, err
+ }
+ rtdb, err := a.DBFactory().NewRefreshTokensDB()
+ if err != nil {
+ return nil, err
+ }
+
+ p := &ConfirmationAPI{
+ logger: a.Logger().Named(mservice.Confirmations),
+ config: defaultConfig(),
+ store: NewStore(cdb),
+ rtdb: rtdb,
+ producer: a.Register().Messaging().Producer(),
+ tokenConfig: a.Config().Mw.Token,
+ signature: middleware.SignatureConf(a.Config().Mw),
+ }
+
+ a.Register().PendingAccountHandler(p.Name(), "/confirmations", api.Post, p.requestCode)
+ a.Register().PendingAccountHandler(p.Name(), "/confirmations/resend", api.Post, p.resendCode)
+ a.Register().PendingAccountHandler(p.Name(), "/confirmations/verify", api.Post, p.verifyCode)
+ return p, nil
+}
+
+func (a *ConfirmationAPI) generateCode() (string, error) {
+ const digits = "0123456789"
+ b := make([]byte, a.config.CodeLength)
+ _, err := rand.Read(b)
+ if err != nil {
+ return "", err
+ }
+ for i := range b {
+ b[i] = digits[int(b[i])%len(digits)]
+ }
+ return string(b), nil
+}
+
+func (a *ConfirmationAPI) parseTarget(raw string) (model.ConfirmationTarget, error) {
+ switch strings.ToLower(strings.TrimSpace(raw)) {
+ case string(model.ConfirmationTargetLogin):
+ return model.ConfirmationTargetLogin, nil
+ case string(model.ConfirmationTargetPayout):
+ return model.ConfirmationTargetPayout, nil
+ default:
+ return "", merrors.InvalidArgument(fmt.Sprintf("unsupported target '%s'", raw), "target")
+ }
+}
+
+func (a *ConfirmationAPI) resolveDestination(reqDest string, account *model.Account) string {
+ destination := strings.ToLower(strings.TrimSpace(reqDest))
+ if destination == "" && account != nil {
+ destination = strings.ToLower(strings.TrimSpace(account.Login))
+ }
+ return destination
+}
+
+func (a *ConfirmationAPI) sendCode(account *model.Account, target model.ConfirmationTarget, destination, code string) {
+ a.logger.Info("Confirmation code generated",
+ zap.String("target", string(target)),
+ zap.String("destination", maskEmail(destination)),
+ mzap.ObjRef("account_ref", account.ID))
+ if err := a.producer.SendMessage(cnotifications.Code(a.Name(), account.ID, destination, target, code)); err != nil {
+ a.logger.Warn("Failed to send confirmation code notification", zap.Error(err), mzap.ObjRef("account_ref", account.ID))
+ }
+ a.logger.Debug("Confirmation code debug dump (do not log in production)", zap.String("code", code))
+}
+
+func maskEmail(email string) string {
+ parts := strings.Split(email, "@")
+ if len(parts) != 2 {
+ return email
+ }
+ local := parts[0]
+ if len(local) > 2 {
+ local = local[:1] + "***" + local[len(local)-1:]
+ } else {
+ local = local[:1] + "***"
+ }
+ return local + "@" + parts[1]
+}
+
+func (a *ConfirmationAPI) createAccessToken(account *model.Account) (sresponse.TokenData, error) {
+ ja := jwtauth.New(a.signature.Algorithm, a.signature.PrivateKey, a.signature.PublicKey)
+ _, res, err := ja.Encode(emodel.Account2Claims(account, a.tokenConfig.Expiration.Account))
+ token := sresponse.TokenData{
+ Token: res,
+ Expiration: time.Now().Add(time.Duration(a.tokenConfig.Expiration.Account) * time.Hour),
+ }
+ return token, err
+}
diff --git a/api/server/internal/server/confirmationimp/store.go b/api/server/internal/server/confirmationimp/store.go
new file mode 100644
index 0000000..a3e83fe
--- /dev/null
+++ b/api/server/internal/server/confirmationimp/store.go
@@ -0,0 +1,181 @@
+package confirmationimp
+
+import (
+ "context"
+ "crypto/rand"
+ "crypto/sha256"
+ "crypto/subtle"
+ "errors"
+ "time"
+
+ "github.com/tech/sendico/pkg/db/confirmation"
+ "github.com/tech/sendico/pkg/merrors"
+ "github.com/tech/sendico/pkg/model"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+var (
+ errConfirmationNotFound = errors.New("confirmation not found or expired")
+ errConfirmationUsed = errors.New("confirmation already used")
+ errConfirmationMismatch = errors.New("confirmation code mismatch")
+ errConfirmationAttemptsExceeded = errors.New("confirmation attempts exceeded")
+ errConfirmationCooldown = errors.New("confirmation cooldown active")
+ errConfirmationResendLimit = errors.New("confirmation resend limit reached")
+)
+
+type ConfirmationStore struct {
+ db confirmation.DB
+}
+
+func NewStore(db confirmation.DB) *ConfirmationStore {
+ return &ConfirmationStore{db: db}
+}
+
+func (s *ConfirmationStore) Create(
+ ctx context.Context,
+ accountRef primitive.ObjectID,
+ destination string,
+ target model.ConfirmationTarget,
+ cfg Config,
+ generator func() (string, error),
+) (string, *model.ConfirmationCode, error) {
+ if err := s.db.DeleteTuple(ctx, accountRef, destination, target); err != nil && !errors.Is(err, merrors.ErrNoData) {
+ return "", nil, err
+ }
+
+ code, _, rec, err := s.buildRecord(accountRef, destination, target, cfg, generator)
+ if err != nil {
+ return "", nil, err
+ }
+
+ if err := s.db.Create(ctx, rec); err != nil {
+ return "", nil, err
+ }
+
+ return code, rec, nil
+}
+
+func (s *ConfirmationStore) Resend(
+ ctx context.Context,
+ accountRef primitive.ObjectID,
+ destination string,
+ target model.ConfirmationTarget,
+ cfg Config,
+ generator func() (string, error),
+) (string, *model.ConfirmationCode, error) {
+ now := time.Now().UTC()
+ active, err := s.db.FindActive(ctx, accountRef, destination, target, now.Unix())
+ if errors.Is(err, merrors.ErrNoData) {
+ return s.Create(ctx, accountRef, destination, target, cfg, generator)
+ }
+ if err != nil {
+ return "", nil, err
+ }
+ if active.ResendCount >= active.ResendLimit {
+ return "", nil, errConfirmationResendLimit
+ }
+ if now.Before(active.CooldownUntil) {
+ return "", nil, errConfirmationCooldown
+ }
+
+ code, salt, updated, err := s.buildRecord(accountRef, destination, target, cfg, generator)
+ if err != nil {
+ return "", nil, err
+ }
+ // Preserve attempt counters but bump resend count.
+ updated.ID = active.ID
+ updated.CreatedAt = active.CreatedAt
+ updated.Attempts = active.Attempts
+ updated.ResendCount = active.ResendCount + 1
+ updated.Salt = salt
+
+ if err := s.db.Update(ctx, updated); err != nil {
+ return "", nil, err
+ }
+ return code, updated, nil
+}
+
+func (s *ConfirmationStore) Verify(
+ ctx context.Context,
+ accountRef primitive.ObjectID,
+ destination string,
+ target model.ConfirmationTarget,
+ code string,
+) error {
+ now := time.Now().UTC()
+ rec, err := s.db.FindActive(ctx, accountRef, destination, target, now.Unix())
+ if errors.Is(err, merrors.ErrNoData) {
+ return errConfirmationNotFound
+ }
+ if err != nil {
+ return err
+ }
+ if rec.Used {
+ return errConfirmationUsed
+ }
+
+ rec.Attempts++
+ if rec.Attempts > rec.MaxAttempts {
+ rec.Used = true
+ _ = s.db.Update(ctx, rec)
+ return errConfirmationAttemptsExceeded
+ }
+
+ if subtle.ConstantTimeCompare(rec.CodeHash, hashCode(rec.Salt, code)) != 1 {
+ _ = s.db.Update(ctx, rec)
+ return errConfirmationMismatch
+ }
+
+ rec.Used = true
+ return s.db.Update(ctx, rec)
+}
+
+func (s *ConfirmationStore) buildRecord(
+ accountRef primitive.ObjectID,
+ destination string,
+ target model.ConfirmationTarget,
+ cfg Config,
+ generator func() (string, error),
+) (string, []byte, *model.ConfirmationCode, error) {
+ code, err := generator()
+ if err != nil {
+ return "", nil, nil, err
+ }
+ salt, err := newSalt()
+ if err != nil {
+ return "", nil, nil, err
+ }
+
+ now := time.Now().UTC()
+ rec := model.NewConfirmationCode(accountRef)
+ rec.Destination = destination
+ rec.Target = target
+ rec.CodeHash = hashCode(salt, code)
+ rec.Salt = salt
+ rec.ExpiresAt = now.Add(cfg.TTL)
+ rec.MaxAttempts = cfg.MaxAttempts
+ rec.ResendLimit = cfg.ResendLimit
+ rec.CooldownUntil = now.Add(cfg.Cooldown)
+ rec.Used = false
+ rec.Attempts = 0
+ rec.ResendCount = 0
+ rec.CreatedAt = now
+ rec.UpdatedAt = now
+
+ return code, salt, rec, nil
+}
+
+func hashCode(salt []byte, code string) []byte {
+ h := sha256.New()
+ h.Write(salt)
+ h.Write([]byte(code))
+ return h.Sum(nil)
+}
+
+func newSalt() ([]byte, error) {
+ buf := make([]byte, 16)
+ if _, err := rand.Read(buf); err != nil {
+ return nil, err
+ }
+ return buf, nil
+}
diff --git a/api/server/internal/server/confirmationimp/types.go b/api/server/internal/server/confirmationimp/types.go
new file mode 100644
index 0000000..743c0b6
--- /dev/null
+++ b/api/server/internal/server/confirmationimp/types.go
@@ -0,0 +1,23 @@
+package confirmationimp
+
+import (
+ "github.com/tech/sendico/pkg/model"
+)
+
+type confirmationRequest struct {
+ Target string `json:"target"`
+ Destination string `json:"destination,omitempty"`
+}
+
+type confirmationVerifyRequest struct {
+ Target string `json:"target"`
+ Code string `json:"code"`
+ Destination string `json:"destination,omitempty"`
+ SessionIdentifier model.SessionIdentifier `json:"sessionIdentifier"`
+}
+
+type confirmationResponse struct {
+ TTLSeconds int `json:"ttl_seconds"`
+ CooldownSeconds int `json:"cooldown_seconds"`
+ Destination string `json:"destination"`
+}
diff --git a/api/server/internal/server/confirmationimp/verify.go b/api/server/internal/server/confirmationimp/verify.go
new file mode 100644
index 0000000..4ff4434
--- /dev/null
+++ b/api/server/internal/server/confirmationimp/verify.go
@@ -0,0 +1,88 @@
+package confirmationimp
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strings"
+
+ "github.com/tech/sendico/pkg/api/http/response"
+ "github.com/tech/sendico/pkg/model"
+ "github.com/tech/sendico/pkg/mutil/mzap"
+ "github.com/tech/sendico/server/interface/api/sresponse"
+ emodel "github.com/tech/sendico/server/interface/model"
+ rtokens "github.com/tech/sendico/server/internal/api/routers/tokens"
+ "go.uber.org/zap"
+)
+
+func (a *ConfirmationAPI) verifyCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc {
+ var req confirmationVerifyRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ a.logger.Warn("Failed to decode confirmation verification request", zap.Error(err))
+ return response.BadPayload(a.logger, a.Name(), err)
+ }
+
+ target, err := a.parseTarget(req.Target)
+ if err != nil {
+ return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error())
+ }
+
+ if target == model.ConfirmationTargetLogin && (token == nil || !token.Pending) {
+ return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token")
+ }
+
+ if strings.TrimSpace(req.Code) == "" {
+ return response.BadRequest(a.logger, a.Name(), "missing_code", "confirmation code is required")
+ }
+
+ destination := a.resolveDestination(req.Destination, account)
+ if destination == "" {
+ return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required")
+ }
+ err = a.store.Verify(r.Context(), account.ID, destination, target, strings.TrimSpace(req.Code))
+ switch {
+ case errors.Is(err, errConfirmationNotFound):
+ return response.NotFound(a.logger, a.Name(), "code_not_found_or_expired")
+ case errors.Is(err, errConfirmationUsed):
+ return response.Forbidden(a.logger, a.Name(), "code_used", "code has already been used")
+ case errors.Is(err, errConfirmationAttemptsExceeded):
+ return response.Forbidden(a.logger, a.Name(), "attempt_limit_reached", "too many failed attempts")
+ case errors.Is(err, errConfirmationMismatch):
+ return response.Forbidden(a.logger, a.Name(), "invalid_code", "code does not match")
+ case err != nil:
+ a.logger.Warn("Failed to verify confirmation code", zap.Error(err), mzap.ObjRef("account_ref", account.ID))
+ return response.Internal(a.logger, a.Name(), err)
+ }
+
+ a.logger.Info("Confirmation code verified", zap.String("target", string(target)), mzap.ObjRef("account_ref", account.ID))
+ if target == model.ConfirmationTargetLogin {
+ if req.SessionIdentifier.ClientID == "" || req.SessionIdentifier.DeviceID == "" {
+ return response.BadRequest(a.logger, a.Name(), "missing_session", "session identifier is required")
+ }
+ accessToken, err := a.createAccessToken(account)
+ if err != nil {
+ a.logger.Warn("Failed to generate access token", zap.Error(err))
+ return response.Internal(a.logger, a.Name(), err)
+ }
+ refreshToken, err := rtokens.PrepareRefreshToken(
+ r.Context(),
+ r,
+ a.rtdb,
+ a.tokenConfig.Length,
+ a.tokenConfig.Expiration.Refresh,
+ &req.SessionIdentifier,
+ account,
+ a.logger,
+ )
+ if err != nil {
+ a.logger.Warn("Failed to generate refresh token", zap.Error(err))
+ return response.Internal(a.logger, a.Name(), err)
+ }
+ rt := sresponse.TokenData{
+ Token: refreshToken.RefreshToken,
+ Expiration: refreshToken.ExpiresAt,
+ }
+ return sresponse.Login(a.logger, account, &accessToken, &rt)
+ }
+ return response.Success(a.logger)
+}
diff --git a/api/server/internal/server/siteimp/call.go b/api/server/internal/server/siteimp/call.go
new file mode 100644
index 0000000..2e7a5fd
--- /dev/null
+++ b/api/server/internal/server/siteimp/call.go
@@ -0,0 +1,29 @@
+package siteimp
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/tech/sendico/pkg/api/http/response"
+ snotifications "github.com/tech/sendico/pkg/messaging/notifications/site"
+ "github.com/tech/sendico/pkg/model"
+ "go.uber.org/zap"
+)
+
+func (a *SiteAPI) callRequest(r *http.Request) http.HandlerFunc {
+ var request model.CallRequest
+ if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
+ a.logger.Warn("Failed to decode call request payload", zap.Error(err))
+ return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode call request payload")
+ }
+ request.Normalize()
+ if err := request.Validate(); err != nil {
+ a.logger.Warn("Call request validation failed", zap.Error(err))
+ return response.BadPayload(a.logger, a.Name(), err)
+ }
+ if err := a.producer.SendMessage(snotifications.CallRequest(a.Name(), &request)); err != nil {
+ a.logger.Warn("Failed to enqueue call request notification", zap.Error(err))
+ return response.Internal(a.logger, a.Name(), err)
+ }
+ return a.acceptedQueued()
+}
diff --git a/api/server/internal/server/siteimp/contact.go b/api/server/internal/server/siteimp/contact.go
new file mode 100644
index 0000000..ab0ceb9
--- /dev/null
+++ b/api/server/internal/server/siteimp/contact.go
@@ -0,0 +1,29 @@
+package siteimp
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/tech/sendico/pkg/api/http/response"
+ snotifications "github.com/tech/sendico/pkg/messaging/notifications/site"
+ "github.com/tech/sendico/pkg/model"
+ "go.uber.org/zap"
+)
+
+func (a *SiteAPI) contactRequest(r *http.Request) http.HandlerFunc {
+ var request model.ContactRequest
+ if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
+ a.logger.Warn("Failed to decode contact request payload", zap.Error(err))
+ return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode contact request payload")
+ }
+ request.Normalize()
+ if err := request.Validate(); err != nil {
+ a.logger.Warn("Contact request validation failed", zap.Error(err))
+ return response.BadPayload(a.logger, a.Name(), err)
+ }
+ if err := a.producer.SendMessage(snotifications.ContactRequest(a.Name(), &request)); err != nil {
+ a.logger.Warn("Failed to enqueue contact request notification", zap.Error(err))
+ return response.Internal(a.logger, a.Name(), err)
+ }
+ return a.acceptedQueued()
+}
diff --git a/api/server/internal/server/siteimp/demo.go b/api/server/internal/server/siteimp/demo.go
new file mode 100644
index 0000000..99cb590
--- /dev/null
+++ b/api/server/internal/server/siteimp/demo.go
@@ -0,0 +1,31 @@
+package siteimp
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/tech/sendico/pkg/api/http/response"
+ snotifications "github.com/tech/sendico/pkg/messaging/notifications/site"
+ "github.com/tech/sendico/pkg/model"
+ "go.uber.org/zap"
+)
+
+func (a *SiteAPI) demoRequest(r *http.Request) http.HandlerFunc {
+ var request model.DemoRequest
+ if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
+ a.logger.Warn("Failed to decode demo request payload", zap.Error(err))
+ return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode demo request payload")
+ }
+ request.Normalize()
+ if err := request.Validate(); err != nil {
+ a.logger.Warn("Demo request validation failed", zap.Error(err))
+ return response.BadPayload(a.logger, a.Name(), err)
+ }
+
+ if err := a.producer.SendMessage(snotifications.DemoRequest(a.Name(), &request)); err != nil {
+ a.logger.Warn("Failed to enqueue demo request notification", zap.Error(err))
+ return response.Internal(a.logger, a.Name(), err)
+ }
+
+ return a.acceptedQueued()
+}
diff --git a/api/server/internal/server/siteimp/response.go b/api/server/internal/server/siteimp/response.go
new file mode 100644
index 0000000..739d9b5
--- /dev/null
+++ b/api/server/internal/server/siteimp/response.go
@@ -0,0 +1,19 @@
+package siteimp
+
+import (
+ "net/http"
+
+ "github.com/tech/sendico/pkg/api/http/response"
+)
+
+type enqueueResponse struct {
+ Status string `json:"status"`
+}
+
+func newEnqueueResponse() enqueueResponse {
+ return enqueueResponse{Status: "queued"}
+}
+
+func (a *SiteAPI) acceptedQueued() http.HandlerFunc {
+ return response.Accepted(a.logger, newEnqueueResponse())
+}
diff --git a/api/server/internal/server/siteimp/service.go b/api/server/internal/server/siteimp/service.go
new file mode 100644
index 0000000..0fd68ef
--- /dev/null
+++ b/api/server/internal/server/siteimp/service.go
@@ -0,0 +1,36 @@
+package siteimp
+
+import (
+ "context"
+
+ api "github.com/tech/sendico/pkg/api/http"
+ "github.com/tech/sendico/pkg/messaging"
+ "github.com/tech/sendico/pkg/mlogger"
+ "github.com/tech/sendico/pkg/mservice"
+ eapi "github.com/tech/sendico/server/interface/api"
+)
+
+type SiteAPI struct {
+ logger mlogger.Logger
+ producer messaging.Producer
+}
+
+func (a *SiteAPI) Name() mservice.Type {
+ return mservice.Site
+}
+
+func (a *SiteAPI) Finish(_ context.Context) error {
+ return nil
+}
+
+func CreateAPI(a eapi.API) (*SiteAPI, error) {
+ p := &SiteAPI{
+ logger: a.Logger().Named(mservice.Site),
+ producer: a.Register().Messaging().Producer(),
+ }
+
+ a.Register().Handler(mservice.Site, "/request/demo", api.Post, p.demoRequest)
+ a.Register().Handler(mservice.Site, "/request/contact", api.Post, p.contactRequest)
+ a.Register().Handler(mservice.Site, "/request/call", api.Post, p.callRequest)
+ return p, nil
+}
diff --git a/ci/prod/.env.runtime b/ci/prod/.env.runtime
index 497b2ad..d67f10c 100644
--- a/ci/prod/.env.runtime
+++ b/ci/prod/.env.runtime
@@ -15,9 +15,9 @@ PERMISSION_IS_FILTERED=false
AMPLI_ENVIRONMENT=production
API_PROTOCOL=https
SERVICE_HOST=app.sendico.io
-API_ENDPOINT=https://app.sendico.io/api
+API_ENDPOINT=/api/v1
WS_PROTOCOL=wss
-WS_ENDPOINT=wss://app.sendico.io/ws
+WS_ENDPOINT=/ws
AMPLITUDE_SECRET=c3d75b3e2520d708440acbb16b923e79
DEFAULT_LOCALE=en
DEFAULT_CURRENCY=EUR
@@ -127,6 +127,7 @@ BFF_HTTP_PORT=8080
CHAIN_GATEWAY_DIR=chain_gateway
CHAIN_GATEWAY_COMPOSE_PROJECT=sendico-chain-gateway
CHAIN_GATEWAY_SERVICE_NAME=sendico_chain_gateway
+CHAIN_GATEWAY_ADDRESS=sendico_chain_gateway:50070
CHAIN_GATEWAY_GRPC_PORT=50070
CHAIN_GATEWAY_METRICS_PORT=9404
diff --git a/ci/prod/compose/bff.yml b/ci/prod/compose/bff.yml
index a50ad11..550c6b0 100644
--- a/ci/prod/compose/bff.yml
+++ b/ci/prod/compose/bff.yml
@@ -29,6 +29,7 @@ services:
NATS_PORT: ${NATS_PORT}
NATS_USER: ${NATS_USER}
NATS_PASSWORD: ${NATS_PASSWORD}
+ CHAIN_GATEWAY_ADDRESS: ${CHAIN_GATEWAY_ADDRESS}
MONGO_HOST: ${MONGO_HOST}
MONGO_PORT: ${MONGO_PORT}
MONGO_DATABASE: ${MONGO_DATABASE}
@@ -43,7 +44,7 @@ services:
ports:
- "0.0.0.0:${BFF_HTTP_PORT}:8081"
healthcheck:
- test: ["CMD-SHELL","wget -qO- http://localhost:8081/health | grep -q '\"status\":\"ok\"'"]
+ test: ["CMD-SHELL","wget -qO- http://localhost:8081/api/v1/health | grep -q '\"status\":\"ok\"'"]
interval: 30s
timeout: 10s
retries: 3
diff --git a/ci/prod/compose/chain_gateway.dockerfile b/ci/prod/compose/chain_gateway.dockerfile
index 028eaa6..b02d311 100644
--- a/ci/prod/compose/chain_gateway.dockerfile
+++ b/ci/prod/compose/chain_gateway.dockerfile
@@ -35,7 +35,9 @@ RUN apk add --no-cache ca-certificates tzdata wget
WORKDIR /app
COPY api/chain/gateway/config.yml /app/config.yml
COPY api/chain/gateway/env /app/env
+COPY api/chain/gateway/entrypoint.sh /app/entrypoint.sh
COPY --from=build /out/chain-gateway /app/chain-gateway
+RUN chmod +x /app/entrypoint.sh
EXPOSE 50070 9403
-ENTRYPOINT ["/app/chain-gateway"]
-CMD ["--config.file", "/app/config.yml"]
+ENTRYPOINT ["/app/entrypoint.sh"]
+CMD ["/app/chain-gateway","--config.file","/app/config.yml"]
diff --git a/ci/prod/compose/chain_gateway.yml b/ci/prod/compose/chain_gateway.yml
index 66fc404..4a3a770 100644
--- a/ci/prod/compose/chain_gateway.yml
+++ b/ci/prod/compose/chain_gateway.yml
@@ -5,6 +5,14 @@ x-common-env: &common-env
- ../env/.env.runtime
- ../env/.env.version
+volumes:
+ chain-gateway-vault-run:
+ driver: local
+ driver_opts:
+ type: tmpfs
+ device: tmpfs
+ o: size=8m,uid=0,gid=0,mode=0700
+
networks:
sendico-net:
external: true
@@ -33,11 +41,16 @@ services:
CHAIN_GATEWAY_ARBITRUM_RPC_URL: ${CHAIN_GATEWAY_ARBITRUM_RPC_URL}
CHAIN_GATEWAY_SERVICE_WALLET_KEY: ${CHAIN_GATEWAY_SERVICE_WALLET_KEY}
CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS: ${CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS}
- CHAIN_GATEWAY_VAULT_TOKEN: ${CHAIN_GATEWAY_VAULT_TOKEN}
+ VAULT_TOKEN_FILE: /run/vault/token
command: ["--config.file", "/app/config.yml"]
ports:
- "0.0.0.0:${CHAIN_GATEWAY_GRPC_PORT}:50070"
- "0.0.0.0:${CHAIN_GATEWAY_METRICS_PORT}:9403"
+ volumes:
+ - chain-gateway-vault-run:/run/vault:ro
+ depends_on:
+ sendico_chain_gateway_vault_agent:
+ condition: service_healthy
healthcheck:
test: ["CMD-SHELL","wget -qO- http://localhost:9403/health | grep -q '\"status\":\"ok\"'"]
interval: 30s
@@ -46,3 +59,32 @@ services:
start_period: 60s
networks:
- sendico-net
+
+ sendico_chain_gateway_vault_agent:
+ <<: *common-env
+ container_name: sendico-chain-gateway-vault-agent
+ restart: unless-stopped
+ image: hashicorp/vault:latest
+ pull_policy: always
+ cap_add: ["IPC_LOCK"]
+ environment:
+ VAULT_ADDR: ${VAULT_ADDR}
+ CHAIN_GATEWAY_VAULT_ROLE_ID: ${CHAIN_GATEWAY_VAULT_ROLE_ID}
+ CHAIN_GATEWAY_VAULT_SECRET_ID: ${CHAIN_GATEWAY_VAULT_SECRET_ID}
+ command: >
+ sh -lc 'set -euo pipefail; umask 077;
+ : "${CHAIN_GATEWAY_VAULT_ROLE_ID:?}"; : "${CHAIN_GATEWAY_VAULT_SECRET_ID:?}";
+ printf "%s" "$CHAIN_GATEWAY_VAULT_ROLE_ID" > /run/vault/role_id;
+ printf "%s" "$CHAIN_GATEWAY_VAULT_SECRET_ID" > /run/vault/secret_id;
+ unset CHAIN_GATEWAY_VAULT_ROLE_ID CHAIN_GATEWAY_VAULT_SECRET_ID;
+ exec vault agent -config=/etc/vault/agent/chain-gateway.hcl'
+ volumes:
+ - ./vault-agent/chain-gateway.hcl:/etc/vault/agent/chain-gateway.hcl:ro
+ - chain-gateway-vault-run:/run/vault
+ healthcheck:
+ test: ["CMD","test","-s","/run/vault/token"]
+ interval: 10s
+ timeout: 5s
+ retries: 6
+ networks:
+ - sendico-net
diff --git a/ci/prod/compose/notification.yml b/ci/prod/compose/notification.yml
index 391ba94..2ad532b 100644
--- a/ci/prod/compose/notification.yml
+++ b/ci/prod/compose/notification.yml
@@ -31,6 +31,9 @@ services:
NATS_URL: ${NATS_URL}
MAIL_USER: ${MAIL_USER}
MAIL_SECRET: ${MAIL_SECRET}
+ TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
+ TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID}
+ TELEGRAM_THREAD_ID: ${TELEGRAM_THREAD_ID}
MONGO_HOST: ${MONGO_HOST}
MONGO_PORT: ${MONGO_PORT}
MONGO_DATABASE: ${MONGO_DATABASE}
diff --git a/ci/prod/compose/vault-agent/chain-gateway.hcl b/ci/prod/compose/vault-agent/chain-gateway.hcl
new file mode 100644
index 0000000..3dbfc80
--- /dev/null
+++ b/ci/prod/compose/vault-agent/chain-gateway.hcl
@@ -0,0 +1,20 @@
+vault {
+ address = "https://vault.sendico.io"
+}
+
+auto_auth {
+ method "approle" {
+ mount_path = "auth/approle"
+ config = {
+ role_id_file_path = "/run/vault/role_id"
+ secret_id_file_path = "/run/vault/secret_id"
+ }
+ }
+
+ sink "file" {
+ config = {
+ path = "/run/vault/token"
+ mode = 0600
+ }
+ }
+}
diff --git a/ci/prod/scripts/deploy/chain_gateway.sh b/ci/prod/scripts/deploy/chain_gateway.sh
index 8207a74..d31d5dd 100755
--- a/ci/prod/scripts/deploy/chain_gateway.sh
+++ b/ci/prod/scripts/deploy/chain_gateway.sh
@@ -21,7 +21,8 @@ REQUIRED_SECRETS=(
CHAIN_GATEWAY_ARBITRUM_RPC_URL
CHAIN_GATEWAY_SERVICE_WALLET_KEY
CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS
- CHAIN_GATEWAY_VAULT_TOKEN
+ CHAIN_GATEWAY_VAULT_ROLE_ID
+ CHAIN_GATEWAY_VAULT_SECRET_ID
NATS_USER
NATS_PASSWORD
NATS_URL
@@ -48,7 +49,8 @@ CHAIN_GATEWAY_MONGO_PASSWORD_B64="$(b64enc "${CHAIN_GATEWAY_MONGO_PASSWORD}")"
CHAIN_GATEWAY_ARBITRUM_RPC_URL_B64="$(b64enc "${CHAIN_GATEWAY_ARBITRUM_RPC_URL}")"
CHAIN_GATEWAY_SERVICE_WALLET_KEY_B64="$(b64enc "${CHAIN_GATEWAY_SERVICE_WALLET_KEY}")"
CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS_B64="$(b64enc "${CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS}")"
-CHAIN_GATEWAY_VAULT_TOKEN_B64="$(b64enc "${CHAIN_GATEWAY_VAULT_TOKEN}")"
+CHAIN_GATEWAY_VAULT_ROLE_ID_B64="$(b64enc "${CHAIN_GATEWAY_VAULT_ROLE_ID}")"
+CHAIN_GATEWAY_VAULT_SECRET_ID_B64="$(b64enc "${CHAIN_GATEWAY_VAULT_SECRET_ID}")"
NATS_USER_B64="$(b64enc "${NATS_USER}")"
NATS_PASSWORD_B64="$(b64enc "${NATS_PASSWORD}")"
NATS_URL_B64="$(b64enc "${NATS_URL}")"
@@ -67,7 +69,7 @@ fi
RSYNC_FLAGS=(-az --delete)
[[ "${DEBUG_DEPLOY:-0}" = "1" ]] && RSYNC_FLAGS=(-avz --delete)
-ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" "mkdir -p ${REMOTE_DIR}/{compose,env}"
+ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" "mkdir -p ${REMOTE_DIR}/compose/secrets ${REMOTE_DIR}/env"
rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" ci/prod/compose/ "$REMOTE_TARGET:${REMOTE_DIR}/compose/"
rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" ci/prod/.env.runtime "$REMOTE_TARGET:${REMOTE_DIR}/env/.env.runtime"
@@ -85,7 +87,8 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" \
CHAIN_GATEWAY_ARBITRUM_RPC_URL_B64="$CHAIN_GATEWAY_ARBITRUM_RPC_URL_B64" \
CHAIN_GATEWAY_SERVICE_WALLET_KEY_B64="$CHAIN_GATEWAY_SERVICE_WALLET_KEY_B64" \
CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS_B64="$CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS_B64" \
- CHAIN_GATEWAY_VAULT_TOKEN_B64="$CHAIN_GATEWAY_VAULT_TOKEN_B64" \
+ CHAIN_GATEWAY_VAULT_ROLE_ID_B64="$CHAIN_GATEWAY_VAULT_ROLE_ID_B64" \
+ CHAIN_GATEWAY_VAULT_SECRET_ID_B64="$CHAIN_GATEWAY_VAULT_SECRET_ID_B64" \
NATS_USER_B64="$NATS_USER_B64" \
NATS_PASSWORD_B64="$NATS_PASSWORD_B64" \
NATS_URL_B64="$NATS_URL_B64" \
@@ -135,7 +138,8 @@ CHAIN_GATEWAY_MONGO_PASSWORD="$(decode_b64 "$CHAIN_GATEWAY_MONGO_PASSWORD_B64")"
CHAIN_GATEWAY_ARBITRUM_RPC_URL="$(decode_b64 "$CHAIN_GATEWAY_ARBITRUM_RPC_URL_B64")"
CHAIN_GATEWAY_SERVICE_WALLET_KEY="$(decode_b64 "$CHAIN_GATEWAY_SERVICE_WALLET_KEY_B64")"
CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS="$(decode_b64 "$CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS_B64")"
-CHAIN_GATEWAY_VAULT_TOKEN="$(decode_b64 "$CHAIN_GATEWAY_VAULT_TOKEN_B64")"
+CHAIN_GATEWAY_VAULT_ROLE_ID="$(decode_b64 "$CHAIN_GATEWAY_VAULT_ROLE_ID_B64")"
+CHAIN_GATEWAY_VAULT_SECRET_ID="$(decode_b64 "$CHAIN_GATEWAY_VAULT_SECRET_ID_B64")"
NATS_USER="$(decode_b64 "$NATS_USER_B64")"
NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")"
NATS_URL="$(decode_b64 "$NATS_URL_B64")"
@@ -143,8 +147,9 @@ NATS_URL="$(decode_b64 "$NATS_URL_B64")"
export CHAIN_GATEWAY_MONGO_USER CHAIN_GATEWAY_MONGO_PASSWORD
export CHAIN_GATEWAY_ARBITRUM_RPC_URL
export CHAIN_GATEWAY_SERVICE_WALLET_KEY CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS
-export CHAIN_GATEWAY_VAULT_TOKEN
+export CHAIN_GATEWAY_VAULT_ROLE_ID CHAIN_GATEWAY_VAULT_SECRET_ID
export NATS_USER NATS_PASSWORD NATS_URL
+
COMPOSE_PROJECT_NAME="$COMPOSE_PROJECT"
export COMPOSE_PROJECT_NAME
read -r -a SERVICES <<<"${SERVICES_LINE}"
diff --git a/ci/prod/scripts/deploy/notification.sh b/ci/prod/scripts/deploy/notification.sh
index 8e5ab99..5f9a75a 100755
--- a/ci/prod/scripts/deploy/notification.sh
+++ b/ci/prod/scripts/deploy/notification.sh
@@ -24,6 +24,8 @@ REQUIRED_SECRETS=(
NATS_USER
NATS_PASSWORD
NATS_URL
+ TELEGRAM_BOT_TOKEN
+ TELEGRAM_CHAT_ID
)
for var in "${REQUIRED_SECRETS[@]}"; do
@@ -50,6 +52,9 @@ API_ENDPOINT_SECRET_B64="$(b64enc "${API_ENDPOINT_SECRET}")"
NATS_USER_B64="$(b64enc "${NATS_USER}")"
NATS_PASSWORD_B64="$(b64enc "${NATS_PASSWORD}")"
NATS_URL_B64="$(b64enc "${NATS_URL}")"
+TELEGRAM_BOT_TOKEN_B64="$(b64enc "${TELEGRAM_BOT_TOKEN}")"
+TELEGRAM_CHAT_ID_B64="$(b64enc "${TELEGRAM_CHAT_ID}")"
+TELEGRAM_THREAD_ID_B64="$(b64enc "${TELEGRAM_THREAD_ID:-}")"
SSH_OPTS=(
-i /root/.ssh/id_rsa
@@ -86,6 +91,9 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" \
NATS_USER_B64="$NATS_USER_B64" \
NATS_PASSWORD_B64="$NATS_PASSWORD_B64" \
NATS_URL_B64="$NATS_URL_B64" \
+ TELEGRAM_BOT_TOKEN_B64="$TELEGRAM_BOT_TOKEN_B64" \
+ TELEGRAM_CHAT_ID_B64="$TELEGRAM_CHAT_ID_B64" \
+ TELEGRAM_THREAD_ID_B64="$TELEGRAM_THREAD_ID_B64" \
bash -s <<'EOSSH'
set -euo pipefail
cd "${REMOTE_DIR}/compose"
@@ -135,10 +143,14 @@ API_ENDPOINT_SECRET="$(decode_b64 "$API_ENDPOINT_SECRET_B64")"
NATS_USER="$(decode_b64 "$NATS_USER_B64")"
NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")"
NATS_URL="$(decode_b64 "$NATS_URL_B64")"
+TELEGRAM_BOT_TOKEN="$(decode_b64 "$TELEGRAM_BOT_TOKEN_B64")"
+TELEGRAM_CHAT_ID="$(decode_b64 "$TELEGRAM_CHAT_ID_B64")"
+TELEGRAM_THREAD_ID="$(decode_b64 "$TELEGRAM_THREAD_ID_B64")"
export MONGO_USER MONGO_PASSWORD
export MAIL_USER MAIL_SECRET API_ENDPOINT_SECRET
export NATS_USER NATS_PASSWORD NATS_URL
+export TELEGRAM_BOT_TOKEN TELEGRAM_CHAT_ID TELEGRAM_THREAD_ID
COMPOSE_PROJECT_NAME="$COMPOSE_PROJECT"
export COMPOSE_PROJECT_NAME
read -r -a SERVICES <<<"${SERVICES_LINE}"
diff --git a/ci/scripts/chain_gateway/deploy.sh b/ci/scripts/chain_gateway/deploy.sh
index f3d701b..417b070 100755
--- a/ci/scripts/chain_gateway/deploy.sh
+++ b/ci/scripts/chain_gateway/deploy.sh
@@ -61,7 +61,12 @@ export CHAIN_GATEWAY_ARBITRUM_RPC_URL="$(./ci/vlt kv_get kv "${CHAIN_GATEWAY_RPC
export CHAIN_GATEWAY_SERVICE_WALLET_KEY="$(./ci/vlt kv_get kv "${CHAIN_GATEWAY_WALLET_SECRET_PATH}" private_key)"
export CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS="$(./ci/vlt kv_get kv "${CHAIN_GATEWAY_WALLET_SECRET_PATH}" address || true)"
-export CHAIN_GATEWAY_VAULT_TOKEN="$(./ci/vlt kv_get kv "${CHAIN_GATEWAY_VAULT_SECRET_PATH}" token)"
+export CHAIN_GATEWAY_VAULT_ROLE_ID="$(./ci/vlt kv_get kv "${CHAIN_GATEWAY_VAULT_SECRET_PATH}" role_id)"
+export CHAIN_GATEWAY_VAULT_SECRET_ID="$(./ci/vlt kv_get kv "${CHAIN_GATEWAY_VAULT_SECRET_PATH}" secret_id)"
+if [ -z "${CHAIN_GATEWAY_VAULT_ROLE_ID}" ] || [ -z "${CHAIN_GATEWAY_VAULT_SECRET_ID}" ]; then
+ echo "[chain-gateway-deploy] vault approle creds are empty for path ${CHAIN_GATEWAY_VAULT_SECRET_PATH}" >&2
+ exit 1
+fi
export NATS_USER="$(./ci/vlt kv_get kv sendico/nats user)"
export NATS_PASSWORD="$(./ci/vlt kv_get kv sendico/nats password)"
diff --git a/ci/scripts/common/bump_version.sh b/ci/scripts/common/bump_version.sh
index 720d641..e126ad6 100755
--- a/ci/scripts/common/bump_version.sh
+++ b/ci/scripts/common/bump_version.sh
@@ -1,51 +1,34 @@
-#!/bin/sh
-set -eu
+# /bin/bash
-START_DIR="$(pwd)"
-echo "[bump-version] invoked from ${START_DIR}"
+echo "===================================="
+echo "Incrementing build version..."
+echo "===================================="
+VERSION_FILE=./version
-REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-echo "[bump-version] repo root resolved to ${REPO_ROOT}"
-cd "${REPO_ROOT}"
+NEW_VERSION=$(cat $VERSION_FILE | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{$NF=sprintf("%0*d", length($NF), ($NF+1)); print}')
+echo $NEW_VERSION > $VERSION_FILE
-VERSION_FILE="./version"
-if [ ! -f "${VERSION_FILE}" ]; then
- if git cat-file -e "HEAD:version" 2>/dev/null; then
- echo "[bump-version] version file missing in workspace, restoring from HEAD" >&2
- git show "HEAD:version" > "${VERSION_FILE}"
- else
- echo "[bump-version] version file not found: ${VERSION_FILE}" >&2
- exit 1
- fi
+echo "New version is "$NEW_VERSION
+
+echo "===================================="
+echo "Bumping client version..."
+echo "===================================="
+FILE="./frontend/mweb/pubspec.yaml"
+if sed --version >/dev/null 2>&1; then
+ # GNU sed
+ sed -i "s/^version: .*/version: ${NEW_VERSION}+1/" "$FILE"
+else
+ # BSD/macOS sed
+ sed -i '' -e "s/^version: .*/version: ${NEW_VERSION}+1/" "$FILE"
fi
-CURRENT_VERSION="$(cat "${VERSION_FILE}")"
-NEXT_VERSION="$(printf '%s' "${CURRENT_VERSION}" | awk -F. -v OFS=. '
- NF==1 { print ++$NF; next }
- {
- $NF=sprintf("%0*d", length($NF), ($NF+1))
- print
- }')"
+set -euo pipefail
-printf '%s\n' "${NEXT_VERSION}" > "${VERSION_FILE}"
-echo "[bump-version] ${CURRENT_VERSION} -> ${NEXT_VERSION}"
+# update version file(s) here
+# e.g.: ./ci/scripts/common/update_version_file.sh
-git add "${VERSION_FILE}"
-if git diff --cached --quiet; then
- echo "[bump-version] no changes staged, skipping commit"
- exit 0
+if ! git diff --quiet; then
+ git add .
+ git commit -m "chore: bump build version [skip ci]"
+ git push origin HEAD:main
fi
-
-AUTHOR_NAME="${GIT_AUTHOR_NAME:-woodpecker}"
-AUTHOR_EMAIL="${GIT_AUTHOR_EMAIL:-ci@sendico.io}"
-git config user.name "${AUTHOR_NAME}"
-git config user.email "${AUTHOR_EMAIL}"
-
-git commit -m "chore(ci): bump version to ${NEXT_VERSION}"
-
-BRANCH="${WOODPECKER_BRANCH:-}"
-if [ -z "${BRANCH}" ] || [ "${BRANCH}" = "HEAD" ]; then
- BRANCH="$(git rev-parse --abbrev-ref HEAD)"
-fi
-
-git push origin "HEAD:${BRANCH}"
diff --git a/ci/scripts/notification/deploy.sh b/ci/scripts/notification/deploy.sh
index 4b97613..b354a66 100755
--- a/ci/scripts/notification/deploy.sh
+++ b/ci/scripts/notification/deploy.sh
@@ -49,6 +49,7 @@ load_env_file ./.env.version
NOTIFICATION_MONGO_SECRET_PATH="${NOTIFICATION_MONGO_SECRET_PATH:?missing NOTIFICATION_MONGO_SECRET_PATH}"
NOTIFICATION_MAIL_SECRET_PATH="${NOTIFICATION_MAIL_SECRET_PATH:?missing NOTIFICATION_MAIL_SECRET_PATH}"
NOTIFICATION_API_SECRET_PATH="${NOTIFICATION_API_SECRET_PATH:?missing NOTIFICATION_API_SECRET_PATH}"
+NOTIFICATION_TELEGRAM_SECRET_PATH="${NOTIFICATION_TELEGRAM_SECRET_PATH:?missing NOTIFICATION_TELEGRAM_SECRET_PATH}"
: "${NATS_HOST:?missing NATS_HOST}"
: "${NATS_PORT:?missing NATS_PORT}"
@@ -60,6 +61,14 @@ export MAIL_SECRET="$(./ci/vlt kv_get kv "${NOTIFICATION_MAIL_SECRET_PATH}" pass
export API_ENDPOINT_SECRET="$(./ci/vlt kv_get kv "${NOTIFICATION_API_SECRET_PATH}" secret)"
+export TELEGRAM_BOT_TOKEN="$(./ci/vlt kv_get kv "${NOTIFICATION_TELEGRAM_SECRET_PATH}" bot_token)"
+export TELEGRAM_CHAT_ID="$(./ci/vlt kv_get kv "${NOTIFICATION_TELEGRAM_SECRET_PATH}" chat_id)"
+TELEGRAM_THREAD_ID=""
+if TELEGRAM_THREAD_ID_VALUE="$(./ci/vlt kv_get kv "${NOTIFICATION_TELEGRAM_SECRET_PATH}" thread_id 2>/dev/null)"; then
+ TELEGRAM_THREAD_ID="$TELEGRAM_THREAD_ID_VALUE"
+fi
+export TELEGRAM_THREAD_ID
+
export NATS_USER="$(./ci/vlt kv_get kv sendico/nats user)"
export NATS_PASSWORD="$(./ci/vlt kv_get kv sendico/nats password)"
export NATS_URL="nats://${NATS_USER}:${NATS_PASSWORD}@${NATS_HOST}:${NATS_PORT}"
diff --git a/frontend/pshared/lib/api/errors/authorization_failed.dart b/frontend/pshared/lib/api/errors/authorization_failed.dart
index 7acc439..841ab8d 100644
--- a/frontend/pshared/lib/api/errors/authorization_failed.dart
+++ b/frontend/pshared/lib/api/errors/authorization_failed.dart
@@ -1,2 +1,22 @@
-class AuthorizationFailed implements Exception {
+class AuthenticationFailedException implements Exception {
+ final String message;
+ final Exception? originalError;
+
+ const AuthenticationFailedException(this.message, [this.originalError]);
+
+ @override
+ String toString() {
+ return 'AuthenticationFailedException: $message${originalError != null ? ' (caused by: $originalError)' : ''}';
+ }
+}
+
+class CircuitBreakerOpenException implements Exception {
+ final String message;
+
+ const CircuitBreakerOpenException(this.message);
+
+ @override
+ String toString() {
+ return 'CircuitBreakerOpenException: $message';
+ }
}
\ No newline at end of file
diff --git a/frontend/pshared/lib/api/requests/login.dart b/frontend/pshared/lib/api/requests/login.dart
index 532d132..57b4cce 100644
--- a/frontend/pshared/lib/api/requests/login.dart
+++ b/frontend/pshared/lib/api/requests/login.dart
@@ -1,20 +1,18 @@
import 'package:json_annotation/json_annotation.dart';
+import 'package:pshared/api/requests/login_data.dart';
+
part 'login.g.dart';
@JsonSerializable(explicitToJson: true)
class LoginRequest {
- final String login;
- final String password;
- final String locale;
+ final LoginData login;
final String clientId;
final String deviceId;
const LoginRequest({
required this.login,
- required this.password,
- required this.locale,
required this.clientId,
required this.deviceId,
});
diff --git a/frontend/pshared/lib/api/requests/login_data.dart b/frontend/pshared/lib/api/requests/login_data.dart
new file mode 100644
index 0000000..ec2bc35
--- /dev/null
+++ b/frontend/pshared/lib/api/requests/login_data.dart
@@ -0,0 +1,76 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'login_data.g.dart';
+
+
+@JsonSerializable(explicitToJson: true, constructor: 'build')
+class LoginData {
+ final String login;
+ final String password;
+ final String locale;
+
+ const LoginData._({
+ required this.login,
+ required this.password,
+ required this.locale,
+ });
+
+ factory LoginData.build({
+ required String login,
+ required String password,
+ required String locale,
+ }) => LoginData._(
+ login: login.trim().toLowerCase(),
+ password: password,
+ locale: locale,
+ );
+
+ factory LoginData.fromJson(Map json) => _$LoginDataFromJson(json);
+ Map toJson() => _$LoginDataToJson(this);
+}
+
+@JsonSerializable(explicitToJson: true, constructor: 'buildIstance')
+class AccountData extends LoginData {
+ final String name;
+ final String lastName;
+
+ const AccountData._({
+ required super.login,
+ required super.password,
+ required super.locale,
+ required this.name,
+ required this.lastName,
+ }) : super._();
+
+ factory AccountData.buildIstance({
+ required String login,
+ required String password,
+ required String locale,
+ required String name,
+ required String lastName,
+ }) => AccountData._(
+ login: login,
+ password: password,
+ locale: locale,
+ name: name.trim(),
+ lastName: lastName.trim(),
+ );
+
+ factory AccountData.build({
+ required LoginData login,
+ required String name,
+ required String lastName,
+ }) => AccountData.buildIstance(
+ login: login.login,
+ password: login.password,
+ locale: login.locale,
+ name: name,
+ lastName: lastName,
+ );
+
+
+ factory AccountData.fromJson(Map json) => _$AccountDataFromJson(json);
+
+ @override
+ Map toJson() => _$AccountDataToJson(this);
+}
diff --git a/frontend/pshared/lib/api/requests/change_password.dart b/frontend/pshared/lib/api/requests/password/change.dart
similarity index 93%
rename from frontend/pshared/lib/api/requests/change_password.dart
rename to frontend/pshared/lib/api/requests/password/change.dart
index 5bdb21f..f4da065 100644
--- a/frontend/pshared/lib/api/requests/change_password.dart
+++ b/frontend/pshared/lib/api/requests/password/change.dart
@@ -1,6 +1,6 @@
import 'package:json_annotation/json_annotation.dart';
-part 'change_password.g.dart';
+part 'change.g.dart';
@JsonSerializable(explicitToJson: true)
diff --git a/frontend/pshared/lib/api/requests/password/forgot.dart b/frontend/pshared/lib/api/requests/password/forgot.dart
new file mode 100644
index 0000000..53eb53e
--- /dev/null
+++ b/frontend/pshared/lib/api/requests/password/forgot.dart
@@ -0,0 +1,20 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'forgot.g.dart';
+
+
+@JsonSerializable()
+class ForgotPasswordRequest {
+ final String login;
+
+ const ForgotPasswordRequest({
+ required this.login,
+ });
+
+ factory ForgotPasswordRequest.fromJson(Map json) => _$ForgotPasswordRequestFromJson(json);
+ Map toJson() => _$ForgotPasswordRequestToJson(this);
+
+ static ForgotPasswordRequest build({
+ required String login,
+ }) => ForgotPasswordRequest(login: login);
+}
diff --git a/frontend/pshared/lib/api/requests/password/reset.dart b/frontend/pshared/lib/api/requests/password/reset.dart
new file mode 100644
index 0000000..701a3ba
--- /dev/null
+++ b/frontend/pshared/lib/api/requests/password/reset.dart
@@ -0,0 +1,20 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'reset.g.dart';
+
+
+@JsonSerializable()
+class ResetPasswordRequest {
+ final String password;
+
+ const ResetPasswordRequest({
+ required this.password,
+ });
+
+ factory ResetPasswordRequest.fromJson(Map json) => _$ResetPasswordRequestFromJson(json);
+ Map toJson() => _$ResetPasswordRequestToJson(this);
+
+ static ResetPasswordRequest build({
+ required String password,
+ }) => ResetPasswordRequest(password: password);
+}
\ No newline at end of file
diff --git a/frontend/pshared/lib/api/requests/signup.dart b/frontend/pshared/lib/api/requests/signup.dart
index 98a817a..64d5c2a 100644
--- a/frontend/pshared/lib/api/requests/signup.dart
+++ b/frontend/pshared/lib/api/requests/signup.dart
@@ -1,40 +1,37 @@
import 'package:json_annotation/json_annotation.dart';
+import 'package:pshared/api/requests/login_data.dart';
+import 'package:pshared/data/dto/describable.dart';
+import 'package:pshared/data/mapper/describable.dart';
+import 'package:pshared/models/describable.dart';
+
part 'signup.g.dart';
@JsonSerializable(explicitToJson: true)
class SignupRequest {
- final String name;
- final String login;
- final String password;
- final String locale;
- final String organizationName;
+ final AccountData account;
+ final DescribableDTO organization;
final String organizationTimeZone;
+ final DescribableDTO ownerRole;
const SignupRequest({
- required this.name,
- required this.login,
- required this.password,
- required this.locale,
- required this.organizationName,
+ required this.account,
+ required this.organization,
required this.organizationTimeZone,
+ required this.ownerRole,
});
factory SignupRequest.build({
- required String name,
- required String login,
- required String password,
- required String locale,
- required String organizationName,
+ required AccountData account,
+ required Describable organization,
required String organizationTimeZone,
+ required Describable ownerRole,
}) => SignupRequest(
- name: name,
- login: login,
- password: password,
- locale: locale,
- organizationName: organizationName,
+ account: account,
+ organization: organization.toDTO(),
organizationTimeZone: organizationTimeZone,
+ ownerRole: ownerRole.toDTO(),
);
factory SignupRequest.fromJson(Map json) => _$SignupRequestFromJson(json);
diff --git a/frontend/pshared/lib/api/responses/login_pending.dart b/frontend/pshared/lib/api/responses/login_pending.dart
new file mode 100644
index 0000000..91e5241
--- /dev/null
+++ b/frontend/pshared/lib/api/responses/login_pending.dart
@@ -0,0 +1,26 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import 'package:pshared/api/responses/account.dart';
+import 'package:pshared/api/responses/token.dart';
+
+part 'login_pending.g.dart';
+
+
+@JsonSerializable(explicitToJson: true)
+class PendingLoginResponse {
+ final AccountResponse account;
+ final TokenData pendingToken;
+ final String destination;
+ final int ttlSeconds;
+
+ const PendingLoginResponse({
+ required this.account,
+ required this.pendingToken,
+ required this.destination,
+ required this.ttlSeconds,
+ });
+
+ factory PendingLoginResponse.fromJson(Map json) => _$PendingLoginResponseFromJson(json);
+
+ Map toJson() => _$PendingLoginResponseToJson(this);
+}
diff --git a/frontend/pshared/lib/config/common.dart b/frontend/pshared/lib/config/common.dart
index b0f6de5..c2240f9 100644
--- a/frontend/pshared/lib/config/common.dart
+++ b/frontend/pshared/lib/config/common.dart
@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
class CommonConstants {
- static String apiProto = const String.fromEnvironment('API_PROTO', defaultValue: 'http');
- static String apiHost = const String.fromEnvironment('API_HOST', defaultValue: 'localhost');
+ static String apiProto = 'https';
+ static String apiHost = 'app.sendico.io';
+ // static String apiProto = const String.fromEnvironment('API_PROTO', defaultValue: 'http');
+ // static String apiHost = const String.fromEnvironment('API_HOST', defaultValue: 'localhost');
// static String apiHost = 'localhost';
// static String apiHost = '10.0.2.2';
static String apiEndpoint = '/api/v1';
diff --git a/frontend/pshared/lib/data/dto/account/account.dart b/frontend/pshared/lib/data/dto/account/account.dart
index b1a080b..f89de61 100644
--- a/frontend/pshared/lib/data/dto/account/account.dart
+++ b/frontend/pshared/lib/data/dto/account/account.dart
@@ -1,6 +1,7 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/account/base.dart';
+import 'package:pshared/data/dto/date_time.dart';
part 'account.g.dart';
@@ -8,14 +9,17 @@ part 'account.g.dart';
@JsonSerializable()
class AccountDTO extends AccountBaseDTO {
final String login;
+ final String locale;
const AccountDTO({
required super.id,
required super.createdAt,
required super.updatedAt,
required super.name,
+ required super.lastName,
+ required super.description,
required super.avatarUrl,
- required super.locale,
+ required this.locale,
required this.login,
});
diff --git a/frontend/pshared/lib/data/dto/account/base.dart b/frontend/pshared/lib/data/dto/account/base.dart
index 0ae451d..453b70e 100644
--- a/frontend/pshared/lib/data/dto/account/base.dart
+++ b/frontend/pshared/lib/data/dto/account/base.dart
@@ -1,6 +1,7 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/storable.dart';
+import 'package:pshared/data/dto/date_time.dart';
part 'base.g.dart';
@@ -8,7 +9,8 @@ part 'base.g.dart';
@JsonSerializable()
class AccountBaseDTO extends StorableDTO {
final String name;
- final String locale;
+ final String lastName;
+ final String? description;
final String? avatarUrl;
const AccountBaseDTO({
@@ -16,8 +18,9 @@ class AccountBaseDTO extends StorableDTO {
required super.createdAt,
required super.updatedAt,
required this.name,
+ required this.description,
required this.avatarUrl,
- required this.locale,
+ required this.lastName,
});
factory AccountBaseDTO.fromJson(Map json) => _$AccountBaseDTOFromJson(json);
diff --git a/frontend/pshared/lib/data/dto/date_time.dart b/frontend/pshared/lib/data/dto/date_time.dart
new file mode 100644
index 0000000..f92e62f
--- /dev/null
+++ b/frontend/pshared/lib/data/dto/date_time.dart
@@ -0,0 +1,12 @@
+import 'package:json_annotation/json_annotation.dart';
+
+
+class UtcIso8601Converter implements JsonConverter {
+ const UtcIso8601Converter();
+
+ @override
+ DateTime fromJson(String json) => DateTime.parse(json).toUtc();
+
+ @override
+ String toJson(DateTime value) => value.toUtc().toIso8601String();
+}
diff --git a/frontend/pshared/lib/data/dto/describable.dart b/frontend/pshared/lib/data/dto/describable.dart
new file mode 100644
index 0000000..39e912d
--- /dev/null
+++ b/frontend/pshared/lib/data/dto/describable.dart
@@ -0,0 +1,18 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'describable.g.dart';
+
+
+@JsonSerializable()
+class DescribableDTO {
+ final String name;
+ final String? description;
+
+ const DescribableDTO({
+ required this.name,
+ this.description,
+ });
+
+ factory DescribableDTO.fromJson(Map json) => _$DescribableDTOFromJson(json);
+ Map toJson() => _$DescribableDTOToJson(this);
+}
diff --git a/frontend/pshared/lib/data/dto/organization.dart b/frontend/pshared/lib/data/dto/organization.dart
index 9e5cc76..d482a0c 100644
--- a/frontend/pshared/lib/data/dto/organization.dart
+++ b/frontend/pshared/lib/data/dto/organization.dart
@@ -1,18 +1,28 @@
import 'package:json_annotation/json_annotation.dart';
-import 'package:pshared/data/dto/storable.dart';
+
+import 'package:pshared/data/dto/date_time.dart';
+import 'package:pshared/data/dto/permissions/bound.dart';
part 'organization.g.dart';
@JsonSerializable()
-class OrganizationDTO extends StorableDTO {
+class OrganizationDTO extends PermissionBoundDTO {
+ final String name;
+ final String? description;
final String timeZone;
final String? logoUrl;
+ final String tenantRef;
const OrganizationDTO({
required super.id,
required super.createdAt,
required super.updatedAt,
+ required super.permissionRef,
+ required super.organizationRef,
+ required this.name,
+ required this.tenantRef,
+ this.description,
required this.timeZone,
this.logoUrl,
});
diff --git a/frontend/pshared/lib/data/dto/organization/bound.dart b/frontend/pshared/lib/data/dto/organization/bound.dart
new file mode 100644
index 0000000..8c5bd07
--- /dev/null
+++ b/frontend/pshared/lib/data/dto/organization/bound.dart
@@ -0,0 +1,23 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import 'package:pshared/data/dto/storable.dart';
+import 'package:pshared/data/dto/date_time.dart';
+
+part 'bound.g.dart';
+
+
+@JsonSerializable()
+class OrganizationBoundDTO extends StorableDTO {
+ final String organizationRef;
+
+ const OrganizationBoundDTO({
+ required super.id,
+ required super.createdAt,
+ required super.updatedAt,
+ required this.organizationRef,
+ });
+
+ factory OrganizationBoundDTO.fromJson(Map json) => _$OrganizationBoundDTOFromJson(json);
+ @override
+ Map toJson() => _$OrganizationBoundDTOToJson(this);
+}
diff --git a/frontend/pshared/lib/data/dto/organization/description.dart b/frontend/pshared/lib/data/dto/organization/description.dart
index 2814f8b..efe2f62 100644
--- a/frontend/pshared/lib/data/dto/organization/description.dart
+++ b/frontend/pshared/lib/data/dto/organization/description.dart
@@ -1,13 +1,17 @@
import 'package:json_annotation/json_annotation.dart';
+import 'package:pshared/data/dto/describable.dart';
+
part 'description.g.dart';
@JsonSerializable()
class OrganizationDescriptionDTO {
+ final DescribableDTO description;
final String? logoUrl;
const OrganizationDescriptionDTO({
+ required this.description,
this.logoUrl,
});
diff --git a/frontend/pshared/lib/data/dto/permissions/bound.dart b/frontend/pshared/lib/data/dto/permissions/bound.dart
new file mode 100644
index 0000000..02eb0d8
--- /dev/null
+++ b/frontend/pshared/lib/data/dto/permissions/bound.dart
@@ -0,0 +1,26 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import 'package:pshared/data/dto/date_time.dart';
+import 'package:pshared/data/dto/storable.dart';
+
+part 'bound.g.dart';
+
+
+@JsonSerializable()
+class PermissionBoundDTO extends StorableDTO {
+ final String permissionRef;
+ final String organizationRef;
+
+ const PermissionBoundDTO({
+ required super.id,
+ required super.createdAt,
+ required super.updatedAt,
+ required this.permissionRef,
+ required this.organizationRef,
+ });
+
+ factory PermissionBoundDTO.fromJson(Map json) => _$PermissionBoundDTOFromJson(json);
+
+ @override
+ Map toJson() => _$PermissionBoundDTOToJson(this);
+}
\ No newline at end of file
diff --git a/frontend/pshared/lib/data/dto/permissions/description/policy.dart b/frontend/pshared/lib/data/dto/permissions/description/policy.dart
index 04f4e43..1629e6c 100644
--- a/frontend/pshared/lib/data/dto/permissions/description/policy.dart
+++ b/frontend/pshared/lib/data/dto/permissions/description/policy.dart
@@ -1,12 +1,14 @@
import 'package:json_annotation/json_annotation.dart';
-import 'package:pshared/data/dto/storable.dart';
+
+import 'package:pshared/data/dto/storable/describable.dart';
+import 'package:pshared/data/dto/date_time.dart';
import 'package:pshared/models/resources.dart';
part 'policy.g.dart';
@JsonSerializable()
-class PolicyDescriptionDTO extends StorableDTO {
+class PolicyDescriptionDTO extends StorableDescribabaleDTO {
final List? resourceTypes;
final String? organizationRef;
@@ -14,6 +16,8 @@ class PolicyDescriptionDTO extends StorableDTO {
required super.id,
required super.createdAt,
required super.updatedAt,
+ required super.name,
+ required super.description,
required this.resourceTypes,
required this.organizationRef,
});
diff --git a/frontend/pshared/lib/data/dto/permissions/description/role.dart b/frontend/pshared/lib/data/dto/permissions/description/role.dart
index 8b92caf..d40e0d3 100644
--- a/frontend/pshared/lib/data/dto/permissions/description/role.dart
+++ b/frontend/pshared/lib/data/dto/permissions/description/role.dart
@@ -1,17 +1,21 @@
import 'package:json_annotation/json_annotation.dart';
-import 'package:pshared/data/dto/storable.dart';
+
+import 'package:pshared/data/dto/date_time.dart';
+import 'package:pshared/data/dto/storable/describable.dart';
part 'role.g.dart';
@JsonSerializable()
-class RoleDescriptionDTO extends StorableDTO {
+class RoleDescriptionDTO extends StorableDescribabaleDTO {
final String organizationRef;
const RoleDescriptionDTO({
required super.id,
required super.createdAt,
required super.updatedAt,
+ required super.name,
+ required super.description,
required this.organizationRef,
});
diff --git a/frontend/pshared/lib/data/dto/reference.dart b/frontend/pshared/lib/data/dto/reference.dart
new file mode 100644
index 0000000..31e7bf8
--- /dev/null
+++ b/frontend/pshared/lib/data/dto/reference.dart
@@ -0,0 +1,16 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'reference.g.dart';
+
+
+@JsonSerializable()
+class ReferenceDTO {
+ final String ref;
+
+ const ReferenceDTO({
+ required this.ref,
+ });
+
+ factory ReferenceDTO.fromJson(Map json) => _$ReferenceDTOFromJson(json);
+ Map toJson() => _$ReferenceDTOToJson(this);
+}
diff --git a/frontend/pshared/lib/data/dto/storable.dart b/frontend/pshared/lib/data/dto/storable.dart
index b189dff..d9ea76e 100644
--- a/frontend/pshared/lib/data/dto/storable.dart
+++ b/frontend/pshared/lib/data/dto/storable.dart
@@ -1,12 +1,18 @@
import 'package:json_annotation/json_annotation.dart';
+import 'package:pshared/data/dto/date_time.dart';
+
part 'storable.g.dart';
@JsonSerializable()
class StorableDTO {
final String id;
+
+ @UtcIso8601Converter()
final DateTime createdAt;
+
+ @UtcIso8601Converter()
final DateTime updatedAt;
const StorableDTO({
diff --git a/frontend/pshared/lib/data/dto/storable/describable.dart b/frontend/pshared/lib/data/dto/storable/describable.dart
new file mode 100644
index 0000000..42af849
--- /dev/null
+++ b/frontend/pshared/lib/data/dto/storable/describable.dart
@@ -0,0 +1,26 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import 'package:pshared/data/dto/date_time.dart';
+import 'package:pshared/data/dto/storable.dart';
+
+part 'describable.g.dart';
+
+
+@JsonSerializable()
+class StorableDescribabaleDTO extends StorableDTO {
+ final String name;
+ final String? description;
+
+ const StorableDescribabaleDTO({
+ required super.id,
+ required super.createdAt,
+ required super.updatedAt,
+ required this.name,
+ this.description,
+ });
+
+ factory StorableDescribabaleDTO.fromJson(Map json) => _$StorableDescribabaleDTOFromJson(json);
+
+ @override
+ Map toJson() => _$StorableDescribabaleDTOToJson(this);
+}
\ No newline at end of file
diff --git a/frontend/pshared/lib/data/mapper/.DS_Store b/frontend/pshared/lib/data/mapper/.DS_Store
deleted file mode 100644
index 226f367..0000000
Binary files a/frontend/pshared/lib/data/mapper/.DS_Store and /dev/null differ
diff --git a/frontend/pshared/lib/data/mapper/account/account.dart b/frontend/pshared/lib/data/mapper/account/account.dart
index fd13dca..6ab6523 100644
--- a/frontend/pshared/lib/data/mapper/account/account.dart
+++ b/frontend/pshared/lib/data/mapper/account/account.dart
@@ -1,5 +1,6 @@
import 'package:pshared/data/dto/account/account.dart';
import 'package:pshared/models/account/account.dart';
+import 'package:pshared/models/describable.dart';
import 'package:pshared/models/storable.dart';
@@ -9,6 +10,8 @@ extension AccountMapper on Account {
createdAt: createdAt,
updatedAt: updatedAt,
name: name,
+ lastName: lastName,
+ description: description,
avatarUrl: avatarUrl,
locale: locale,
login: login,
@@ -19,8 +22,9 @@ extension AccountDTOMapper on AccountDTO {
Account toDomain() => Account(
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
avatarUrl: avatarUrl,
+ describable: newDescribable(name: name, description: description),
+ lastName: lastName,
locale: locale,
login: login,
- name: name,
);
}
diff --git a/frontend/pshared/lib/data/mapper/account/base.dart b/frontend/pshared/lib/data/mapper/account/base.dart
index f0241ef..430bed7 100644
--- a/frontend/pshared/lib/data/mapper/account/base.dart
+++ b/frontend/pshared/lib/data/mapper/account/base.dart
@@ -1,5 +1,6 @@
import 'package:pshared/data/dto/account/base.dart';
import 'package:pshared/models/account/base.dart';
+import 'package:pshared/models/describable.dart';
import 'package:pshared/models/storable.dart';
@@ -8,17 +9,18 @@ extension AccountBaseMapper on AccountBase {
id: storable.id,
createdAt: storable.createdAt,
updatedAt: storable.updatedAt,
+ name: describable.name,
+ description: describable.description,
+ lastName: lastName,
avatarUrl: avatarUrl,
- name: name,
- locale: locale,
);
}
extension AccountDTOMapper on AccountBaseDTO {
AccountBase toDomain() => AccountBase(
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
+ describable: newDescribable(name: name, description: description),
+ lastName: lastName,
avatarUrl: avatarUrl,
- name: name,
- locale: locale,
);
}
diff --git a/frontend/pshared/lib/data/mapper/describable.dart b/frontend/pshared/lib/data/mapper/describable.dart
new file mode 100644
index 0000000..d9ab58e
--- /dev/null
+++ b/frontend/pshared/lib/data/mapper/describable.dart
@@ -0,0 +1,17 @@
+import 'package:pshared/data/dto/describable.dart';
+import 'package:pshared/models/describable.dart';
+
+
+extension DescribableMapper on Describable {
+ DescribableDTO toDTO() => DescribableDTO(
+ name: name,
+ description: description,
+ );
+}
+
+extension DescribableDTOMapper on DescribableDTO {
+ Describable toDomain() => newDescribable(
+ name: name,
+ description: description,
+ );
+}
\ No newline at end of file
diff --git a/frontend/pshared/lib/data/mapper/organization.dart b/frontend/pshared/lib/data/mapper/organization.dart
index 41970d5..71c2c70 100644
--- a/frontend/pshared/lib/data/mapper/organization.dart
+++ b/frontend/pshared/lib/data/mapper/organization.dart
@@ -1,5 +1,8 @@
import 'package:pshared/data/dto/organization.dart';
+import 'package:pshared/models/describable.dart';
+import 'package:pshared/models/organization/bound.dart';
import 'package:pshared/models/organization/organization.dart';
+import 'package:pshared/models/permissions/bound.dart';
import 'package:pshared/models/storable.dart';
@@ -8,15 +11,26 @@ extension OrganizationMapper on Organization {
id: storable.id,
createdAt: storable.createdAt,
updatedAt: storable.updatedAt,
+ name: describable.name,
+ description: describable.description,
timeZone: timeZone,
logoUrl: logoUrl,
+ organizationRef: permissionBound.organizationRef,
+ permissionRef: permissionBound.permissionRef,
+ tenantRef: tenantRef,
);
}
extension OrganizationDTOMapper on OrganizationDTO {
Organization toDomain() => Organization(
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
+ describable: newDescribable(name: name, description: description),
timeZone: timeZone,
logoUrl: logoUrl,
+ permissionBound: newPermissionBound(
+ organizationBound: newOrganizationBound(organizationRef: organizationRef),
+ permissionRef: permissionRef,
+ ),
+ tenantRef: tenantRef,
);
}
diff --git a/frontend/pshared/lib/data/mapper/organization/bound.dart b/frontend/pshared/lib/data/mapper/organization/bound.dart
new file mode 100644
index 0000000..6b81615
--- /dev/null
+++ b/frontend/pshared/lib/data/mapper/organization/bound.dart
@@ -0,0 +1,18 @@
+import 'package:pshared/data/dto/organization/bound.dart';
+import 'package:pshared/models/organization/bound.dart';
+
+
+extension OrganizationBoundMapper on OrganizationBound {
+ OrganizationBoundDTO toDTO() => OrganizationBoundDTO(
+ id: '', // OrganizationBound doesn't have storable fields, so we need to provide defaults
+ createdAt: DateTime.now(),
+ updatedAt: DateTime.now(),
+ organizationRef: organizationRef,
+ );
+}
+
+extension OrganizationBoundDTOMapper on OrganizationBoundDTO {
+ OrganizationBound toDomain() => newOrganizationBound(
+ organizationRef: organizationRef,
+ );
+}
diff --git a/frontend/pshared/lib/data/mapper/organization/description.dart b/frontend/pshared/lib/data/mapper/organization/description.dart
index d80483f..7e9c753 100644
--- a/frontend/pshared/lib/data/mapper/organization/description.dart
+++ b/frontend/pshared/lib/data/mapper/organization/description.dart
@@ -1,9 +1,11 @@
import 'package:pshared/data/dto/organization/description.dart';
+import 'package:pshared/data/mapper/describable.dart';
import 'package:pshared/models/organization/description.dart';
extension OrganizationDescriptionMapper on OrganizationDescription {
OrganizationDescriptionDTO toDTO() => OrganizationDescriptionDTO(
+ description: description.toDTO(),
logoUrl: logoUrl,
);
}
@@ -11,5 +13,6 @@ extension OrganizationDescriptionMapper on OrganizationDescription {
extension AccountDescriptionDTOMapper on OrganizationDescriptionDTO {
OrganizationDescription toDomain() => OrganizationDescription(
logoUrl: logoUrl,
+ description: description.toDomain(),
);
}
diff --git a/frontend/pshared/lib/data/mapper/permissions/descriptions/policy.dart b/frontend/pshared/lib/data/mapper/permissions/descriptions/policy.dart
index e444a9d..92268ba 100644
--- a/frontend/pshared/lib/data/mapper/permissions/descriptions/policy.dart
+++ b/frontend/pshared/lib/data/mapper/permissions/descriptions/policy.dart
@@ -1,4 +1,5 @@
import 'package:pshared/data/dto/permissions/description/policy.dart';
+import 'package:pshared/models/describable.dart';
import 'package:pshared/models/permissions/descriptions/policy.dart';
import 'package:pshared/models/storable.dart';
@@ -8,6 +9,8 @@ extension PolicyDescriptionMapper on PolicyDescription {
id: storable.id,
createdAt: storable.createdAt,
updatedAt: storable.updatedAt,
+ name: describable.name,
+ description: describable.description,
resourceTypes: resourceTypes,
organizationRef: organizationRef,
);
@@ -16,6 +19,7 @@ extension PolicyDescriptionMapper on PolicyDescription {
extension PolicyDescriptionDTOMapper on PolicyDescriptionDTO {
PolicyDescription toDomain() => PolicyDescription(
storable: newStorable(id: id, createdAt: createdAt, updatedAt: createdAt),
+ describable: newDescribable(name: name, description: description),
resourceTypes: resourceTypes,
organizationRef: organizationRef,
);
diff --git a/frontend/pshared/lib/data/mapper/permissions/descriptions/role.dart b/frontend/pshared/lib/data/mapper/permissions/descriptions/role.dart
index d0596cb..b3b8e2a 100644
--- a/frontend/pshared/lib/data/mapper/permissions/descriptions/role.dart
+++ b/frontend/pshared/lib/data/mapper/permissions/descriptions/role.dart
@@ -1,4 +1,5 @@
import 'package:pshared/data/dto/permissions/description/role.dart';
+import 'package:pshared/models/describable.dart';
import 'package:pshared/models/permissions/descriptions/role.dart';
import 'package:pshared/models/storable.dart';
@@ -8,6 +9,8 @@ extension RoleDescriptionMapper on RoleDescription {
id: storable.id,
createdAt: storable.createdAt,
updatedAt: storable.updatedAt,
+ name: describable.name,
+ description: describable.description,
organizationRef: organizationRef,
);
}
@@ -15,6 +18,7 @@ extension RoleDescriptionMapper on RoleDescription {
extension RoleDescriptionDTOMapper on RoleDescriptionDTO {
RoleDescription toDomain() => RoleDescription(
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
+ describable: newDescribable(name: name, description: description),
organizationRef: organizationRef,
);
}
diff --git a/frontend/pshared/lib/data/mapper/reference.dart b/frontend/pshared/lib/data/mapper/reference.dart
new file mode 100644
index 0000000..b6fbe5f
--- /dev/null
+++ b/frontend/pshared/lib/data/mapper/reference.dart
@@ -0,0 +1,11 @@
+import 'package:pshared/data/dto/reference.dart';
+import 'package:pshared/models/reference.dart';
+
+
+extension ReferenceMapper on Reference {
+ ReferenceDTO toDTO() => ReferenceDTO(ref: ref);
+}
+
+extension ReferenceDTOMapper on ReferenceDTO {
+ Reference toDomain() => newReference(ref: ref);
+}
diff --git a/frontend/pshared/lib/data/mapper/storable.dart b/frontend/pshared/lib/data/mapper/storable.dart
index 8990947..1696d92 100644
--- a/frontend/pshared/lib/data/mapper/storable.dart
+++ b/frontend/pshared/lib/data/mapper/storable.dart
@@ -3,7 +3,7 @@ import 'package:pshared/models/storable.dart';
extension StorableMapper on Storable {
- StorableDTO toDTO() => StorableDTO(id: id, createdAt: createdAt, updatedAt: updatedAt);
+ StorableDTO toDTO() => StorableDTO(id: id, createdAt: createdAt.toUtc(), updatedAt: updatedAt.toUtc());
}
extension StorableDTOMapper on StorableDTO {
diff --git a/frontend/pshared/lib/models/account/account.dart b/frontend/pshared/lib/models/account/account.dart
index 2d41b0a..8e97466 100644
--- a/frontend/pshared/lib/models/account/account.dart
+++ b/frontend/pshared/lib/models/account/account.dart
@@ -1,36 +1,35 @@
+import 'package:flutter/foundation.dart';
+
import 'package:pshared/models/account/base.dart';
+import 'package:pshared/models/describable.dart';
+@immutable
class Account extends AccountBase {
final String login;
+ final String locale;
const Account({
required super.storable,
+ required super.describable,
required super.avatarUrl,
+ required super.lastName,
required this.login,
- required super.locale,
- required super.name,
+ required this.locale,
});
- factory Account.fromBase(AccountBase accountBase, String login) => Account(
- storable: accountBase.storable,
- avatarUrl: accountBase.avatarUrl,
- locale: accountBase.locale,
- name: accountBase.name,
- login: login,
- );
-
@override
Account copyWith({
+ Describable? describable,
+ String? lastName,
String? Function()? avatarUrl,
- String? name,
String? locale,
- }) {
- final updatedBase = super.copyWith(
- avatarUrl: avatarUrl,
- name: name,
- locale: locale,
- );
- return Account.fromBase(updatedBase, login);
- }
+ }) => Account(
+ storable: storable,
+ describable: describableCopyWithOther(this.describable, describable),
+ lastName: lastName ?? this.lastName,
+ avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl,
+ login: login,
+ locale: locale ?? this.locale,
+ );
}
\ No newline at end of file
diff --git a/frontend/pshared/lib/models/account/base.dart b/frontend/pshared/lib/models/account/base.dart
index 5fdd112..fd5722e 100644
--- a/frontend/pshared/lib/models/account/base.dart
+++ b/frontend/pshared/lib/models/account/base.dart
@@ -1,8 +1,16 @@
+import 'package:flutter/foundation.dart';
+
+import 'package:pshared/models/describable.dart';
import 'package:pshared/models/storable.dart';
+import 'package:pshared/models/storable/describable.dart';
+import 'package:pshared/utils/name_initials.dart';
-class AccountBase implements Storable {
+@immutable
+class AccountBase implements StorableDescribable {
final Storable storable;
+ final Describable describable;
+ final String lastName;
@override
String get id => storable.id;
@@ -10,26 +18,30 @@ class AccountBase implements Storable {
DateTime get createdAt => storable.createdAt;
@override
DateTime get updatedAt => storable.updatedAt;
+ @override
+ String get name => describable.name;
+ @override
+ String? get description => describable.description;
final String? avatarUrl;
- final String name;
- final String locale;
const AccountBase({
required this.storable,
- required this.name,
- required this.locale,
+ required this.describable,
required this.avatarUrl,
+ required this.lastName,
});
+ String get nameInitials => getNameInitials(describable.name);
+
AccountBase copyWith({
+ Describable? describable,
+ String? lastName,
String? Function()? avatarUrl,
- String? name,
- String? locale,
}) => AccountBase(
storable: storable,
avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl,
- locale: locale ?? this.locale,
- name: name ?? this.name,
+ describable: describable ?? this.describable,
+ lastName: lastName ?? this.lastName,
);
}
diff --git a/frontend/pshared/lib/models/auth/login_outcome.dart b/frontend/pshared/lib/models/auth/login_outcome.dart
new file mode 100644
index 0000000..24df29e
--- /dev/null
+++ b/frontend/pshared/lib/models/auth/login_outcome.dart
@@ -0,0 +1,17 @@
+import 'package:pshared/models/account/account.dart';
+import 'package:pshared/models/auth/pending_login.dart';
+
+
+class LoginOutcome {
+ final Account? account;
+ final PendingLogin? pending;
+
+ const LoginOutcome._({this.account, this.pending});
+
+ factory LoginOutcome.completed(Account account) => LoginOutcome._(account: account);
+
+ factory LoginOutcome.pending(PendingLogin pending) => LoginOutcome._(pending: pending);
+
+ bool get isPending => pending != null;
+ bool get isCompleted => account != null;
+}
diff --git a/frontend/pshared/lib/models/auth/pending_login.dart b/frontend/pshared/lib/models/auth/pending_login.dart
new file mode 100644
index 0000000..3585bcc
--- /dev/null
+++ b/frontend/pshared/lib/models/auth/pending_login.dart
@@ -0,0 +1,33 @@
+import 'package:pshared/api/responses/login_pending.dart';
+import 'package:pshared/api/responses/token.dart';
+import 'package:pshared/data/mapper/account/account.dart';
+import 'package:pshared/models/account/account.dart';
+import 'package:pshared/models/session_identifier.dart';
+
+
+class PendingLogin {
+ final Account account;
+ final TokenData pendingToken;
+ final String destination;
+ final int ttlSeconds;
+ final SessionIdentifier session;
+
+ const PendingLogin({
+ required this.account,
+ required this.pendingToken,
+ required this.destination,
+ required this.ttlSeconds,
+ required this.session,
+ });
+
+ factory PendingLogin.fromResponse(
+ PendingLoginResponse response, {
+ required SessionIdentifier session,
+ }) => PendingLogin(
+ account: response.account.account.toDomain(),
+ pendingToken: response.pendingToken,
+ destination: response.destination,
+ ttlSeconds: response.ttlSeconds,
+ session: session,
+ );
+}
diff --git a/frontend/pshared/lib/models/describable.dart b/frontend/pshared/lib/models/describable.dart
new file mode 100644
index 0000000..22a7f52
--- /dev/null
+++ b/frontend/pshared/lib/models/describable.dart
@@ -0,0 +1,39 @@
+import 'package:flutter/foundation.dart';
+
+
+abstract class Describable {
+ String get name;
+ String? get description;
+}
+
+@immutable
+class _DescribableImp implements Describable {
+ @override
+ final String name;
+ @override
+ final String? description;
+
+ const _DescribableImp({
+ required this.name,
+ required this.description,
+ });
+}
+
+Describable newDescribable({required String name, String? description}) =>
+ _DescribableImp(name: name, description: description);
+
+
+extension DescribableCopier on Describable {
+ Describable copyWith({
+ String? name,
+ String? Function()? description,
+ }) => newDescribable(
+ name: name ?? this.name,
+ description: description != null ? description() : this.description,
+ );
+}
+
+Describable describableCopyWithOther(Describable current, Describable? other) => current.copyWith(
+ name: other?.name,
+ description: () => other?.description,
+);
\ No newline at end of file
diff --git a/frontend/pshared/lib/models/organization/bound.dart b/frontend/pshared/lib/models/organization/bound.dart
new file mode 100644
index 0000000..acce7fb
--- /dev/null
+++ b/frontend/pshared/lib/models/organization/bound.dart
@@ -0,0 +1,18 @@
+import 'package:flutter/foundation.dart';
+
+
+abstract class OrganizationBound {
+ String get organizationRef;
+}
+
+@immutable
+class _OrganizationBoundImp implements OrganizationBound {
+ @override
+ final String organizationRef;
+
+ const _OrganizationBoundImp({
+ required this.organizationRef,
+ });
+}
+
+OrganizationBound newOrganizationBound({ required String organizationRef }) => _OrganizationBoundImp(organizationRef: organizationRef);
\ No newline at end of file
diff --git a/frontend/pshared/lib/models/organization/description.dart b/frontend/pshared/lib/models/organization/description.dart
index 7e6f5f9..81d199f 100644
--- a/frontend/pshared/lib/models/organization/description.dart
+++ b/frontend/pshared/lib/models/organization/description.dart
@@ -1,7 +1,12 @@
+import 'package:pshared/models/describable.dart';
+
+
class OrganizationDescription {
+ final Describable description;
final String? logoUrl;
const OrganizationDescription({
+ required this.description,
this.logoUrl,
});
}
\ No newline at end of file
diff --git a/frontend/pshared/lib/models/organization/organization.dart b/frontend/pshared/lib/models/organization/organization.dart
index b04182a..1150e8b 100644
--- a/frontend/pshared/lib/models/organization/organization.dart
+++ b/frontend/pshared/lib/models/organization/organization.dart
@@ -1,8 +1,13 @@
+import 'package:pshared/models/describable.dart';
+import 'package:pshared/models/permissions/bound.dart';
+import 'package:pshared/models/permissions/bound/describable.dart';
import 'package:pshared/models/storable.dart';
-class Organization implements Storable {
+class Organization implements PermissionBoundStorableDescribable {
final Storable storable;
+ final PermissionBound permissionBound;
+ final Describable describable;
@override
String get id => storable.id;
@@ -10,25 +15,39 @@ class Organization implements Storable {
DateTime get createdAt => storable.createdAt;
@override
DateTime get updatedAt => storable.updatedAt;
+ @override
+ String get organizationRef => permissionBound.organizationRef;
+ @override
+ String get permissionRef => permissionBound.permissionRef;
+ @override
+ String get name => describable.name;
+ @override
+ String? get description => describable.description;
final String timeZone;
final String? logoUrl;
+ final String tenantRef;
const Organization({
required this.storable,
+ required this.describable,
required this.timeZone,
+ required this.permissionBound,
+ required this.tenantRef,
this.logoUrl,
});
Organization copyWith({
- String? name,
- String? Function()? description,
+ Describable? describable,
String? timeZone,
String? Function()? logoUrl,
}) => Organization(
storable: storable, // Same Storable, same id
+ describable: describableCopyWithOther(this.describable, describable),
timeZone: timeZone ?? this.timeZone,
logoUrl: logoUrl != null ? logoUrl() : this.logoUrl,
+ permissionBound: permissionBound,
+ tenantRef: tenantRef,
);
}
diff --git a/frontend/pshared/lib/models/permission_bound.dart b/frontend/pshared/lib/models/permission_bound.dart
deleted file mode 100644
index 6618f26..0000000
--- a/frontend/pshared/lib/models/permission_bound.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-import 'package:pshared/config/constants.dart';
-
-
-abstract class PermissionBound {
- String get permissionRef;
- String get organizationRef;
-}
-
-class _PermissionBoundImp implements PermissionBound {
- @override
- final String permissionRef;
- @override
- final String organizationRef;
-
- const _PermissionBoundImp({
- required this.permissionRef,
- required this.organizationRef,
- });
-}
-
-PermissionBound newPermissionBound({ required String organizationRef, String? permissionRef}) =>
- _PermissionBoundImp(permissionRef: permissionRef ?? Constants.nilObjectRef, organizationRef: organizationRef);
\ No newline at end of file
diff --git a/frontend/pshared/lib/models/permissions/bound.dart b/frontend/pshared/lib/models/permissions/bound.dart
new file mode 100644
index 0000000..476b2a4
--- /dev/null
+++ b/frontend/pshared/lib/models/permissions/bound.dart
@@ -0,0 +1,32 @@
+import 'package:flutter/foundation.dart';
+
+import 'package:pshared/config/constants.dart';
+import 'package:pshared/models/organization/bound.dart';
+
+
+abstract class PermissionBound extends OrganizationBound {
+ String get permissionRef;
+}
+
+@immutable
+class _PermissionBoundImp implements PermissionBound {
+ @override
+ final String permissionRef;
+ final OrganizationBound organizationBound;
+
+ @override
+ get organizationRef => organizationBound.organizationRef;
+
+ const _PermissionBoundImp({
+ required this.permissionRef,
+ required this.organizationBound,
+ });
+}
+
+PermissionBound newPermissionBound({
+ required OrganizationBound organizationBound,
+ String? permissionRef,
+}) => _PermissionBoundImp(
+ permissionRef: permissionRef ?? Constants.nilObjectRef,
+ organizationBound: organizationBound,
+);
\ No newline at end of file
diff --git a/frontend/pshared/lib/models/permissions/bound/describable.dart b/frontend/pshared/lib/models/permissions/bound/describable.dart
new file mode 100644
index 0000000..3b141ba
--- /dev/null
+++ b/frontend/pshared/lib/models/permissions/bound/describable.dart
@@ -0,0 +1,6 @@
+import 'package:pshared/models/permissions/bound/storable.dart';
+import 'package:pshared/models/storable/describable.dart';
+
+
+abstract class PermissionBoundStorableDescribable implements PermissionBoundStorable, StorableDescribable {
+}
diff --git a/frontend/pshared/lib/models/permission_bound_storable.dart b/frontend/pshared/lib/models/permissions/bound/storable.dart
similarity index 69%
rename from frontend/pshared/lib/models/permission_bound_storable.dart
rename to frontend/pshared/lib/models/permissions/bound/storable.dart
index 4ee0d63..47d783c 100644
--- a/frontend/pshared/lib/models/permission_bound_storable.dart
+++ b/frontend/pshared/lib/models/permissions/bound/storable.dart
@@ -1,4 +1,4 @@
-import 'package:pshared/models/permission_bound.dart';
+import 'package:pshared/models/permissions/bound.dart';
import 'package:pshared/models/storable.dart';
diff --git a/frontend/pshared/lib/models/permissions/descriptions/policy.dart b/frontend/pshared/lib/models/permissions/descriptions/policy.dart
index 6aecef0..ac746ce 100644
--- a/frontend/pshared/lib/models/permissions/descriptions/policy.dart
+++ b/frontend/pshared/lib/models/permissions/descriptions/policy.dart
@@ -1,9 +1,12 @@
+import 'package:pshared/models/describable.dart';
import 'package:pshared/models/resources.dart';
import 'package:pshared/models/storable.dart';
+import 'package:pshared/models/storable/describable.dart';
-class PolicyDescription implements Storable {
+class PolicyDescription implements StorableDescribable {
final Storable storable;
+ final Describable describable;
final List? resourceTypes;
final String? organizationRef;
@@ -13,9 +16,14 @@ class PolicyDescription implements Storable {
DateTime get createdAt => storable.createdAt;
@override
DateTime get updatedAt => storable.updatedAt;
+ @override
+ String get name => describable.name;
+ @override
+ String? get description => describable.description;
const PolicyDescription({
required this.storable,
+ required this.describable,
required this.resourceTypes,
required this.organizationRef,
});
diff --git a/frontend/pshared/lib/models/permissions/descriptions/role.dart b/frontend/pshared/lib/models/permissions/descriptions/role.dart
index ecde4fb..1e04a27 100644
--- a/frontend/pshared/lib/models/permissions/descriptions/role.dart
+++ b/frontend/pshared/lib/models/permissions/descriptions/role.dart
@@ -1,8 +1,11 @@
+import 'package:pshared/models/describable.dart';
import 'package:pshared/models/storable.dart';
+import 'package:pshared/models/storable/describable.dart';
-class RoleDescription implements Storable {
+class RoleDescription implements StorableDescribable {
final Storable storable;
+ final Describable describable;
@override
String get id => storable.id;
@@ -10,18 +13,25 @@ class RoleDescription implements Storable {
DateTime get createdAt => storable.createdAt;
@override
DateTime get updatedAt => storable.updatedAt;
+ @override
+ String get name => describable.name;
+ @override
+ String? get description => describable.description;
final String organizationRef;
const RoleDescription({
required this.storable,
+ required this.describable,
required this.organizationRef,
});
factory RoleDescription.build({
+ required Describable roleDescription,
required String organizationRef,
}) => RoleDescription(
storable: newStorable(),
+ describable: roleDescription,
organizationRef: organizationRef
);
}
diff --git a/frontend/pshared/lib/models/reference.dart b/frontend/pshared/lib/models/reference.dart
new file mode 100644
index 0000000..3bcc422
--- /dev/null
+++ b/frontend/pshared/lib/models/reference.dart
@@ -0,0 +1,22 @@
+abstract class Reference {
+ String get ref;
+}
+
+class _ReferenceImp implements Reference {
+ @override
+ final String ref;
+
+ const _ReferenceImp({
+ required this.ref,
+ });
+}
+
+Reference newReference({required String ref}) => _ReferenceImp(ref: ref);
+
+extension ReferenceCopier on Reference {
+ Reference copyWith({
+ String? ref,
+ }) => newReference(
+ ref: ref ?? this.ref,
+ );
+}
diff --git a/frontend/pshared/lib/models/session_identifier.dart b/frontend/pshared/lib/models/session_identifier.dart
new file mode 100644
index 0000000..106f8f8
--- /dev/null
+++ b/frontend/pshared/lib/models/session_identifier.dart
@@ -0,0 +1,18 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'session_identifier.g.dart';
+
+@JsonSerializable()
+class SessionIdentifier {
+ final String clientId;
+ final String deviceId;
+
+ const SessionIdentifier({
+ required this.clientId,
+ required this.deviceId,
+ });
+
+ factory SessionIdentifier.fromJson(Map json) => _$SessionIdentifierFromJson(json);
+
+ Map toJson() => _$SessionIdentifierToJson(this);
+}
diff --git a/frontend/pshared/lib/models/storable.dart b/frontend/pshared/lib/models/storable.dart
index ce95482..dfd59e3 100644
--- a/frontend/pshared/lib/models/storable.dart
+++ b/frontend/pshared/lib/models/storable.dart
@@ -1,4 +1,6 @@
-import 'package:pshared/config/constants.dart';
+import 'package:flutter/foundation.dart';
+
+import 'package:pshared/config/web.dart';
abstract class Storable {
@@ -7,6 +9,7 @@ abstract class Storable {
DateTime get updatedAt;
}
+@immutable
class _StorableImp implements Storable {
@override
final String id;
diff --git a/frontend/pshared/lib/models/storable/describable.dart b/frontend/pshared/lib/models/storable/describable.dart
new file mode 100644
index 0000000..6d792e6
--- /dev/null
+++ b/frontend/pshared/lib/models/storable/describable.dart
@@ -0,0 +1,6 @@
+import 'package:pshared/models/describable.dart';
+import 'package:pshared/models/storable.dart';
+
+
+abstract class StorableDescribable implements Storable, Describable {
+}
\ No newline at end of file
diff --git a/frontend/pshared/lib/provider/account.dart b/frontend/pshared/lib/provider/account.dart
index 111ae4b..fea9b02 100644
--- a/frontend/pshared/lib/provider/account.dart
+++ b/frontend/pshared/lib/provider/account.dart
@@ -4,21 +4,52 @@ import 'package:share_plus/share_plus.dart';
import 'package:pshared/api/errors/unauthorized.dart';
import 'package:pshared/api/requests/signup.dart';
+import 'package:pshared/api/requests/login_data.dart';
+import 'package:pshared/config/constants.dart';
import 'package:pshared/models/account/account.dart';
-import 'package:pshared/provider/exception.dart';
+import 'package:pshared/models/auth/login_outcome.dart';
+import 'package:pshared/models/auth/pending_login.dart';
+import 'package:pshared/models/describable.dart';
+import 'package:pshared/models/storable.dart';
+import 'package:pshared/provider/locale.dart';
import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/account.dart';
+import 'package:pshared/service/authorization/service.dart';
+import 'package:pshared/utils/exception.dart';
class AccountProvider extends ChangeNotifier {
+ static String get currentUserRef => Constants.nilObjectRef;
+
// The resource now wraps our Account? state along with its loading/error state.
Resource _resource = Resource(data: null);
Resource get resource => _resource;
+ late LocaleProvider _localeProvider;
+ PendingLogin? _pendingLogin;
Account? get account => _resource.data;
+ PendingLogin? get pendingLogin => _pendingLogin;
bool get isLoggedIn => account != null;
bool get isLoading => _resource.isLoading;
Object? get error => _resource.error;
+ bool get isReady => (!isLoading) && (account != null);
+
+ Account? currentUser() {
+ final acc = account;
+ if (acc == null) return null;
+ return Account(
+ storable: newStorable(
+ id: currentUserRef,
+ createdAt: acc.createdAt,
+ updatedAt: acc.updatedAt,
+ ),
+ describable: acc.describable,
+ lastName: acc.lastName,
+ avatarUrl: acc.avatarUrl,
+ login: acc.login,
+ locale: acc.locale,
+ );
+ }
// Private helper to update the resource and notify listeners.
void _setResource(Resource newResource) {
@@ -26,28 +57,50 @@ class AccountProvider extends ChangeNotifier {
notifyListeners();
}
+ void updateProvider(LocaleProvider localeProvider) => _localeProvider = localeProvider;
- Future login({
+ void _pickupLocale(String locale) => _localeProvider.setLocale(Locale(locale));
+
+ Future login({
required String email,
required String password,
required String locale,
}) async {
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
- final acc = await AccountService.login(email, password, locale);
- _setResource(Resource(data: acc, isLoading: false));
- return acc;
+ final outcome = await AccountService.login(LoginData.build(
+ login: email,
+ password: password,
+ locale: locale,
+ ));
+ if (outcome.account != null) {
+ _setResource(Resource(data: outcome.account, isLoading: false));
+ _pickupLocale(outcome.account!.locale);
+ } else {
+ _pendingLogin = outcome.pending;
+ _setResource(_resource.copyWith(isLoading: false));
+ }
+ return outcome;
} catch (e) {
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
rethrow;
}
}
+ void completePendingLogin(Account account) {
+ _pendingLogin = null;
+ _setResource(Resource(data: account, isLoading: false, error: null));
+ _pickupLocale(account.locale);
+ }
+
+ Future isAuthorizationStored() async => AuthorizationService.isAuthorizationStored();
+
Future restore() async {
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
final acc = await AccountService.restore();
_setResource(Resource(data: acc, isLoading: false));
+ _pickupLocale(acc.locale);
return acc;
} catch (e) {
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
@@ -55,24 +108,20 @@ class AccountProvider extends ChangeNotifier {
}
}
- Future signup(
- String name,
- String login,
- String password,
- String locale,
- String organizationName,
- String timezone,
- ) async {
+ Future signup({
+ required AccountData account,
+ required Describable organization,
+ required String timezone,
+ required Describable ownerRole,
+ }) async {
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
await AccountService.signup(
SignupRequest.build(
- name: name,
- login: login.trim().toLowerCase(),
- password: password,
- locale: locale,
- organizationName: organizationName,
+ account: account,
+ organization: organization,
organizationTimeZone: timezone,
+ ownerRole: ownerRole,
),
);
// Signup might not automatically log in the user,
@@ -96,6 +145,7 @@ class AccountProvider extends ChangeNotifier {
}
Future update({
+ Describable? describable,
String? locale,
String? avatarUrl,
String? notificationFrequency,
@@ -105,6 +155,7 @@ class AccountProvider extends ChangeNotifier {
try {
final updated = await AccountService.update(
account!.copyWith(
+ describable: describable,
avatarUrl: () => avatarUrl ?? account!.avatarUrl,
locale: locale ?? account!.locale,
),
@@ -141,4 +192,26 @@ class AccountProvider extends ChangeNotifier {
rethrow;
}
}
+
+ Future forgotPassword(String email) async {
+ _setResource(_resource.copyWith(isLoading: true, error: null));
+ try {
+ await AccountService.forgotPassword(email);
+ _setResource(_resource.copyWith(isLoading: false));
+ } catch (e) {
+ _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
+ rethrow;
+ }
+ }
+
+ Future resetPassword(String accountId, String token, String newPassword) async {
+ _setResource(_resource.copyWith(isLoading: true, error: null));
+ try {
+ await AccountService.resetPassword(accountId, token, newPassword);
+ _setResource(_resource.copyWith(isLoading: false));
+ } catch (e) {
+ _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
+ rethrow;
+ }
+ }
}
diff --git a/frontend/pshared/lib/provider/organizations.dart b/frontend/pshared/lib/provider/organizations.dart
index c338885..2a06728 100644
--- a/frontend/pshared/lib/provider/organizations.dart
+++ b/frontend/pshared/lib/provider/organizations.dart
@@ -5,9 +5,9 @@ import 'package:collection/collection.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/models/organization/organization.dart';
import 'package:pshared/provider/resource.dart';
-import 'package:pshared/provider/exception.dart';
import 'package:pshared/service/organization.dart';
import 'package:pshared/service/secure_storage.dart';
+import 'package:pshared/utils/exception.dart';
class OrganizationsProvider extends ChangeNotifier {
diff --git a/frontend/pshared/lib/provider/pfe/provider.dart b/frontend/pshared/lib/provider/pfe/provider.dart
deleted file mode 100644
index 6fff15b..0000000
--- a/frontend/pshared/lib/provider/pfe/provider.dart
+++ /dev/null
@@ -1,51 +0,0 @@
-import 'package:flutter/material.dart';
-
-import 'package:pshared/service/pfe/service.dart';
-import 'package:pshared/provider/exception.dart';
-import 'package:pshared/provider/resource.dart';
-import 'package:pshared/service/account.dart';
-
-
-class PfeProvider extends ChangeNotifier {
- // The resource now wraps our Account? state along with its loading/error state.
- Resource _resource = Resource(data: null);
- Resource get resource => _resource;
-
- String? get session => _resource.data;
- bool get isLoggedIn => session != null;
- bool get isLoading => _resource.isLoading;
- Object? get error => _resource.error;
-
- // Private helper to update the resource and notify listeners.
- void _setResource(Resource newResource) {
- _resource = newResource;
- notifyListeners();
- }
-
-
- Future login({
- required String email,
- required String password,
- }) async {
- _setResource(_resource.copyWith(isLoading: true, error: null));
- try {
- final acc = await PfeService.login(email, password);
- _setResource(Resource(data: acc, isLoading: false));
- return acc;
- } catch (e) {
- _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
- rethrow;
- }
- }
-
- Future logout() async {
- _setResource(_resource.copyWith(isLoading: true, error: null));
- try {
- await AccountService.logout();
- _setResource(Resource(data: null, isLoading: false));
- } catch (e) {
- _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
- rethrow;
- }
- }
-}
diff --git a/frontend/pshared/lib/provider/template.dart b/frontend/pshared/lib/provider/template.dart
index 0a8d819..c26e89c 100644
--- a/frontend/pshared/lib/provider/template.dart
+++ b/frontend/pshared/lib/provider/template.dart
@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
-import 'package:pshared/models/permission_bound_storable.dart';
-import 'package:pshared/provider/exception.dart';
+import 'package:pshared/models/permissions/bound/storable.dart';
import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/template.dart';
+import 'package:pshared/utils/exception.dart';
List mergeLists({
diff --git a/frontend/pshared/lib/service/account.dart b/frontend/pshared/lib/service/account.dart
index a51ccff..facb9a1 100644
--- a/frontend/pshared/lib/service/account.dart
+++ b/frontend/pshared/lib/service/account.dart
@@ -4,10 +4,17 @@ import 'package:share_plus/share_plus.dart';
import 'package:pshared/api/requests/signup.dart';
import 'package:pshared/api/responses/account.dart';
-import 'package:pshared/api/requests/change_password.dart';
+import 'package:pshared/api/requests/login_data.dart';
+import 'package:pshared/api/requests/password/change.dart';
+import 'package:pshared/api/requests/password/forgot.dart';
+import 'package:pshared/api/requests/password/reset.dart';
+import 'package:pshared/api/responses/login.dart';
import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/models/account/account.dart';
+import 'package:pshared/models/auth/login_outcome.dart';
+import 'package:pshared/models/auth/pending_login.dart';
import 'package:pshared/service/authorization/service.dart';
+import 'package:pshared/service/authorization/storage.dart';
import 'package:pshared/service/files.dart';
import 'package:pshared/service/services.dart';
import 'package:pshared/utils/http/requests.dart';
@@ -17,9 +24,44 @@ class AccountService {
static final _logger = Logger('service.account');
static const String _objectType = Services.account;
- static Future login(String email, String password, String locale) async {
+ static Future login(LoginData login) async {
_logger.fine('Logging in');
- return AuthorizationService.login(_objectType, email, password, locale);
+ return AuthorizationService.login(_objectType, login);
+ }
+
+ static Future resendLoginCode(PendingLogin pending, {String? destination}) async {
+ await getPOSTResponse(
+ _objectType,
+ 'confirmations/resend',
+ {
+ 'target': 'login',
+ if (destination != null) 'destination': destination,
+ },
+ authToken: pending.pendingToken.token,
+ );
+ }
+
+ static Future confirmLoginCode({
+ required PendingLogin pending,
+ required String code,
+ String? destination,
+ }) async {
+ final response = await getPOSTResponse(
+ _objectType,
+ 'confirmations/verify',
+ {
+ 'target': 'login',
+ 'code': code,
+ if (destination != null) 'destination': destination,
+ 'sessionIdentifier': pending.session.toJson(),
+ },
+ authToken: pending.pendingToken.token,
+ );
+
+ final loginResponse = LoginResponse.fromJson(response);
+ await AuthorizationStorage.updateToken(loginResponse.accessToken);
+ await AuthorizationStorage.updateRefreshToken(loginResponse.refreshToken);
+ return loginResponse.account.toDomain();
}
static Future restore() async {
@@ -27,6 +69,7 @@ class AccountService {
}
static Future signup(SignupRequest request) async {
+ // Use regular HTTP for public signup endpoint (no auth needed)
await getPOSTResponse(_objectType, 'signup', request.toJson());
}
@@ -42,9 +85,20 @@ class AccountService {
static Future update(Account account) async {
_logger.fine('Patching account ${account.id}');
+ // Use AuthorizationService for authenticated operations
return _getAccount(AuthorizationService.getPUTResponse(_objectType, '', account.toDTO().toJson()));
}
+ static Future forgotPassword(String email) async {
+ _logger.fine('Requesting password reset for email: $email');
+ await getPUTResponse(_objectType, 'password', ForgotPasswordRequest.build(login: email).toJson());
+ }
+
+ static Future resetPassword(String accountRef, String token, String newPassword) async {
+ _logger.fine('Resetting password for account: $accountRef');
+ await getPOSTResponse(_objectType, 'password/reset/$accountRef/$token', ResetPasswordRequest.build(password: newPassword).toJson());
+ }
+
static Future changePassword(String oldPassword, String newPassword) async {
_logger.fine('Changing password');
return _getAccount(AuthorizationService.getPATCHResponse(
diff --git a/frontend/pshared/lib/service/authorization/circuit_breaker.dart b/frontend/pshared/lib/service/authorization/circuit_breaker.dart
new file mode 100644
index 0000000..d64d48b
--- /dev/null
+++ b/frontend/pshared/lib/service/authorization/circuit_breaker.dart
@@ -0,0 +1,92 @@
+import 'package:logging/logging.dart';
+
+/// Circuit breaker pattern implementation for authentication service failures
+class AuthCircuitBreaker {
+ static final _logger = Logger('service.auth_circuit_breaker');
+
+ static int _failureCount = 0;
+ static DateTime? _lastFailure;
+ static const int _failureThreshold = 3;
+ static const Duration _recoveryTime = Duration(minutes: 5);
+
+ /// Returns true if the circuit breaker is open (blocking operations)
+ static bool get isOpen {
+ if (_failureCount < _failureThreshold) return false;
+ if (_lastFailure == null) return false;
+
+ final isOpen = DateTime.now().difference(_lastFailure!) < _recoveryTime;
+ if (isOpen) {
+ _logger.warning('Circuit breaker is OPEN. Failure count: $_failureCount, last failure: $_lastFailure');
+ }
+ return isOpen;
+ }
+
+ /// Returns true if the circuit breaker is in half-open state (allowing test requests)
+ static bool get isHalfOpen {
+ if (_failureCount < _failureThreshold) return false;
+ if (_lastFailure == null) return false;
+
+ return DateTime.now().difference(_lastFailure!) >= _recoveryTime;
+ }
+
+ /// Executes an operation with circuit breaker protection
+ static Future execute(Future Function() operation) async {
+ if (isOpen) {
+ final timeSinceFailure = _lastFailure != null
+ ? DateTime.now().difference(_lastFailure!)
+ : Duration.zero;
+ final timeUntilRecovery = _recoveryTime - timeSinceFailure;
+
+ _logger.warning('Circuit breaker blocking operation. Recovery in: ${timeUntilRecovery.inSeconds}s');
+ throw Exception('Auth service temporarily unavailable. Try again in ${timeUntilRecovery.inMinutes} minutes.');
+ }
+
+ try {
+ _logger.fine('Executing operation through circuit breaker');
+ final result = await operation();
+ _reset();
+ return result;
+ } catch (e) {
+ _recordFailure();
+ rethrow;
+ }
+ }
+
+ /// Records a failure and updates the circuit breaker state
+ static void _recordFailure() {
+ _failureCount++;
+ _lastFailure = DateTime.now();
+ _logger.warning('Auth circuit breaker recorded failure #$_failureCount at $_lastFailure');
+
+ if (_failureCount >= _failureThreshold) {
+ _logger.severe('Auth circuit breaker OPENED after $_failureCount failures');
+ }
+ }
+
+ /// Resets the circuit breaker to closed state
+ static void _reset() {
+ if (_failureCount > 0) {
+ _logger.info('Auth circuit breaker CLOSED - resetting failure count from $_failureCount to 0');
+ }
+ _failureCount = 0;
+ _lastFailure = null;
+ }
+
+ /// Manual reset (for testing or administrative purposes)
+ static void manualReset() {
+ _logger.info('Auth circuit breaker manually reset');
+ _reset();
+ }
+
+ /// Get current status for debugging
+ static Map getStatus() {
+ return {
+ 'failureCount': _failureCount,
+ 'lastFailure': _lastFailure?.toIso8601String(),
+ 'isOpen': isOpen,
+ 'isHalfOpen': isHalfOpen,
+ 'threshold': _failureThreshold,
+ 'recoveryTime': _recoveryTime.inSeconds,
+ };
+ }
+}
\ No newline at end of file
diff --git a/frontend/pshared/lib/service/authorization/retry_helper.dart b/frontend/pshared/lib/service/authorization/retry_helper.dart
new file mode 100644
index 0000000..6dcb71b
--- /dev/null
+++ b/frontend/pshared/lib/service/authorization/retry_helper.dart
@@ -0,0 +1,87 @@
+import 'dart:math';
+
+import 'package:logging/logging.dart';
+
+import 'package:pshared/utils/exception.dart';
+
+
+class RetryHelper {
+ static final _logger = Logger('auth.retry');
+
+ /// Executes an operation with exponential backoff retry logic
+ static Future withExponentialBackoff(
+ Future Function() operation, {
+ int maxRetries = 3,
+ Duration initialDelay = const Duration(milliseconds: 500),
+ double backoffMultiplier = 2.0,
+ Duration maxDelay = const Duration(seconds: 30),
+ bool Function(Exception)? shouldRetry,
+ }) async {
+ Exception? lastException;
+
+ // Total attempts = initial attempt + maxRetries
+ final totalAttempts = maxRetries + 1;
+
+ for (int attempt = 1; attempt <= totalAttempts; attempt++) {
+ try {
+ _logger.fine('Attempting operation (attempt $attempt/$totalAttempts)');
+ return await operation();
+ } catch (e) {
+ lastException = toException(e);
+
+ // Don't retry if we've reached max attempts
+ if (attempt == totalAttempts) {
+ _logger.warning('Operation failed after $totalAttempts attempts: $lastException');
+ rethrow;
+ }
+
+ // Check if we should retry this specific error
+ if (shouldRetry != null && !shouldRetry(lastException)) {
+ _logger.fine('Operation failed with non-retryable error: $lastException');
+ rethrow;
+ }
+
+ // Calculate delay with exponential backoff
+ final delayMs = min(
+ initialDelay.inMilliseconds * pow(backoffMultiplier, attempt - 1).toInt(),
+ maxDelay.inMilliseconds,
+ );
+ final delay = Duration(milliseconds: delayMs);
+
+ _logger.fine('Operation failed (attempt $attempt), retrying in ${delay.inMilliseconds}ms: $lastException');
+ await Future.delayed(delay);
+ }
+ }
+
+ // This should never be reached due to rethrow above, but just in case
+ throw lastException ?? Exception('Retry logic error');
+ }
+
+ /// Determines if an error is retryable (network/temporary errors)
+ static bool isRetryableError(Exception error) {
+ final errorString = error.toString().toLowerCase();
+
+ // Network connectivity issues
+ if (errorString.contains('socket') ||
+ errorString.contains('connection') ||
+ errorString.contains('timeout') ||
+ errorString.contains('network')) {
+ return true;
+ }
+
+ // Server temporary errors (5xx)
+ if (errorString.contains('500') ||
+ errorString.contains('502') ||
+ errorString.contains('503') ||
+ errorString.contains('504')) {
+ return true;
+ }
+
+ // Rate limiting
+ if (errorString.contains('429')) {
+ return true;
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/frontend/pshared/lib/service/authorization/service.dart b/frontend/pshared/lib/service/authorization/service.dart
index 79ba68a..a73a752 100644
--- a/frontend/pshared/lib/service/authorization/service.dart
+++ b/frontend/pshared/lib/service/authorization/service.dart
@@ -1,20 +1,52 @@
import 'package:logging/logging.dart';
-import 'package:pshared/api/errors/upload_failed.dart';
+import 'package:pshared/api/errors/authorization_failed.dart';
import 'package:pshared/api/requests/login.dart';
+import 'package:pshared/api/requests/login_data.dart';
import 'package:pshared/api/responses/account.dart';
import 'package:pshared/api/responses/login.dart';
-import 'package:pshared/config/constants.dart';
+import 'package:pshared/api/responses/login_pending.dart';
+import 'package:pshared/config/web.dart';
import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/models/account/account.dart';
+import 'package:pshared/models/auth/login_outcome.dart';
+import 'package:pshared/models/auth/pending_login.dart';
+import 'package:pshared/models/session_identifier.dart';
+import 'package:pshared/service/authorization/circuit_breaker.dart';
+import 'package:pshared/service/authorization/retry_helper.dart';
import 'package:pshared/service/authorization/storage.dart';
import 'package:pshared/service/authorization/token.dart';
import 'package:pshared/service/device_id.dart';
+import 'package:pshared/utils/exception.dart';
import 'package:pshared/utils/http/requests.dart' as httpr;
+/// AuthorizationService provides centralized authorization management
+/// with token refresh, retry logic, and circuit breaker patterns
class AuthorizationService {
- static final _logger = Logger('service.authorization');
+ static final _logger = Logger('service.authorization.auth_service');
+
+ static Future login(String service, LoginData login) async {
+ _logger.fine('Logging in ${login.login} with ${login.locale} locale');
+ final deviceId = await DeviceIdManager.getDeviceId();
+ final response = await httpr.getPOSTResponse(
+ service,
+ '/login',
+ LoginRequest(login: login, deviceId: deviceId, clientId: Constants.clientId).toJson(),
+ );
+
+ if (response.containsKey('refreshToken')) {
+ return LoginOutcome.completed((await completeLogin(response)).account.toDomain());
+ }
+ if (response.containsKey('pendingToken')) {
+ final pending = PendingLogin.fromResponse(
+ PendingLoginResponse.fromJson(response),
+ session: SessionIdentifier(clientId: Constants.clientId, deviceId: deviceId),
+ );
+ return LoginOutcome.pending(pending);
+ }
+ throw AuthenticationFailedException('Unexpected login response', Exception(response.toString()));
+ }
static Future _updateAccessToken(AccountResponse response) async {
await AuthorizationStorage.updateToken(response.accessToken);
@@ -31,59 +63,88 @@ class AuthorizationService {
return lr;
}
- static Future login(String service, String email, String password, String locale) async {
- _logger.fine('Logging in $email with $locale locale');
- final deviceId = await DeviceIdManager.getDeviceId();
- final response = await httpr.getPOSTResponse(
- service,
- '/login',
- LoginRequest(
- login: email.toLowerCase(),
- password: password,
- locale: locale,
- deviceId: deviceId,
- clientId: Constants.clientId,
- ).toJson());
-
- return (await _completeLogin(response)).account.toDomain();
- }
+ static Future completeLogin(Map response) => _completeLogin(response);
static Future restore() async {
- return (await TokenService.rotateRefreshToken()).account.toDomain();
+ return (await TokenService.refreshAccessToken()).account.toDomain();
}
static Future logout() async {
return AuthorizationStorage.removeTokens();
}
- static Future