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> _authenticatedRequest( - String service, - String url, - Future> Function(String, String, Map, {String? authToken}) requestType, - {Map? body}) async { - final accessToken = await TokenService.getAccessToken(); - return requestType(service, url, body ?? {}, authToken: accessToken); - } - - static Future> getPOSTResponse(String service, String url, Map body) async => _authenticatedRequest(service, url, httpr.getPOSTResponse, body: body); - + // Original AuthorizationService methods - keeping the interface unchanged static Future> getGETResponse(String service, String url) async { - final accessToken = await TokenService.getAccessToken(); - return httpr.getGETResponse(service, url, authToken: accessToken); + final token = await TokenService.getAccessTokenSafe(); + return httpr.getGETResponse(service, url, authToken: token); } - static Future> getPUTResponse(String service, String url, Map body) async => _authenticatedRequest(service, url, httpr.getPUTResponse, body: body); + static Future> getPOSTResponse(String service, String url, Map body) async { + final token = await TokenService.getAccessTokenSafe(); + return httpr.getPOSTResponse(service, url, body, authToken: token); + } - static Future> getPATCHResponse(String service, String url, Map body) async => _authenticatedRequest(service, url, httpr.getPATCHResponse, body: body); + static Future> getPUTResponse(String service, String url, Map body) async { + final token = await TokenService.getAccessTokenSafe(); + return httpr.getPUTResponse(service, url, body, authToken: token); + } - static Future> getDELETEResponse(String service, String url, Map body) async => _authenticatedRequest(service, url, httpr.getDELETEResponse, body: body); + static Future> getPATCHResponse(String service, String url, Map body) async { + final token = await TokenService.getAccessTokenSafe(); + return httpr.getPATCHResponse(service, url, body, authToken: token); + } + + static Future> getDELETEResponse(String service, String url, Map body) async { + final token = await TokenService.getAccessTokenSafe(); + return httpr.getDELETEResponse(service, url, body, authToken: token); + } static Future getFileUploadResponseAuth(String service, String url, String fileName, String fileType, String mediaType, List bytes) async { - final accessToken = await TokenService.getAccessToken(); - final res = await httpr.getFileUploadResponse(service, url, fileName, fileType, mediaType, bytes, authToken: accessToken); + final token = await TokenService.getAccessTokenSafe(); + final res = await httpr.getFileUploadResponse(service, url, fileName, fileType, mediaType, bytes, authToken: token); if (res == null) { - throw ErrorUploadFailed(); + throw Exception('Upload failed'); } return res.url; } + + static Future isAuthorizationStored() async => AuthorizationStorage.isAuthorizationStored(); + + /// Execute an operation with automatic token management and retry logic + static Future executeWithAuth( + Future Function() operation, + String description, { + int? maxRetries, + }) async => AuthCircuitBreaker.execute(() async => RetryHelper.withExponentialBackoff( + operation, + maxRetries: maxRetries ?? 3, + initialDelay: Duration(milliseconds: 100), + maxDelay: Duration(seconds: 5), + shouldRetry: (error) => RetryHelper.isRetryableError(error), + )); + + + /// Handle 401 unauthorized errors with automatic token recovery + static Future handleUnauthorized( + Future Function() operation, + String description, + ) async { + _logger.warning('Handling unauthorized error with token recovery: $description'); + + return executeWithAuth( + () async { + try { + // Attempt token recovery first + await TokenService.handleUnauthorized(); + + // Retry the original operation + return await operation(); + } catch (e) { + _logger.severe('Token recovery failed', e); + throw AuthenticationFailedException('Token recovery failed', toException(e)); + } + }, + 'unauthorized recovery: $description', + ); + } } diff --git a/frontend/pshared/lib/service/authorization/storage.dart b/frontend/pshared/lib/service/authorization/storage.dart index bb49431..a72cebd 100644 --- a/frontend/pshared/lib/service/authorization/storage.dart +++ b/frontend/pshared/lib/service/authorization/storage.dart @@ -20,6 +20,34 @@ class AuthorizationStorage { return TokenData.fromJson(jsonDecode(tokenJson)); } + + static Future _checkTokenUsable(String keyName) async { + final hasKey = await SecureStorageService.containsKey(keyName); + if (!hasKey) return false; + + try { + final tokenData = await _getTokenData(keyName); + return tokenData.expiration.isAfter(DateTime.now()); + } catch (e, st) { + _logger.warning('Error reading token from $keyName: $e', e, st); + rethrow; + } + } + + + static Future isAuthorizationStored() async { + _logger.fine('Checking if authorization is stored'); + + final accessUsable = await _checkTokenUsable(Constants.accessTokenStorageKey); + if (accessUsable) return true; + + final refreshUsable = await _checkTokenUsable(Constants.refreshTokenStorageKey); + if (refreshUsable) return true; + + return false; + } + + static Future getAccessToken() async { _logger.fine('Getting access token'); return _getTokenData(Constants.accessTokenStorageKey); diff --git a/frontend/pshared/lib/service/authorization/token.dart b/frontend/pshared/lib/service/authorization/token.dart index 0357e98..06ebb49 100644 --- a/frontend/pshared/lib/service/authorization/token.dart +++ b/frontend/pshared/lib/service/authorization/token.dart @@ -1,20 +1,25 @@ - - import 'package:logging/logging.dart'; +import 'package:pshared/api/errors/authorization_failed.dart'; import 'package:pshared/api/errors/unauthorized.dart'; import 'package:pshared/api/requests/tokens/access_refresh.dart'; import 'package:pshared/api/requests/tokens/refresh_rotate.dart'; import 'package:pshared/api/responses/account.dart'; +import 'package:pshared/api/responses/error/server.dart'; import 'package:pshared/api/responses/login.dart'; import 'package:pshared/api/responses/token.dart'; import 'package:pshared/config/constants.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_mutex.dart'; import 'package:pshared/service/device_id.dart'; import 'package:pshared/service/services.dart'; +import 'package:pshared/utils/exception.dart'; import 'package:pshared/utils/http/requests.dart'; + class TokenService { static final _logger = Logger('service.authorization.token'); static const String _objectType = Services.account; @@ -26,7 +31,11 @@ class TokenService { static Future getAccessToken() async { TokenData token = await AuthorizationStorage.getAccessToken(); if (_isTokenExpiringSoon(token, const Duration(hours: 4))) { - token = (await _refreshAccessToken()).accessToken; + // Use mutex to prevent concurrent refresh operations + final refreshedToken = await TokenRefreshMutex().executeRefresh(() async { + return (await refreshAccessToken()).accessToken.token; + }); + return refreshedToken; } return token.token; } @@ -36,13 +45,13 @@ class TokenService { await AuthorizationStorage.updateRefreshToken(response.refreshToken); } - static Future _refreshAccessToken() async { + static Future refreshAccessToken() async { _logger.fine('Refreshing access token...'); final deviceId = await DeviceIdManager.getDeviceId(); final refresh = await AuthorizationStorage.getRefreshToken(); if (_isTokenExpiringSoon(refresh, const Duration(days: 7))) { - return await rotateRefreshToken(); + return await _rotateRefreshToken(); } final response = await getPOSTResponse( @@ -60,7 +69,7 @@ class TokenService { return accountResp; } - static Future rotateRefreshToken() async { + static Future _rotateRefreshToken() async { _logger.fine('Rotating refresh token...'); final refresh = await AuthorizationStorage.getRefreshToken(); @@ -82,4 +91,89 @@ class TokenService { return loginResponse; } + /// Enhanced method to handle unexpected 401 errors with fallback logic + static Future handleUnauthorized() async { + _logger.warning('Handling unexpected 401 unauthorized error'); + + return AuthCircuitBreaker.execute(() async { + return RetryHelper.withExponentialBackoff( + () async { + try { + // Try refresh first (faster) + final currentRefresh = await AuthorizationStorage.getRefreshToken(); + if (!_isTokenExpiringSoon(currentRefresh, const Duration(days: 1))) { + _logger.fine('Attempting access token refresh for 401 recovery'); + await TokenRefreshMutex().executeRefresh(() async { + await refreshAccessToken(); + return 'refreshed'; + }); + return; + } + + // Fallback to rotation if refresh token expiring soon + _logger.fine('Attempting refresh token rotation for 401 recovery'); + await TokenRefreshMutex().executeRotation(() async { + await _rotateRefreshToken(); + }); + } catch (e) { + _logger.severe('Token recovery failed: $e'); + throw AuthenticationFailedException('Token recovery failed', toException(e)); + } + }, + maxRetries: 2, + shouldRetry: (error) { + // Only retry on network errors, not auth errors + return RetryHelper.isRetryableError(error) && !_isAuthError(error); + }, + ); + }); + } + + /// Enhanced getAccessToken with better error handling + static Future getAccessTokenSafe() async { + return AuthCircuitBreaker.execute(() async { + return RetryHelper.withExponentialBackoff( + () async { + TokenData token = await AuthorizationStorage.getAccessToken(); + if (_isTokenExpiringSoon(token, const Duration(hours: 4))) { + // Use mutex to prevent concurrent refresh operations + final refreshedToken = await TokenRefreshMutex().executeRefresh(() async { + return (await refreshAccessToken()).accessToken.token; + }); + return refreshedToken; + } + return token.token; + }, + maxRetries: 2, + shouldRetry: (error) => RetryHelper.isRetryableError(error), + ); + }); + } + + /// Check if error is authentication-related (non-retryable) + static bool _isAuthError(Exception error) { + if (error is ErrorUnauthorized || error is AuthenticationFailedException) { + return true; + } + + if (error is ErrorResponse && error.code == 401) { + return true; + } + + final errorString = error.toString().toLowerCase(); + return errorString.contains('unauthorized') || + errorString.contains('401') || + errorString.contains('authentication') || + errorString.contains('token'); + } + + /// Get circuit breaker status for debugging + static Map getAuthStatus() { + return { + 'circuitBreaker': AuthCircuitBreaker.getStatus(), + 'tokenMutex': TokenRefreshMutex().getStatus(), + 'timestamp': DateTime.now().toIso8601String(), + }; + } + } diff --git a/frontend/pshared/lib/service/authorization/token_mutex.dart b/frontend/pshared/lib/service/authorization/token_mutex.dart new file mode 100644 index 0000000..35ac296 --- /dev/null +++ b/frontend/pshared/lib/service/authorization/token_mutex.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:logging/logging.dart'; + +/// Mutex to prevent concurrent token refresh operations +/// This ensures only one refresh operation happens at a time, +/// preventing race conditions during app startup when multiple +/// providers try to refresh tokens simultaneously. +class TokenRefreshMutex { + static final _instance = TokenRefreshMutex._(); + factory TokenRefreshMutex() => _instance; + TokenRefreshMutex._(); + + static final _logger = Logger('service.authorization.token_mutex'); + + Completer? _currentRefresh; + Completer? _currentRotation; + + /// Execute a token refresh operation with mutex protection + /// If another refresh is in progress, wait for it to complete + Future executeRefresh(Future Function() refreshOperation) async { + if (_currentRefresh != null) { + _logger.fine('Token refresh already in progress, waiting for completion'); + return await _currentRefresh!.future; + } + + _logger.fine('Starting new token refresh operation'); + _currentRefresh = Completer(); + + try { + final result = await refreshOperation(); + if (_currentRefresh != null) { + _currentRefresh!.complete(result); + _logger.fine('Token refresh completed successfully'); + } + return result; + } catch (e, st) { + _logger.warning('Token refresh failed', e, st); + if (_currentRefresh != null) { + _currentRefresh!.completeError(e, st); + } + rethrow; + } finally { + _currentRefresh = null; + } + } + + /// Execute a token rotation operation with mutex protection + /// If another rotation is in progress, wait for it to complete + Future executeRotation(Future Function() rotationOperation) async { + if (_currentRotation != null) { + _logger.fine('Token rotation already in progress, waiting for completion'); + return await _currentRotation!.future; + } + + _logger.fine('Starting new token rotation operation'); + _currentRotation = Completer(); + + try { + await rotationOperation(); + if (_currentRotation != null) { + _currentRotation!.complete(); + _logger.fine('Token rotation completed successfully'); + } + } catch (e, st) { + _logger.warning('Token rotation failed', e, st); + if (_currentRotation != null) { + _currentRotation!.completeError(e, st); + } + rethrow; + } finally { + _currentRotation = null; + } + } + + /// Check if a refresh operation is currently in progress + bool get isRefreshInProgress => _currentRefresh != null; + + /// Check if a rotation operation is currently in progress + bool get isRotationInProgress => _currentRotation != null; + + /// Get current status for debugging + Map getStatus() { + return { + 'refreshInProgress': isRefreshInProgress, + 'rotationInProgress': isRotationInProgress, + 'timestamp': DateTime.now().toIso8601String(), + }; + } + + /// Force reset the mutex (for testing or emergency situations) + void forceReset() { + _logger.warning('Force resetting token refresh mutex'); + if (_currentRefresh != null && !_currentRefresh!.isCompleted) { + _currentRefresh!.completeError(Exception('Mutex force reset')); + } + if (_currentRotation != null && !_currentRotation!.isCompleted) { + _currentRotation!.completeError(Exception('Mutex force reset')); + } + _currentRefresh = null; + _currentRotation = null; + } +} \ No newline at end of file diff --git a/frontend/pshared/lib/service/secure_storage.dart b/frontend/pshared/lib/service/secure_storage.dart index fa0f9e0..90df91b 100644 --- a/frontend/pshared/lib/service/secure_storage.dart +++ b/frontend/pshared/lib/service/secure_storage.dart @@ -1,5 +1,6 @@ import 'package:shared_preferences/shared_preferences.dart'; + class SecureStorageService { static Future get(String key) async { final prefs = await SharedPreferences.getInstance(); @@ -18,6 +19,11 @@ class SecureStorageService { return _setImp(prefs, key, value); } + static Future containsKey(String key) async { + final prefs = await SharedPreferences.getInstance(); + return prefs.containsKey(key); + } + static Future delete(String key) async { final prefs = await SharedPreferences.getInstance(); await prefs.remove(key); diff --git a/frontend/pshared/lib/service/services.dart b/frontend/pshared/lib/service/services.dart index efeb745..f6c9564 100644 --- a/frontend/pshared/lib/service/services.dart +++ b/frontend/pshared/lib/service/services.dart @@ -6,27 +6,13 @@ class Services { static const String invitations = 'invitations'; static const String organization = 'organizations'; static const String permission = 'permissions'; - static const String project = 'projects'; - static const String pgroup = 'priority_groups'; - static const String priorities = 'priorities'; - static const String reactions = 'reactions'; static const String storage = 'storage'; - static const String taskStatus = 'statuses'; - static const String tasks = 'tasks'; static const String amplitude = 'amplitude'; - static const String automations = 'automation'; - static const String changes = 'changes'; static const String clients = 'clients'; - static const String invoices = 'invoices'; static const String logo = 'logo'; static const String notifications = 'notifications'; static const String policies = 'policies'; - static const String properties = 'properties'; static const String refreshTokens = 'refresh_tokens'; static const String roles = 'roles'; - static const String steps = 'steps'; - static const String teams = 'teams'; - static const String workflows = 'workflows'; - static const String workspaces = 'workspaces'; } diff --git a/frontend/pshared/lib/provider/exception.dart b/frontend/pshared/lib/utils/exception.dart similarity index 100% rename from frontend/pshared/lib/provider/exception.dart rename to frontend/pshared/lib/utils/exception.dart diff --git a/frontend/pweb/caddy/Caddyfile b/frontend/pweb/caddy/Caddyfile index 09e9a43..1df6dbd 100644 --- a/frontend/pweb/caddy/Caddyfile +++ b/frontend/pweb/caddy/Caddyfile @@ -1,13 +1,134 @@ +######################################## +# Global options +######################################## { email {$CADDY_ACME_EMAIL} + # debug } -{$SERVICE_HOST} { - root * /usr/share/pweb +######################################## +# Sendico site definition +######################################## +(sendico_site) { + vars static_root /usr/share/pweb + encode zstd gzip - try_files {path} /index.html - file_server + header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" } + + route { + ######################################## + # Backend API + ######################################## + handle /api/v1/* { + # Proxy directly to the BFF container on the Docker network. + reverse_proxy sendico-bff:8081 { + health_uri /api/v1/health + health_interval 15s + health_timeout 4s + health_status 2xx + } + header Cache-Control "no-cache, no-store, must-revalidate" + header Pragma "no-cache" + header Expires "0" + } + + ######################################## + # Static assets with tailored caching + ######################################## + handle /version.json { + root * {vars.static_root} + file_server + header Cache-Control "no-cache, no-store, must-revalidate" + header Pragma "no-cache" + header Expires "0" + } + + handle /*.main.dart.js { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=3600" + } + + handle /*.dart.js { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=3600" + } + + handle /*.js { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=3600" + } + + handle /assets/*.js { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=31536000, immutable" + } + + handle /assets/*.css { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=31536000, immutable" + } + + handle /assets/* { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=31536000, immutable" + } + + handle /canvaskit/* { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=86400" + } + + handle /icons/* { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=86400" + } + + handle /*.html { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=3600" + } + + handle /*.css { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=3600" + } + + handle /*.json { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=3600" + } + + ######################################## + # SPA fallback + ######################################## + handle { + root * {vars.static_root} + try_files {path} /index.html + file_server + header Cache-Control "no-cache, no-store, must-revalidate" + header Pragma "no-cache" + header Expires "0" + } + } +} + +######################################## +# Main site +######################################## +{$SERVICE_HOST} { + import sendico_site } diff --git a/frontend/pweb/lib/.gitignore b/frontend/pweb/lib/.gitignore new file mode 100644 index 0000000..86d4c2d --- /dev/null +++ b/frontend/pweb/lib/.gitignore @@ -0,0 +1 @@ +generated diff --git a/frontend/pweb/lib/generated/i18n/app_localizations.dart b/frontend/pweb/lib/generated/i18n/app_localizations.dart deleted file mode 100644 index 91889a2..0000000 --- a/frontend/pweb/lib/generated/i18n/app_localizations.dart +++ /dev/null @@ -1,1562 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:intl/intl.dart' as intl; - -import 'app_localizations_en.dart'; -import 'app_localizations_ru.dart'; - -// ignore_for_file: type=lint - -/// Callers can lookup localized strings with an instance of AppLocalizations -/// returned by `AppLocalizations.of(context)`. -/// -/// Applications need to include `AppLocalizations.delegate()` in their app's -/// `localizationDelegates` list, and the locales they support in the app's -/// `supportedLocales` list. For example: -/// -/// ```dart -/// import 'i18n/app_localizations.dart'; -/// -/// return MaterialApp( -/// localizationsDelegates: AppLocalizations.localizationsDelegates, -/// supportedLocales: AppLocalizations.supportedLocales, -/// home: MyApplicationHome(), -/// ); -/// ``` -/// -/// ## Update pubspec.yaml -/// -/// Please make sure to update your pubspec.yaml to include the following -/// packages: -/// -/// ```yaml -/// dependencies: -/// # Internationalization support. -/// flutter_localizations: -/// sdk: flutter -/// intl: any # Use the pinned version from flutter_localizations -/// -/// # Rest of dependencies -/// ``` -/// -/// ## iOS Applications -/// -/// iOS applications define key application metadata, including supported -/// locales, in an Info.plist file that is built into the application bundle. -/// To configure the locales supported by your app, you’ll need to edit this -/// file. -/// -/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. -/// Then, in the Project Navigator, open the Info.plist file under the Runner -/// project’s Runner folder. -/// -/// Next, select the Information Property List item, select Add Item from the -/// Editor menu, then select Localizations from the pop-up menu. -/// -/// Select and expand the newly-created Localizations item then, for each -/// locale your application supports, add a new item and select the locale -/// you wish to add from the pop-up menu in the Value field. This list should -/// be consistent with the languages listed in the AppLocalizations.supportedLocales -/// property. -abstract class AppLocalizations { - AppLocalizations(String locale) - : localeName = intl.Intl.canonicalizedLocale(locale.toString()); - - final String localeName; - - static AppLocalizations? of(BuildContext context) { - return Localizations.of(context, AppLocalizations); - } - - static const LocalizationsDelegate delegate = - _AppLocalizationsDelegate(); - - /// A list of this localizations delegate along with the default localizations - /// delegates. - /// - /// Returns a list of localizations delegates containing this delegate along with - /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, - /// and GlobalWidgetsLocalizations.delegate. - /// - /// Additional delegates can be added by appending to this list in - /// MaterialApp. This list does not have to be used at all if a custom list - /// of delegates is preferred or required. - static const List> localizationsDelegates = - >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; - - /// A list of this localizations delegate's supported locales. - static const List supportedLocales = [ - Locale('en'), - Locale('ru'), - ]; - - /// No description provided for @login. - /// - /// In en, this message translates to: - /// **'Login'** - String get login; - - /// No description provided for @logout. - /// - /// In en, this message translates to: - /// **'Logout'** - String get logout; - - /// No description provided for @profile. - /// - /// In en, this message translates to: - /// **'Profile'** - String get profile; - - /// No description provided for @signup. - /// - /// In en, this message translates to: - /// **'Sign up'** - String get signup; - - /// No description provided for @username. - /// - /// In en, this message translates to: - /// **'Email'** - String get username; - - /// No description provided for @usernameHint. - /// - /// In en, this message translates to: - /// **'email@example.com'** - String get usernameHint; - - /// No description provided for @usernameErrorInvalid. - /// - /// In en, this message translates to: - /// **'Provide a valid email address'** - String get usernameErrorInvalid; - - /// No description provided for @usernameUnknownTLD. - /// - /// In en, this message translates to: - /// **'Domain .{domain} is not known, please, check it'** - String usernameUnknownTLD(Object domain); - - /// No description provided for @password. - /// - /// In en, this message translates to: - /// **'Password'** - String get password; - - /// No description provided for @confirmPassword. - /// - /// In en, this message translates to: - /// **'Confirm password'** - String get confirmPassword; - - /// No description provided for @passwordValidationRuleDigit. - /// - /// In en, this message translates to: - /// **'has digit'** - String get passwordValidationRuleDigit; - - /// No description provided for @passwordValidationRuleUpperCase. - /// - /// In en, this message translates to: - /// **'has uppercase letter'** - String get passwordValidationRuleUpperCase; - - /// No description provided for @passwordValidationRuleLowerCase. - /// - /// In en, this message translates to: - /// **'has lowercase letter'** - String get passwordValidationRuleLowerCase; - - /// No description provided for @passwordValidationRuleSpecialCharacter. - /// - /// In en, this message translates to: - /// **'has special character letter'** - String get passwordValidationRuleSpecialCharacter; - - /// No description provided for @passwordValidationRuleMinCharacters. - /// - /// In en, this message translates to: - /// **'is {charNum} characters long at least'** - String passwordValidationRuleMinCharacters(Object charNum); - - /// No description provided for @passwordsDoNotMatch. - /// - /// In en, this message translates to: - /// **'Passwords do not match'** - String get passwordsDoNotMatch; - - /// No description provided for @passwordValidationError. - /// - /// In en, this message translates to: - /// **'Check that your password {matchesCriteria}'** - String passwordValidationError(Object matchesCriteria); - - /// No description provided for @notificationError. - /// - /// In en, this message translates to: - /// **'Error occurred: {error}'** - String notificationError(Object error); - - /// No description provided for @loginUserNotFound. - /// - /// In en, this message translates to: - /// **'Account {account} has not been registered in the system'** - String loginUserNotFound(Object account); - - /// No description provided for @loginPasswordIncorrect. - /// - /// In en, this message translates to: - /// **'Authorization failed, please check your password'** - String get loginPasswordIncorrect; - - /// No description provided for @internalErrorOccurred. - /// - /// In en, this message translates to: - /// **'An internal server error occurred: {error}, we already know about it and working hard to fix it'** - String internalErrorOccurred(Object error); - - /// No description provided for @noErrorInformation. - /// - /// In en, this message translates to: - /// **'Some error occurred, but we have not error information. We are already investigating the issue'** - String get noErrorInformation; - - /// No description provided for @yourName. - /// - /// In en, this message translates to: - /// **'Your name'** - String get yourName; - - /// No description provided for @nameHint. - /// - /// In en, this message translates to: - /// **'John Doe'** - String get nameHint; - - /// No description provided for @errorPageNotFoundTitle. - /// - /// In en, this message translates to: - /// **'Page Not Found'** - String get errorPageNotFoundTitle; - - /// No description provided for @errorPageNotFoundMessage. - /// - /// In en, this message translates to: - /// **'Oops! We couldn\'t find that page.'** - String get errorPageNotFoundMessage; - - /// No description provided for @errorPageNotFoundHint. - /// - /// In en, this message translates to: - /// **'The page you\'re looking for doesn\'t exist or has been moved. Please check the URL or return to the home page.'** - String get errorPageNotFoundHint; - - /// No description provided for @errorUnknown. - /// - /// In en, this message translates to: - /// **'Unknown error occurred'** - String get errorUnknown; - - /// No description provided for @unknown. - /// - /// In en, this message translates to: - /// **'unknown'** - String get unknown; - - /// No description provided for @goToLogin. - /// - /// In en, this message translates to: - /// **'Go to Login'** - String get goToLogin; - - /// No description provided for @goBack. - /// - /// In en, this message translates to: - /// **'Go Back'** - String get goBack; - - /// No description provided for @goToMainPage. - /// - /// In en, this message translates to: - /// **'Go to Main Page'** - String get goToMainPage; - - /// No description provided for @goToSignUp. - /// - /// In en, this message translates to: - /// **'Go to Sign Up'** - String get goToSignUp; - - /// No description provided for @signupError. - /// - /// In en, this message translates to: - /// **'Failed to signup: {error}'** - String signupError(Object error); - - /// No description provided for @signupSuccess. - /// - /// In en, this message translates to: - /// **'Email confirmation message has been sent to {email}. Please, open it and click link to activate your account.'** - String signupSuccess(Object email); - - /// No description provided for @connectivityError. - /// - /// In en, this message translates to: - /// **'Cannot reach the server at {serverAddress}. Check your network and try again.'** - String connectivityError(Object serverAddress); - - /// No description provided for @errorAccountExists. - /// - /// In en, this message translates to: - /// **'Account already exists'** - String get errorAccountExists; - - /// No description provided for @errorAccountNotVerified. - /// - /// In en, this message translates to: - /// **'Your account hasn\'t been verified yet. Please check your email to complete the verification'** - String get errorAccountNotVerified; - - /// No description provided for @errorLoginUnauthorized. - /// - /// In en, this message translates to: - /// **'Login or password is incorrect. Please try again'** - String get errorLoginUnauthorized; - - /// No description provided for @errorInternalError. - /// - /// In en, this message translates to: - /// **'An internal error occurred. We\'re aware of the issue and working to resolve it. Please try again later'** - String get errorInternalError; - - /// No description provided for @errorVerificationTokenNotFound. - /// - /// In en, this message translates to: - /// **'Account for verification not found. Sign up again'** - String get errorVerificationTokenNotFound; - - /// No description provided for @created. - /// - /// In en, this message translates to: - /// **'Created'** - String get created; - - /// No description provided for @edited. - /// - /// In en, this message translates to: - /// **'Edited'** - String get edited; - - /// No description provided for @errorDataConflict. - /// - /// In en, this message translates to: - /// **'We can’t process your data because it has conflicting or contradictory information.'** - String get errorDataConflict; - - /// No description provided for @errorAccessDenied. - /// - /// In en, this message translates to: - /// **'You do not have permission to access this resource. If you need access, please contact an administrator.'** - String get errorAccessDenied; - - /// No description provided for @errorBrokenPayload. - /// - /// In en, this message translates to: - /// **'The data you sent is invalid or incomplete. Please check your submission and try again.'** - String get errorBrokenPayload; - - /// No description provided for @errorInvalidArgument. - /// - /// In en, this message translates to: - /// **'One or more arguments are invalid. Verify your input and try again.'** - String get errorInvalidArgument; - - /// No description provided for @errorBrokenReference. - /// - /// In en, this message translates to: - /// **'The resource you\'re trying to access could not be referenced. It may have been moved or deleted.'** - String get errorBrokenReference; - - /// No description provided for @errorInvalidQueryParameter. - /// - /// In en, this message translates to: - /// **'One or more query parameters are missing or incorrect. Check them and try again.'** - String get errorInvalidQueryParameter; - - /// No description provided for @errorNotImplemented. - /// - /// In en, this message translates to: - /// **'This feature is not yet available. Please try again later or contact support.'** - String get errorNotImplemented; - - /// No description provided for @errorLicenseRequired. - /// - /// In en, this message translates to: - /// **'A valid license is required to perform this action. Please contact your administrator.'** - String get errorLicenseRequired; - - /// No description provided for @errorNotFound. - /// - /// In en, this message translates to: - /// **'We couldn\'t find the resource you requested. It may have been removed or is temporarily unavailable.'** - String get errorNotFound; - - /// No description provided for @errorNameMissing. - /// - /// In en, this message translates to: - /// **'Please provide a name before continuing.'** - String get errorNameMissing; - - /// No description provided for @errorEmailMissing. - /// - /// In en, this message translates to: - /// **'Please provide an email address before continuing.'** - String get errorEmailMissing; - - /// No description provided for @errorPasswordMissing. - /// - /// In en, this message translates to: - /// **'Please provide a password before continuing.'** - String get errorPasswordMissing; - - /// No description provided for @errorEmailNotRegistered. - /// - /// In en, this message translates to: - /// **'We could not find an account associated with that email address.'** - String get errorEmailNotRegistered; - - /// No description provided for @errorDuplicateEmail. - /// - /// In en, this message translates to: - /// **'This email address is already in use. Try another one or reset your password.'** - String get errorDuplicateEmail; - - /// No description provided for @showDetailsAction. - /// - /// In en, this message translates to: - /// **'Show Details'** - String get showDetailsAction; - - /// No description provided for @errorLogin. - /// - /// In en, this message translates to: - /// **'Error logging in'** - String get errorLogin; - - /// Error message displayed when invitation creation fails - /// - /// In en, this message translates to: - /// **'Failed to create invitaiton'** - String get errorCreatingInvitation; - - /// No description provided for @footerCompanyName. - /// - /// In en, this message translates to: - /// **'Sibilla Solutions LTD'** - String get footerCompanyName; - - /// No description provided for @footerAddress. - /// - /// In en, this message translates to: - /// **'27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus'** - String get footerAddress; - - /// No description provided for @footerSupport. - /// - /// In en, this message translates to: - /// **'Support'** - String get footerSupport; - - /// No description provided for @footerEmail. - /// - /// In en, this message translates to: - /// **'Email TBD'** - String get footerEmail; - - /// No description provided for @footerPhoneLabel. - /// - /// In en, this message translates to: - /// **'Phone'** - String get footerPhoneLabel; - - /// No description provided for @footerPhone. - /// - /// In en, this message translates to: - /// **'+357 22 000 253'** - String get footerPhone; - - /// No description provided for @footerTermsOfService. - /// - /// In en, this message translates to: - /// **'Terms of Service'** - String get footerTermsOfService; - - /// No description provided for @footerPrivacyPolicy. - /// - /// In en, this message translates to: - /// **'Privacy Policy'** - String get footerPrivacyPolicy; - - /// No description provided for @footerCookiePolicy. - /// - /// In en, this message translates to: - /// **'Cookie Policy'** - String get footerCookiePolicy; - - /// No description provided for @navigationLogout. - /// - /// In en, this message translates to: - /// **'Logout'** - String get navigationLogout; - - /// No description provided for @dashboard. - /// - /// In en, this message translates to: - /// **'Dashboard'** - String get dashboard; - - /// No description provided for @navigationUsersSettings. - /// - /// In en, this message translates to: - /// **'Users'** - String get navigationUsersSettings; - - /// No description provided for @navigationRolesSettings. - /// - /// In en, this message translates to: - /// **'Roles'** - String get navigationRolesSettings; - - /// No description provided for @navigationPermissionsSettings. - /// - /// In en, this message translates to: - /// **'Permissions'** - String get navigationPermissionsSettings; - - /// No description provided for @usersManagement. - /// - /// In en, this message translates to: - /// **'User Management'** - String get usersManagement; - - /// No description provided for @navigationOrganizationSettings. - /// - /// In en, this message translates to: - /// **'Organization settings'** - String get navigationOrganizationSettings; - - /// No description provided for @navigationAccountSettings. - /// - /// In en, this message translates to: - /// **'Profile settings'** - String get navigationAccountSettings; - - /// No description provided for @twoFactorPrompt. - /// - /// In en, this message translates to: - /// **'Enter the 6-digit code we sent to your device'** - String get twoFactorPrompt; - - /// No description provided for @twoFactorResend. - /// - /// In en, this message translates to: - /// **'Didn’t receive a code? Resend'** - String get twoFactorResend; - - /// No description provided for @twoFactorTitle. - /// - /// In en, this message translates to: - /// **'Two-Factor Authentication'** - String get twoFactorTitle; - - /// No description provided for @twoFactorError. - /// - /// In en, this message translates to: - /// **'Invalid code. Please try again.'** - String get twoFactorError; - - /// No description provided for @payoutNavDashboard. - /// - /// In en, this message translates to: - /// **'Dashboard'** - String get payoutNavDashboard; - - /// No description provided for @payoutNavSendPayout. - /// - /// In en, this message translates to: - /// **'Send payout'** - String get payoutNavSendPayout; - - /// No description provided for @payoutNavRecipients. - /// - /// In en, this message translates to: - /// **'Recipients'** - String get payoutNavRecipients; - - /// No description provided for @payoutNavReports. - /// - /// In en, this message translates to: - /// **'Reports'** - String get payoutNavReports; - - /// No description provided for @payoutNavSettings. - /// - /// In en, this message translates to: - /// **'Settings'** - String get payoutNavSettings; - - /// No description provided for @payoutNavLogout. - /// - /// In en, this message translates to: - /// **'Logout'** - String get payoutNavLogout; - - /// No description provided for @payoutNavMethods. - /// - /// In en, this message translates to: - /// **'Payouts'** - String get payoutNavMethods; - - /// No description provided for @expand. - /// - /// In en, this message translates to: - /// **'Expand'** - String get expand; - - /// No description provided for @collapse. - /// - /// In en, this message translates to: - /// **'Collapse'** - String get collapse; - - /// Title of the recipient address book page - /// - /// In en, this message translates to: - /// **'Recipient address book'** - String get pageTitleRecipients; - - /// Tooltip and button label to add a new recipient - /// - /// In en, this message translates to: - /// **'Add new'** - String get actionAddNew; - - /// Column header for who manages the payout data - /// - /// In en, this message translates to: - /// **'Data owner'** - String get colDataOwner; - - /// Column header for recipient avatar - /// - /// In en, this message translates to: - /// **'Avatar'** - String get colAvatar; - - /// Column header for recipient name - /// - /// In en, this message translates to: - /// **'Name'** - String get colName; - - /// Column header for recipient email address - /// - /// In en, this message translates to: - /// **'Email'** - String get colEmail; - - /// Column header for payout readiness status - /// - /// In en, this message translates to: - /// **'Status'** - String get colStatus; - - /// Status indicating payouts can be sent immediately - /// - /// In en, this message translates to: - /// **'Ready'** - String get statusReady; - - /// Status indicating recipient is registered but not yet fully ready - /// - /// In en, this message translates to: - /// **'Registered'** - String get statusRegistered; - - /// Status indicating recipient has not completed registration - /// - /// In en, this message translates to: - /// **'Not registered'** - String get statusNotRegistered; - - /// Label for recipients whose payout data is managed internally by the user/company - /// - /// In en, this message translates to: - /// **'Managed by me'** - String get typeInternal; - - /// Label for recipients who manage their own payout data - /// - /// In en, this message translates to: - /// **'Self‑managed'** - String get typeExternal; - - /// No description provided for @searchHint. - /// - /// In en, this message translates to: - /// **'Search recipients'** - String get searchHint; - - /// No description provided for @colActions. - /// - /// In en, this message translates to: - /// **'Actions'** - String get colActions; - - /// No description provided for @menuEdit. - /// - /// In en, this message translates to: - /// **'Edit'** - String get menuEdit; - - /// No description provided for @menuSendPayout. - /// - /// In en, this message translates to: - /// **'Send payout'** - String get menuSendPayout; - - /// No description provided for @tooltipRowActions. - /// - /// In en, this message translates to: - /// **'More actions'** - String get tooltipRowActions; - - /// No description provided for @accountSettings. - /// - /// In en, this message translates to: - /// **'Account Settings'** - String get accountSettings; - - /// No description provided for @accountNameUpdateError. - /// - /// In en, this message translates to: - /// **'Failed to update account name'** - String get accountNameUpdateError; - - /// No description provided for @settingsSuccessfullyUpdated. - /// - /// In en, this message translates to: - /// **'Settings successfully updated'** - String get settingsSuccessfullyUpdated; - - /// No description provided for @language. - /// - /// In en, this message translates to: - /// **'Language'** - String get language; - - /// No description provided for @failedToUpdateLanguage. - /// - /// In en, this message translates to: - /// **'Failed to update language'** - String get failedToUpdateLanguage; - - /// No description provided for @settingsImageUpdateError. - /// - /// In en, this message translates to: - /// **'Couldn\'t update the image'** - String get settingsImageUpdateError; - - /// No description provided for @settingsImageTitle. - /// - /// In en, this message translates to: - /// **'Image'** - String get settingsImageTitle; - - /// No description provided for @settingsImageHint. - /// - /// In en, this message translates to: - /// **'Tap to change the image'** - String get settingsImageHint; - - /// No description provided for @accountName. - /// - /// In en, this message translates to: - /// **'Name'** - String get accountName; - - /// No description provided for @accountNameHint. - /// - /// In en, this message translates to: - /// **'Specify your name'** - String get accountNameHint; - - /// No description provided for @avatar. - /// - /// In en, this message translates to: - /// **'Profile photo'** - String get avatar; - - /// No description provided for @avatarHint. - /// - /// In en, this message translates to: - /// **'Tap to update'** - String get avatarHint; - - /// No description provided for @avatarUpdateError. - /// - /// In en, this message translates to: - /// **'Failed to update profile photo'** - String get avatarUpdateError; - - /// No description provided for @settings. - /// - /// In en, this message translates to: - /// **'Settings'** - String get settings; - - /// No description provided for @notSet. - /// - /// In en, this message translates to: - /// **'not set'** - String get notSet; - - /// No description provided for @search. - /// - /// In en, this message translates to: - /// **'Search...'** - String get search; - - /// No description provided for @ok. - /// - /// In en, this message translates to: - /// **'Ok'** - String get ok; - - /// No description provided for @cancel. - /// - /// In en, this message translates to: - /// **'Cancel'** - String get cancel; - - /// No description provided for @confirm. - /// - /// In en, this message translates to: - /// **'Confirm'** - String get confirm; - - /// No description provided for @back. - /// - /// In en, this message translates to: - /// **'Back'** - String get back; - - /// Title of the operation history page - /// - /// In en, this message translates to: - /// **'Operation history'** - String get operationfryTitle; - - /// Label for the filters expansion panel - /// - /// In en, this message translates to: - /// **'Filters'** - String get filters; - - /// Label for the date‐range filter - /// - /// In en, this message translates to: - /// **'Period'** - String get period; - - /// Placeholder when no period is selected - /// - /// In en, this message translates to: - /// **'Select period'** - String get selectPeriod; - - /// Button text to apply the filters - /// - /// In en, this message translates to: - /// **'Apply'** - String get apply; - - /// Template for a single status filter chip - /// - /// In en, this message translates to: - /// **'{status}'** - String status(String status); - - /// Status indicating the operation succeeded - /// - /// In en, this message translates to: - /// **'Successful'** - String get operationStatusSuccessful; - - /// Status indicating the operation is pending - /// - /// In en, this message translates to: - /// **'Pending'** - String get operationStatusPending; - - /// Status indicating the operation failed - /// - /// In en, this message translates to: - /// **'Unsuccessful'** - String get operationStatusUnsuccessful; - - /// Table column header for status - /// - /// In en, this message translates to: - /// **'Status'** - String get statusColumn; - - /// Table column header for file name - /// - /// In en, this message translates to: - /// **'File name'** - String get fileNameColumn; - - /// Table column header for the original amount - /// - /// In en, this message translates to: - /// **'Amount'** - String get amountColumn; - - /// Table column header for the converted amount - /// - /// In en, this message translates to: - /// **'To amount'** - String get toAmountColumn; - - /// Table column header for the payment ID - /// - /// In en, this message translates to: - /// **'Pay ID'** - String get payIdColumn; - - /// Table column header for the masked card number - /// - /// In en, this message translates to: - /// **'Card number'** - String get cardNumberColumn; - - /// Table column header for recipient name - /// - /// In en, this message translates to: - /// **'Name'** - String get nameColumn; - - /// Table column header for the date/time - /// - /// In en, this message translates to: - /// **'Date'** - String get dateColumn; - - /// Table column header for any comment - /// - /// In en, this message translates to: - /// **'Comment'** - String get commentColumn; - - /// No description provided for @paymentConfigTitle. - /// - /// In en, this message translates to: - /// **'Where to receive money'** - String get paymentConfigTitle; - - /// No description provided for @paymentConfigSubtitle. - /// - /// In en, this message translates to: - /// **'Add multiple methods and choose your primary one.'** - String get paymentConfigSubtitle; - - /// No description provided for @addPaymentMethod. - /// - /// In en, this message translates to: - /// **'Add payment method'** - String get addPaymentMethod; - - /// No description provided for @makeMain. - /// - /// In en, this message translates to: - /// **'Make primary'** - String get makeMain; - - /// No description provided for @advanced. - /// - /// In en, this message translates to: - /// **'Advanced'** - String get advanced; - - /// No description provided for @fallbackExplanation. - /// - /// In en, this message translates to: - /// **'If the primary method is unavailable, we will try the next enabled one in the list.'** - String get fallbackExplanation; - - /// Button label to delete a payment method - /// - /// In en, this message translates to: - /// **'Delete'** - String get delete; - - /// Confirmation dialog message shown before a payment method is removed - /// - /// In en, this message translates to: - /// **'Are you sure you want to delete this payment method?'** - String get deletePaymentConfirmation; - - /// Button label to edit a payment method - /// - /// In en, this message translates to: - /// **'Edit'** - String get edit; - - /// Tooltip for an overflow menu button that reveals extra actions for a payment method - /// - /// In en, this message translates to: - /// **'More actions'** - String get moreActions; - - /// No description provided for @noPayouts. - /// - /// In en, this message translates to: - /// **'No Payouts'** - String get noPayouts; - - /// No description provided for @enterBankName. - /// - /// In en, this message translates to: - /// **'Enter bank name'** - String get enterBankName; - - /// No description provided for @paymentType. - /// - /// In en, this message translates to: - /// **'Payment Method Type'** - String get paymentType; - - /// No description provided for @selectPaymentType. - /// - /// In en, this message translates to: - /// **'Please select a payment method type'** - String get selectPaymentType; - - /// No description provided for @paymentTypeCard. - /// - /// In en, this message translates to: - /// **'Credit Card'** - String get paymentTypeCard; - - /// No description provided for @paymentTypeBankAccount. - /// - /// In en, this message translates to: - /// **'Russian Bank Account'** - String get paymentTypeBankAccount; - - /// No description provided for @paymentTypeIban. - /// - /// In en, this message translates to: - /// **'IBAN'** - String get paymentTypeIban; - - /// No description provided for @paymentTypeWallet. - /// - /// In en, this message translates to: - /// **'Wallet'** - String get paymentTypeWallet; - - /// No description provided for @cardNumber. - /// - /// In en, this message translates to: - /// **'Card Number'** - String get cardNumber; - - /// No description provided for @enterCardNumber. - /// - /// In en, this message translates to: - /// **'Enter the card number'** - String get enterCardNumber; - - /// No description provided for @cardholderName. - /// - /// In en, this message translates to: - /// **'Cardholder Name'** - String get cardholderName; - - /// No description provided for @iban. - /// - /// In en, this message translates to: - /// **'IBAN'** - String get iban; - - /// No description provided for @enterIban. - /// - /// In en, this message translates to: - /// **'Enter IBAN'** - String get enterIban; - - /// No description provided for @bic. - /// - /// In en, this message translates to: - /// **'BIC'** - String get bic; - - /// No description provided for @bankName. - /// - /// In en, this message translates to: - /// **'Bank Name'** - String get bankName; - - /// No description provided for @accountHolder. - /// - /// In en, this message translates to: - /// **'Account Holder'** - String get accountHolder; - - /// No description provided for @enterAccountHolder. - /// - /// In en, this message translates to: - /// **'Enter account holder'** - String get enterAccountHolder; - - /// No description provided for @enterBic. - /// - /// In en, this message translates to: - /// **'Enter BIC'** - String get enterBic; - - /// No description provided for @walletId. - /// - /// In en, this message translates to: - /// **'Wallet ID'** - String get walletId; - - /// No description provided for @enterWalletId. - /// - /// In en, this message translates to: - /// **'Enter wallet ID'** - String get enterWalletId; - - /// No description provided for @recipients. - /// - /// In en, this message translates to: - /// **'Recipients'** - String get recipients; - - /// No description provided for @recipientName. - /// - /// In en, this message translates to: - /// **'Recipient Name'** - String get recipientName; - - /// No description provided for @enterRecipientName. - /// - /// In en, this message translates to: - /// **'Enter recipient name'** - String get enterRecipientName; - - /// No description provided for @inn. - /// - /// In en, this message translates to: - /// **'INN'** - String get inn; - - /// No description provided for @enterInn. - /// - /// In en, this message translates to: - /// **'Enter INN'** - String get enterInn; - - /// No description provided for @kpp. - /// - /// In en, this message translates to: - /// **'KPP'** - String get kpp; - - /// No description provided for @enterKpp. - /// - /// In en, this message translates to: - /// **'Enter KPP'** - String get enterKpp; - - /// No description provided for @accountNumber. - /// - /// In en, this message translates to: - /// **'Account Number'** - String get accountNumber; - - /// No description provided for @enterAccountNumber. - /// - /// In en, this message translates to: - /// **'Enter account number'** - String get enterAccountNumber; - - /// No description provided for @correspondentAccount. - /// - /// In en, this message translates to: - /// **'Correspondent Account'** - String get correspondentAccount; - - /// No description provided for @enterCorrespondentAccount. - /// - /// In en, this message translates to: - /// **'Enter correspondent account'** - String get enterCorrespondentAccount; - - /// No description provided for @bik. - /// - /// In en, this message translates to: - /// **'BIK'** - String get bik; - - /// No description provided for @enterBik. - /// - /// In en, this message translates to: - /// **'Enter BIK'** - String get enterBik; - - /// No description provided for @add. - /// - /// In en, this message translates to: - /// **'Add'** - String get add; - - /// No description provided for @expiryDate. - /// - /// In en, this message translates to: - /// **'Expiry (MM/YY)'** - String get expiryDate; - - /// No description provided for @firstName. - /// - /// In en, this message translates to: - /// **'First Name'** - String get firstName; - - /// No description provided for @enterFirstName. - /// - /// In en, this message translates to: - /// **'Enter First Name'** - String get enterFirstName; - - /// No description provided for @lastName. - /// - /// In en, this message translates to: - /// **'Last Name'** - String get lastName; - - /// No description provided for @enterLastName. - /// - /// In en, this message translates to: - /// **'Enter Last Name'** - String get enterLastName; - - /// No description provided for @sendSingle. - /// - /// In en, this message translates to: - /// **'Send single transaction'** - String get sendSingle; - - /// No description provided for @sendMultiple. - /// - /// In en, this message translates to: - /// **'Send multiple transactions'** - String get sendMultiple; - - /// No description provided for @addFunds. - /// - /// In en, this message translates to: - /// **'Add Funds'** - String get addFunds; - - /// No description provided for @close. - /// - /// In en, this message translates to: - /// **'Close'** - String get close; - - /// No description provided for @multiplePayout. - /// - /// In en, this message translates to: - /// **'Multiple Payout'** - String get multiplePayout; - - /// No description provided for @howItWorks. - /// - /// In en, this message translates to: - /// **'How it works?'** - String get howItWorks; - - /// No description provided for @exampleTitle. - /// - /// In en, this message translates to: - /// **'File Format & Sample'** - String get exampleTitle; - - /// No description provided for @downloadSampleCSV. - /// - /// In en, this message translates to: - /// **'Download sample.csv'** - String get downloadSampleCSV; - - /// No description provided for @tokenColumn. - /// - /// In en, this message translates to: - /// **'Token (required)'** - String get tokenColumn; - - /// No description provided for @currency. - /// - /// In en, this message translates to: - /// **'Currency'** - String get currency; - - /// No description provided for @amount. - /// - /// In en, this message translates to: - /// **'Amount'** - String get amount; - - /// No description provided for @comment. - /// - /// In en, this message translates to: - /// **'Comment'** - String get comment; - - /// No description provided for @uploadCSV. - /// - /// In en, this message translates to: - /// **'Upload your CSV'** - String get uploadCSV; - - /// No description provided for @upload. - /// - /// In en, this message translates to: - /// **'Upload'** - String get upload; - - /// No description provided for @hintUpload. - /// - /// In en, this message translates to: - /// **'Supported format: .CSV · Max size 1 MB'** - String get hintUpload; - - /// No description provided for @uploadHistory. - /// - /// In en, this message translates to: - /// **'Upload History'** - String get uploadHistory; - - /// No description provided for @payout. - /// - /// In en, this message translates to: - /// **'Payout'** - String get payout; - - /// No description provided for @sendTo. - /// - /// In en, this message translates to: - /// **'Send Payout To'** - String get sendTo; - - /// No description provided for @send. - /// - /// In en, this message translates to: - /// **'Send Payout'** - String get send; - - /// No description provided for @recipientPaysFee. - /// - /// In en, this message translates to: - /// **'Recipient pays the fee'** - String get recipientPaysFee; - - /// Label showing the amount sent - /// - /// In en, this message translates to: - /// **'Sent amount: \${amount}'** - String sentAmount(String amount); - - /// Label showing the transaction fee - /// - /// In en, this message translates to: - /// **'Fee: \${fee}'** - String fee(String fee); - - /// Label showing how much the recipient will receive - /// - /// In en, this message translates to: - /// **'Recipient will receive: \${amount}'** - String recipientWillReceive(String amount); - - /// Label showing the total amount of the transaction - /// - /// In en, this message translates to: - /// **'Total: \${total}'** - String total(String total); - - /// No description provided for @hideDetails. - /// - /// In en, this message translates to: - /// **'Hide Details'** - String get hideDetails; - - /// No description provided for @showDetails. - /// - /// In en, this message translates to: - /// **'Show Details'** - String get showDetails; - - /// No description provided for @whereGetMoney. - /// - /// In en, this message translates to: - /// **'Source of funds for debit'** - String get whereGetMoney; - - /// No description provided for @details. - /// - /// In en, this message translates to: - /// **'Details'** - String get details; - - /// No description provided for @addRecipient. - /// - /// In en, this message translates to: - /// **'Add Recipient'** - String get addRecipient; - - /// No description provided for @editRecipient. - /// - /// In en, this message translates to: - /// **'Edit Recipient'** - String get editRecipient; - - /// No description provided for @saveRecipient. - /// - /// In en, this message translates to: - /// **'Save Recipient'** - String get saveRecipient; - - /// No description provided for @choosePaymentMethod. - /// - /// In en, this message translates to: - /// **'Payment Methods (choose at least 1)'** - String get choosePaymentMethod; - - /// No description provided for @recipientFormRule. - /// - /// In en, this message translates to: - /// **'Recipient must have at least one payment method'** - String get recipientFormRule; - - /// No description provided for @allStatus. - /// - /// In en, this message translates to: - /// **'All'** - String get allStatus; - - /// No description provided for @readyStatus. - /// - /// In en, this message translates to: - /// **'Ready'** - String get readyStatus; - - /// No description provided for @registeredStatus. - /// - /// In en, this message translates to: - /// **'Registered'** - String get registeredStatus; - - /// No description provided for @notRegisteredStatus. - /// - /// In en, this message translates to: - /// **'Not registered'** - String get notRegisteredStatus; - - /// No description provided for @noRecipientSelected. - /// - /// In en, this message translates to: - /// **'No recipient selected'** - String get noRecipientSelected; - - /// No description provided for @companyName. - /// - /// In en, this message translates to: - /// **'Name of your company'** - String get companyName; - - /// No description provided for @companynameRequired. - /// - /// In en, this message translates to: - /// **'Company name required'** - String get companynameRequired; - - /// No description provided for @errorSignUp. - /// - /// In en, this message translates to: - /// **'Error occured while signing up, try again later'** - String get errorSignUp; - - /// No description provided for @companyDescription. - /// - /// In en, this message translates to: - /// **'Company Description'** - String get companyDescription; - - /// No description provided for @companyDescriptionHint. - /// - /// In en, this message translates to: - /// **'Describe any of the fields of the Company\'s business'** - String get companyDescriptionHint; - - /// No description provided for @optional. - /// - /// In en, this message translates to: - /// **'optional'** - String get optional; -} - -class _AppLocalizationsDelegate - extends LocalizationsDelegate { - const _AppLocalizationsDelegate(); - - @override - Future load(Locale locale) { - return SynchronousFuture(lookupAppLocalizations(locale)); - } - - @override - bool isSupported(Locale locale) => - ['en', 'ru'].contains(locale.languageCode); - - @override - bool shouldReload(_AppLocalizationsDelegate old) => false; -} - -AppLocalizations lookupAppLocalizations(Locale locale) { - // Lookup logic when only language code is specified. - switch (locale.languageCode) { - case 'en': - return AppLocalizationsEn(); - case 'ru': - return AppLocalizationsRu(); - } - - throw FlutterError( - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.', - ); -} diff --git a/frontend/pweb/lib/generated/i18n/app_localizations_en.dart b/frontend/pweb/lib/generated/i18n/app_localizations_en.dart deleted file mode 100644 index ddcad3b..0000000 --- a/frontend/pweb/lib/generated/i18n/app_localizations_en.dart +++ /dev/null @@ -1,779 +0,0 @@ -// ignore: unused_import -import 'package:intl/intl.dart' as intl; -import 'app_localizations.dart'; - -// ignore_for_file: type=lint - -/// The translations for English (`en`). -class AppLocalizationsEn extends AppLocalizations { - AppLocalizationsEn([String locale = 'en']) : super(locale); - - @override - String get login => 'Login'; - - @override - String get logout => 'Logout'; - - @override - String get profile => 'Profile'; - - @override - String get signup => 'Sign up'; - - @override - String get username => 'Email'; - - @override - String get usernameHint => 'email@example.com'; - - @override - String get usernameErrorInvalid => 'Provide a valid email address'; - - @override - String usernameUnknownTLD(Object domain) { - return 'Domain .$domain is not known, please, check it'; - } - - @override - String get password => 'Password'; - - @override - String get confirmPassword => 'Confirm password'; - - @override - String get passwordValidationRuleDigit => 'has digit'; - - @override - String get passwordValidationRuleUpperCase => 'has uppercase letter'; - - @override - String get passwordValidationRuleLowerCase => 'has lowercase letter'; - - @override - String get passwordValidationRuleSpecialCharacter => - 'has special character letter'; - - @override - String passwordValidationRuleMinCharacters(Object charNum) { - return 'is $charNum characters long at least'; - } - - @override - String get passwordsDoNotMatch => 'Passwords do not match'; - - @override - String passwordValidationError(Object matchesCriteria) { - return 'Check that your password $matchesCriteria'; - } - - @override - String notificationError(Object error) { - return 'Error occurred: $error'; - } - - @override - String loginUserNotFound(Object account) { - return 'Account $account has not been registered in the system'; - } - - @override - String get loginPasswordIncorrect => - 'Authorization failed, please check your password'; - - @override - String internalErrorOccurred(Object error) { - return 'An internal server error occurred: $error, we already know about it and working hard to fix it'; - } - - @override - String get noErrorInformation => - 'Some error occurred, but we have not error information. We are already investigating the issue'; - - @override - String get yourName => 'Your name'; - - @override - String get nameHint => 'John Doe'; - - @override - String get errorPageNotFoundTitle => 'Page Not Found'; - - @override - String get errorPageNotFoundMessage => 'Oops! We couldn\'t find that page.'; - - @override - String get errorPageNotFoundHint => - 'The page you\'re looking for doesn\'t exist or has been moved. Please check the URL or return to the home page.'; - - @override - String get errorUnknown => 'Unknown error occurred'; - - @override - String get unknown => 'unknown'; - - @override - String get goToLogin => 'Go to Login'; - - @override - String get goBack => 'Go Back'; - - @override - String get goToMainPage => 'Go to Main Page'; - - @override - String get goToSignUp => 'Go to Sign Up'; - - @override - String signupError(Object error) { - return 'Failed to signup: $error'; - } - - @override - String signupSuccess(Object email) { - return 'Email confirmation message has been sent to $email. Please, open it and click link to activate your account.'; - } - - @override - String connectivityError(Object serverAddress) { - return 'Cannot reach the server at $serverAddress. Check your network and try again.'; - } - - @override - String get errorAccountExists => 'Account already exists'; - - @override - String get errorAccountNotVerified => - 'Your account hasn\'t been verified yet. Please check your email to complete the verification'; - - @override - String get errorLoginUnauthorized => - 'Login or password is incorrect. Please try again'; - - @override - String get errorInternalError => - 'An internal error occurred. We\'re aware of the issue and working to resolve it. Please try again later'; - - @override - String get errorVerificationTokenNotFound => - 'Account for verification not found. Sign up again'; - - @override - String get created => 'Created'; - - @override - String get edited => 'Edited'; - - @override - String get errorDataConflict => - 'We can’t process your data because it has conflicting or contradictory information.'; - - @override - String get errorAccessDenied => - 'You do not have permission to access this resource. If you need access, please contact an administrator.'; - - @override - String get errorBrokenPayload => - 'The data you sent is invalid or incomplete. Please check your submission and try again.'; - - @override - String get errorInvalidArgument => - 'One or more arguments are invalid. Verify your input and try again.'; - - @override - String get errorBrokenReference => - 'The resource you\'re trying to access could not be referenced. It may have been moved or deleted.'; - - @override - String get errorInvalidQueryParameter => - 'One or more query parameters are missing or incorrect. Check them and try again.'; - - @override - String get errorNotImplemented => - 'This feature is not yet available. Please try again later or contact support.'; - - @override - String get errorLicenseRequired => - 'A valid license is required to perform this action. Please contact your administrator.'; - - @override - String get errorNotFound => - 'We couldn\'t find the resource you requested. It may have been removed or is temporarily unavailable.'; - - @override - String get errorNameMissing => 'Please provide a name before continuing.'; - - @override - String get errorEmailMissing => - 'Please provide an email address before continuing.'; - - @override - String get errorPasswordMissing => - 'Please provide a password before continuing.'; - - @override - String get errorEmailNotRegistered => - 'We could not find an account associated with that email address.'; - - @override - String get errorDuplicateEmail => - 'This email address is already in use. Try another one or reset your password.'; - - @override - String get showDetailsAction => 'Show Details'; - - @override - String get errorLogin => 'Error logging in'; - - @override - String get errorCreatingInvitation => 'Failed to create invitaiton'; - - @override - String get footerCompanyName => 'Sibilla Solutions LTD'; - - @override - String get footerAddress => - '27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus'; - - @override - String get footerSupport => 'Support'; - - @override - String get footerEmail => 'Email TBD'; - - @override - String get footerPhoneLabel => 'Phone'; - - @override - String get footerPhone => '+357 22 000 253'; - - @override - String get footerTermsOfService => 'Terms of Service'; - - @override - String get footerPrivacyPolicy => 'Privacy Policy'; - - @override - String get footerCookiePolicy => 'Cookie Policy'; - - @override - String get navigationLogout => 'Logout'; - - @override - String get dashboard => 'Dashboard'; - - @override - String get navigationUsersSettings => 'Users'; - - @override - String get navigationRolesSettings => 'Roles'; - - @override - String get navigationPermissionsSettings => 'Permissions'; - - @override - String get usersManagement => 'User Management'; - - @override - String get navigationOrganizationSettings => 'Organization settings'; - - @override - String get navigationAccountSettings => 'Profile settings'; - - @override - String get twoFactorPrompt => 'Enter the 6-digit code we sent to your device'; - - @override - String get twoFactorResend => 'Didn’t receive a code? Resend'; - - @override - String get twoFactorTitle => 'Two-Factor Authentication'; - - @override - String get twoFactorError => 'Invalid code. Please try again.'; - - @override - String get payoutNavDashboard => 'Dashboard'; - - @override - String get payoutNavSendPayout => 'Send payout'; - - @override - String get payoutNavRecipients => 'Recipients'; - - @override - String get payoutNavReports => 'Reports'; - - @override - String get payoutNavSettings => 'Settings'; - - @override - String get payoutNavLogout => 'Logout'; - - @override - String get payoutNavMethods => 'Payouts'; - - @override - String get expand => 'Expand'; - - @override - String get collapse => 'Collapse'; - - @override - String get pageTitleRecipients => 'Recipient address book'; - - @override - String get actionAddNew => 'Add new'; - - @override - String get colDataOwner => 'Data owner'; - - @override - String get colAvatar => 'Avatar'; - - @override - String get colName => 'Name'; - - @override - String get colEmail => 'Email'; - - @override - String get colStatus => 'Status'; - - @override - String get statusReady => 'Ready'; - - @override - String get statusRegistered => 'Registered'; - - @override - String get statusNotRegistered => 'Not registered'; - - @override - String get typeInternal => 'Managed by me'; - - @override - String get typeExternal => 'Self‑managed'; - - @override - String get searchHint => 'Search recipients'; - - @override - String get colActions => 'Actions'; - - @override - String get menuEdit => 'Edit'; - - @override - String get menuSendPayout => 'Send payout'; - - @override - String get tooltipRowActions => 'More actions'; - - @override - String get accountSettings => 'Account Settings'; - - @override - String get accountNameUpdateError => 'Failed to update account name'; - - @override - String get settingsSuccessfullyUpdated => 'Settings successfully updated'; - - @override - String get language => 'Language'; - - @override - String get failedToUpdateLanguage => 'Failed to update language'; - - @override - String get settingsImageUpdateError => 'Couldn\'t update the image'; - - @override - String get settingsImageTitle => 'Image'; - - @override - String get settingsImageHint => 'Tap to change the image'; - - @override - String get accountName => 'Name'; - - @override - String get accountNameHint => 'Specify your name'; - - @override - String get avatar => 'Profile photo'; - - @override - String get avatarHint => 'Tap to update'; - - @override - String get avatarUpdateError => 'Failed to update profile photo'; - - @override - String get settings => 'Settings'; - - @override - String get notSet => 'not set'; - - @override - String get search => 'Search...'; - - @override - String get ok => 'Ok'; - - @override - String get cancel => 'Cancel'; - - @override - String get confirm => 'Confirm'; - - @override - String get back => 'Back'; - - @override - String get operationfryTitle => 'Operation history'; - - @override - String get filters => 'Filters'; - - @override - String get period => 'Period'; - - @override - String get selectPeriod => 'Select period'; - - @override - String get apply => 'Apply'; - - @override - String status(String status) { - return '$status'; - } - - @override - String get operationStatusSuccessful => 'Successful'; - - @override - String get operationStatusPending => 'Pending'; - - @override - String get operationStatusUnsuccessful => 'Unsuccessful'; - - @override - String get statusColumn => 'Status'; - - @override - String get fileNameColumn => 'File name'; - - @override - String get amountColumn => 'Amount'; - - @override - String get toAmountColumn => 'To amount'; - - @override - String get payIdColumn => 'Pay ID'; - - @override - String get cardNumberColumn => 'Card number'; - - @override - String get nameColumn => 'Name'; - - @override - String get dateColumn => 'Date'; - - @override - String get commentColumn => 'Comment'; - - @override - String get paymentConfigTitle => 'Where to receive money'; - - @override - String get paymentConfigSubtitle => - 'Add multiple methods and choose your primary one.'; - - @override - String get addPaymentMethod => 'Add payment method'; - - @override - String get makeMain => 'Make primary'; - - @override - String get advanced => 'Advanced'; - - @override - String get fallbackExplanation => - 'If the primary method is unavailable, we will try the next enabled one in the list.'; - - @override - String get delete => 'Delete'; - - @override - String get deletePaymentConfirmation => - 'Are you sure you want to delete this payment method?'; - - @override - String get edit => 'Edit'; - - @override - String get moreActions => 'More actions'; - - @override - String get noPayouts => 'No Payouts'; - - @override - String get enterBankName => 'Enter bank name'; - - @override - String get paymentType => 'Payment Method Type'; - - @override - String get selectPaymentType => 'Please select a payment method type'; - - @override - String get paymentTypeCard => 'Credit Card'; - - @override - String get paymentTypeBankAccount => 'Russian Bank Account'; - - @override - String get paymentTypeIban => 'IBAN'; - - @override - String get paymentTypeWallet => 'Wallet'; - - @override - String get cardNumber => 'Card Number'; - - @override - String get enterCardNumber => 'Enter the card number'; - - @override - String get cardholderName => 'Cardholder Name'; - - @override - String get iban => 'IBAN'; - - @override - String get enterIban => 'Enter IBAN'; - - @override - String get bic => 'BIC'; - - @override - String get bankName => 'Bank Name'; - - @override - String get accountHolder => 'Account Holder'; - - @override - String get enterAccountHolder => 'Enter account holder'; - - @override - String get enterBic => 'Enter BIC'; - - @override - String get walletId => 'Wallet ID'; - - @override - String get enterWalletId => 'Enter wallet ID'; - - @override - String get recipients => 'Recipients'; - - @override - String get recipientName => 'Recipient Name'; - - @override - String get enterRecipientName => 'Enter recipient name'; - - @override - String get inn => 'INN'; - - @override - String get enterInn => 'Enter INN'; - - @override - String get kpp => 'KPP'; - - @override - String get enterKpp => 'Enter KPP'; - - @override - String get accountNumber => 'Account Number'; - - @override - String get enterAccountNumber => 'Enter account number'; - - @override - String get correspondentAccount => 'Correspondent Account'; - - @override - String get enterCorrespondentAccount => 'Enter correspondent account'; - - @override - String get bik => 'BIK'; - - @override - String get enterBik => 'Enter BIK'; - - @override - String get add => 'Add'; - - @override - String get expiryDate => 'Expiry (MM/YY)'; - - @override - String get firstName => 'First Name'; - - @override - String get enterFirstName => 'Enter First Name'; - - @override - String get lastName => 'Last Name'; - - @override - String get enterLastName => 'Enter Last Name'; - - @override - String get sendSingle => 'Send single transaction'; - - @override - String get sendMultiple => 'Send multiple transactions'; - - @override - String get addFunds => 'Add Funds'; - - @override - String get close => 'Close'; - - @override - String get multiplePayout => 'Multiple Payout'; - - @override - String get howItWorks => 'How it works?'; - - @override - String get exampleTitle => 'File Format & Sample'; - - @override - String get downloadSampleCSV => 'Download sample.csv'; - - @override - String get tokenColumn => 'Token (required)'; - - @override - String get currency => 'Currency'; - - @override - String get amount => 'Amount'; - - @override - String get comment => 'Comment'; - - @override - String get uploadCSV => 'Upload your CSV'; - - @override - String get upload => 'Upload'; - - @override - String get hintUpload => 'Supported format: .CSV · Max size 1 MB'; - - @override - String get uploadHistory => 'Upload History'; - - @override - String get payout => 'Payout'; - - @override - String get sendTo => 'Send Payout To'; - - @override - String get send => 'Send Payout'; - - @override - String get recipientPaysFee => 'Recipient pays the fee'; - - @override - String sentAmount(String amount) { - return 'Sent amount: \$$amount'; - } - - @override - String fee(String fee) { - return 'Fee: \$$fee'; - } - - @override - String recipientWillReceive(String amount) { - return 'Recipient will receive: \$$amount'; - } - - @override - String total(String total) { - return 'Total: \$$total'; - } - - @override - String get hideDetails => 'Hide Details'; - - @override - String get showDetails => 'Show Details'; - - @override - String get whereGetMoney => 'Source of funds for debit'; - - @override - String get details => 'Details'; - - @override - String get addRecipient => 'Add Recipient'; - - @override - String get editRecipient => 'Edit Recipient'; - - @override - String get saveRecipient => 'Save Recipient'; - - @override - String get choosePaymentMethod => 'Payment Methods (choose at least 1)'; - - @override - String get recipientFormRule => - 'Recipient must have at least one payment method'; - - @override - String get allStatus => 'All'; - - @override - String get readyStatus => 'Ready'; - - @override - String get registeredStatus => 'Registered'; - - @override - String get notRegisteredStatus => 'Not registered'; - - @override - String get noRecipientSelected => 'No recipient selected'; - - @override - String get companyName => 'Name of your company'; - - @override - String get companynameRequired => 'Company name required'; - - @override - String get errorSignUp => 'Error occured while signing up, try again later'; - - @override - String get companyDescription => 'Company Description'; - - @override - String get companyDescriptionHint => - 'Describe any of the fields of the Company\'s business'; - - @override - String get optional => 'optional'; -} diff --git a/frontend/pweb/lib/generated/i18n/app_localizations_ru.dart b/frontend/pweb/lib/generated/i18n/app_localizations_ru.dart deleted file mode 100644 index caf30fe..0000000 --- a/frontend/pweb/lib/generated/i18n/app_localizations_ru.dart +++ /dev/null @@ -1,782 +0,0 @@ -// ignore: unused_import -import 'package:intl/intl.dart' as intl; -import 'app_localizations.dart'; - -// ignore_for_file: type=lint - -/// The translations for Russian (`ru`). -class AppLocalizationsRu extends AppLocalizations { - AppLocalizationsRu([String locale = 'ru']) : super(locale); - - @override - String get login => 'Войти'; - - @override - String get logout => 'Выйти'; - - @override - String get profile => 'Профиль'; - - @override - String get signup => 'Регистрация'; - - @override - String get username => 'Email'; - - @override - String get usernameHint => 'email@example.com'; - - @override - String get usernameErrorInvalid => - 'Укажите действительный адрес электронной почты'; - - @override - String usernameUnknownTLD(Object domain) { - return 'Домен .$domain неизвестен, пожалуйста, проверьте его'; - } - - @override - String get password => 'Пароль'; - - @override - String get confirmPassword => 'Подтвердите пароль'; - - @override - String get passwordValidationRuleDigit => 'содержит цифру'; - - @override - String get passwordValidationRuleUpperCase => 'содержит заглавную букву'; - - @override - String get passwordValidationRuleLowerCase => 'содержит строчную букву'; - - @override - String get passwordValidationRuleSpecialCharacter => - 'содержит специальный символ'; - - @override - String passwordValidationRuleMinCharacters(Object charNum) { - return 'длина не менее $charNum символов'; - } - - @override - String get passwordsDoNotMatch => 'Пароли не совпадают'; - - @override - String passwordValidationError(Object matchesCriteria) { - return 'Убедитесь, что ваш пароль $matchesCriteria'; - } - - @override - String notificationError(Object error) { - return 'Произошла ошибка: $error'; - } - - @override - String loginUserNotFound(Object account) { - return 'Аккаунт $account не зарегистрирован в системе'; - } - - @override - String get loginPasswordIncorrect => - 'Ошибка авторизации, пожалуйста, проверьте пароль'; - - @override - String internalErrorOccurred(Object error) { - return 'Произошла внутренняя ошибка сервера: $error, мы уже знаем о ней и усердно работаем над исправлением'; - } - - @override - String get noErrorInformation => - 'Произошла ошибка, но у нас нет информации о ней. Мы уже расследуем этот вопрос'; - - @override - String get yourName => 'Ваше имя'; - - @override - String get nameHint => 'Иван Иванов'; - - @override - String get errorPageNotFoundTitle => 'Страница не найдена'; - - @override - String get errorPageNotFoundMessage => - 'Упс! Мы не смогли найти эту страницу.'; - - @override - String get errorPageNotFoundHint => - 'Запрашиваемая страница не существует или была перемещена. Пожалуйста, проверьте URL или вернитесь на главную страницу.'; - - @override - String get errorUnknown => 'Произошла неизвестная ошибка'; - - @override - String get unknown => 'неизвестно'; - - @override - String get goToLogin => 'Перейти к входу'; - - @override - String get goBack => 'Назад'; - - @override - String get goToMainPage => 'На главную'; - - @override - String get goToSignUp => 'Перейти к регистрации'; - - @override - String signupError(Object error) { - return 'Не удалось зарегистрироваться: $error'; - } - - @override - String signupSuccess(Object email) { - return 'Письмо с подтверждением email отправлено на $email. Пожалуйста, откройте его и перейдите по ссылке для активации вашего аккаунта.'; - } - - @override - String connectivityError(Object serverAddress) { - return 'Не удается связаться с сервером $serverAddress. Проверьте ваше интернет-соединение и попробуйте снова.'; - } - - @override - String get errorAccountExists => 'Account already exists'; - - @override - String get errorAccountNotVerified => - 'Ваш аккаунт еще не подтвержден. Пожалуйста, проверьте вашу электронную почту для завершения верификации'; - - @override - String get errorLoginUnauthorized => - 'Неверный логин или пароль. Пожалуйста, попробуйте снова'; - - @override - String get errorInternalError => - 'Произошла внутренняя ошибка. Мы в курсе проблемы и работаем над ее решением. Пожалуйста, попробуйте позже'; - - @override - String get errorVerificationTokenNotFound => - 'Аккаунт для верификации не найден. Зарегистрируйтесь снова'; - - @override - String get created => 'Создано'; - - @override - String get edited => 'Изменено'; - - @override - String get errorDataConflict => - 'Мы не можем обработать ваши данные, так как они содержат конфликтующую или противоречивую информацию.'; - - @override - String get errorAccessDenied => - 'У вас нет разрешения на доступ к этому ресурсу. Если вам нужен доступ, пожалуйста, обратитесь к администратору.'; - - @override - String get errorBrokenPayload => - 'Отправленные данные недействительны или неполны. Пожалуйста, проверьте введенные данные и попробуйте снова.'; - - @override - String get errorInvalidArgument => - 'Один или несколько аргументов недействительны. Проверьте введенные данные и попробуйте снова.'; - - @override - String get errorBrokenReference => - 'Ресурс, к которому вы пытаетесь получить доступ, не может быть найден. Возможно, он был перемещен или удален.'; - - @override - String get errorInvalidQueryParameter => - 'Один или несколько параметров запроса отсутствуют или указаны неверно. Проверьте их и попробуйте снова.'; - - @override - String get errorNotImplemented => - 'Эта функция еще недоступна. Пожалуйста, попробуйте позже или обратитесь в службу поддержки.'; - - @override - String get errorLicenseRequired => - 'Для выполнения этого действия требуется действующая лицензия. Пожалуйста, обратитесь к вашему администратору.'; - - @override - String get errorNotFound => - 'Мы не смогли найти запрошенный ресурс. Возможно, он был удален или временно недоступен.'; - - @override - String get errorNameMissing => 'Пожалуйста, укажите имя для продолжения.'; - - @override - String get errorEmailMissing => - 'Пожалуйста, укажите адрес электронной почты для продолжения.'; - - @override - String get errorPasswordMissing => - 'Пожалуйста, укажите пароль для продолжения.'; - - @override - String get errorEmailNotRegistered => - 'Мы не нашли аккаунт, связанный с этим адресом электронной почты.'; - - @override - String get errorDuplicateEmail => - 'Этот адрес электронной почты уже используется. Попробуйте другой или восстановите пароль.'; - - @override - String get showDetailsAction => 'Показать детали'; - - @override - String get errorLogin => 'Ошибка входа'; - - @override - String get errorCreatingInvitation => 'Не удалось создать приглашение'; - - @override - String get footerCompanyName => 'Sibilla Solutions LTD'; - - @override - String get footerAddress => - '27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus'; - - @override - String get footerSupport => 'Поддержка'; - - @override - String get footerEmail => 'Email TBD'; - - @override - String get footerPhoneLabel => 'Телефон'; - - @override - String get footerPhone => '+357 22 000 253'; - - @override - String get footerTermsOfService => 'Условия обслуживания'; - - @override - String get footerPrivacyPolicy => 'Политика конфиденциальности'; - - @override - String get footerCookiePolicy => 'Политика использования файлов cookie'; - - @override - String get navigationLogout => 'Выйти'; - - @override - String get dashboard => 'Дашборд'; - - @override - String get navigationUsersSettings => 'Пользователи'; - - @override - String get navigationRolesSettings => 'Роли'; - - @override - String get navigationPermissionsSettings => 'Разрешения'; - - @override - String get usersManagement => 'Управление пользователями'; - - @override - String get navigationOrganizationSettings => 'Настройки организации'; - - @override - String get navigationAccountSettings => 'Настройки профиля'; - - @override - String get twoFactorPrompt => - 'Введите 6-значный код, отправленный на ваше устройство'; - - @override - String get twoFactorResend => 'Не получили код? Отправить снова'; - - @override - String get twoFactorTitle => 'Двухфакторная аутентификация'; - - @override - String get twoFactorError => 'Неверный код. Пожалуйста, попробуйте снова.'; - - @override - String get payoutNavDashboard => 'Дашборд'; - - @override - String get payoutNavSendPayout => 'Отправить выплату'; - - @override - String get payoutNavRecipients => 'Получатели'; - - @override - String get payoutNavReports => 'Отчеты'; - - @override - String get payoutNavSettings => 'Настройки'; - - @override - String get payoutNavLogout => 'Выйти'; - - @override - String get payoutNavMethods => 'Выплаты'; - - @override - String get expand => 'Развернуть'; - - @override - String get collapse => 'Свернуть'; - - @override - String get pageTitleRecipients => 'Адресная книга получателей'; - - @override - String get actionAddNew => 'Добавить'; - - @override - String get colDataOwner => 'Владелец данных'; - - @override - String get colAvatar => 'Аватар'; - - @override - String get colName => 'Имя'; - - @override - String get colEmail => 'Email'; - - @override - String get colStatus => 'Статус'; - - @override - String get statusReady => 'Готов'; - - @override - String get statusRegistered => 'Зарегистрирован'; - - @override - String get statusNotRegistered => 'Не зарегистрирован'; - - @override - String get typeInternal => 'Управляется мной'; - - @override - String get typeExternal => 'Самоуправляемый'; - - @override - String get searchHint => 'Поиск получателей'; - - @override - String get colActions => 'Действия'; - - @override - String get menuEdit => 'Редактировать'; - - @override - String get menuSendPayout => 'Отправить выплату'; - - @override - String get tooltipRowActions => 'Другие действия'; - - @override - String get accountSettings => 'Настройки аккаунта'; - - @override - String get accountNameUpdateError => 'Не удалось обновить имя аккаунта'; - - @override - String get settingsSuccessfullyUpdated => 'Настройки успешно обновлены'; - - @override - String get language => 'Язык'; - - @override - String get failedToUpdateLanguage => 'Не удалось обновить язык'; - - @override - String get settingsImageUpdateError => 'Не удалось обновить изображение'; - - @override - String get settingsImageTitle => 'Изображение'; - - @override - String get settingsImageHint => 'Нажмите, чтобы изменить изображение'; - - @override - String get accountName => 'Имя'; - - @override - String get accountNameHint => 'Укажите ваше имя'; - - @override - String get avatar => 'Фото профиля'; - - @override - String get avatarHint => 'Нажмите для обновления'; - - @override - String get avatarUpdateError => 'Не удалось обновить фото профиля'; - - @override - String get settings => 'Настройки'; - - @override - String get notSet => 'не задано'; - - @override - String get search => 'Поиск...'; - - @override - String get ok => 'Ок'; - - @override - String get cancel => 'Отмена'; - - @override - String get confirm => 'Подтвердить'; - - @override - String get back => 'Назад'; - - @override - String get operationfryTitle => 'История операций'; - - @override - String get filters => 'Фильтры'; - - @override - String get period => 'Период'; - - @override - String get selectPeriod => 'Выберите период'; - - @override - String get apply => 'Применить'; - - @override - String status(String status) { - return '$status'; - } - - @override - String get operationStatusSuccessful => 'Успешно'; - - @override - String get operationStatusPending => 'В ожидании'; - - @override - String get operationStatusUnsuccessful => 'Неуспешно'; - - @override - String get statusColumn => 'Статус'; - - @override - String get fileNameColumn => 'Имя файла'; - - @override - String get amountColumn => 'Сумма'; - - @override - String get toAmountColumn => 'На сумму'; - - @override - String get payIdColumn => 'Pay ID'; - - @override - String get cardNumberColumn => 'Номер карты'; - - @override - String get nameColumn => 'Имя'; - - @override - String get dateColumn => 'Дата'; - - @override - String get commentColumn => 'Комментарий'; - - @override - String get paymentConfigTitle => 'Куда получать деньги'; - - @override - String get paymentConfigSubtitle => - 'Добавьте несколько методов и выберите основной.'; - - @override - String get addPaymentMethod => 'Добавить способ оплаты'; - - @override - String get makeMain => 'Сделать основным'; - - @override - String get advanced => 'Дополнительно'; - - @override - String get fallbackExplanation => - 'Если основной метод недоступен, мы попробуем следующий включенный метод в списке.'; - - @override - String get delete => 'Удалить'; - - @override - String get deletePaymentConfirmation => - 'Вы уверены, что хотите удалить этот способ оплаты?'; - - @override - String get edit => 'Редактировать'; - - @override - String get moreActions => 'Еще действия'; - - @override - String get noPayouts => 'Нет выплат'; - - @override - String get enterBankName => 'Введите название банка'; - - @override - String get paymentType => 'Тип способа оплаты'; - - @override - String get selectPaymentType => 'Пожалуйста, выберите тип способа оплаты'; - - @override - String get paymentTypeCard => 'Кредитная карта'; - - @override - String get paymentTypeBankAccount => 'Российский банковский счет'; - - @override - String get paymentTypeIban => 'IBAN'; - - @override - String get paymentTypeWallet => 'Кошелек'; - - @override - String get cardNumber => 'Номер карты'; - - @override - String get enterCardNumber => 'Введите номер карты'; - - @override - String get cardholderName => 'Имя держателя карты'; - - @override - String get iban => 'IBAN'; - - @override - String get enterIban => 'Введите IBAN'; - - @override - String get bic => 'BIC'; - - @override - String get bankName => 'Название банка'; - - @override - String get accountHolder => 'Владелец счета'; - - @override - String get enterAccountHolder => 'Введите владельца счета'; - - @override - String get enterBic => 'Введите BIC'; - - @override - String get walletId => 'ID кошелька'; - - @override - String get enterWalletId => 'Введите ID кошелька'; - - @override - String get recipients => 'Получатели'; - - @override - String get recipientName => 'Имя получателя'; - - @override - String get enterRecipientName => 'Введите имя получателя'; - - @override - String get inn => 'ИНН'; - - @override - String get enterInn => 'Введите ИНН'; - - @override - String get kpp => 'КПП'; - - @override - String get enterKpp => 'Введите КПП'; - - @override - String get accountNumber => 'Номер счета'; - - @override - String get enterAccountNumber => 'Введите номер счета'; - - @override - String get correspondentAccount => 'Корреспондентский счет'; - - @override - String get enterCorrespondentAccount => 'Введите корреспондентский счет'; - - @override - String get bik => 'БИК'; - - @override - String get enterBik => 'Введите БИК'; - - @override - String get add => 'Добавить'; - - @override - String get expiryDate => 'Срок действия (ММ/ГГ)'; - - @override - String get firstName => 'Имя'; - - @override - String get enterFirstName => 'Введите имя'; - - @override - String get lastName => 'Фамилия'; - - @override - String get enterLastName => 'Введите фамилию'; - - @override - String get sendSingle => 'Отправить одну транзакцию'; - - @override - String get sendMultiple => 'Отправить несколько транзакций'; - - @override - String get addFunds => 'Пополнить счет'; - - @override - String get close => 'Закрыть'; - - @override - String get multiplePayout => 'Множественная выплата'; - - @override - String get howItWorks => 'Как это работает?'; - - @override - String get exampleTitle => 'Формат файла и образец'; - - @override - String get downloadSampleCSV => 'Скачать sample.csv'; - - @override - String get tokenColumn => 'Токен (обязательно)'; - - @override - String get currency => 'Валюта'; - - @override - String get amount => 'Сумма'; - - @override - String get comment => 'Комментарий'; - - @override - String get uploadCSV => 'Загрузите ваш CSV'; - - @override - String get upload => 'Загрузить'; - - @override - String get hintUpload => 'Поддерживаемый формат: .CSV · Макс. размер 1 МБ'; - - @override - String get uploadHistory => 'История загрузок'; - - @override - String get payout => 'Выплата'; - - @override - String get sendTo => 'Отправить выплату'; - - @override - String get send => 'Отправить выплату'; - - @override - String get recipientPaysFee => 'Получатель оплачивает комиссию'; - - @override - String sentAmount(String amount) { - return 'Отправленная сумма: \$$amount'; - } - - @override - String fee(String fee) { - return 'Комиссия: \$$fee'; - } - - @override - String recipientWillReceive(String amount) { - return 'Получатель получит: \$$amount'; - } - - @override - String total(String total) { - return 'Итого: \$$total'; - } - - @override - String get hideDetails => 'Скрыть детали'; - - @override - String get showDetails => 'Показать детали'; - - @override - String get whereGetMoney => 'Источник средств для списания'; - - @override - String get details => 'Детали'; - - @override - String get addRecipient => 'Добавить получателя'; - - @override - String get editRecipient => 'Редактировать получателя'; - - @override - String get saveRecipient => 'Сохранить получателя'; - - @override - String get choosePaymentMethod => 'Способы оплаты (выберите хотя бы 1)'; - - @override - String get recipientFormRule => - 'Получатель должен иметь хотя бы один способ оплаты'; - - @override - String get allStatus => 'Все'; - - @override - String get readyStatus => 'Готов'; - - @override - String get registeredStatus => 'Зарегистрирован'; - - @override - String get notRegisteredStatus => 'Не зарегистрирован'; - - @override - String get noRecipientSelected => 'Получатель не выбран'; - - @override - String get companyName => 'Name of your company'; - - @override - String get companynameRequired => 'Company name required'; - - @override - String get errorSignUp => 'Error occured while signing up, try again later'; - - @override - String get companyDescription => 'Company Description'; - - @override - String get companyDescriptionHint => - 'Describe any of the fields of the Company\'s business'; - - @override - String get optional => 'optional'; -} diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index b152d02..b5d53d1 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -431,5 +431,7 @@ "errorSignUp": "Error occured while signing up, try again later", "companyDescription": "Company Description", "companyDescriptionHint": "Describe any of the fields of the Company's business", - "optional": "optional" + "optional": "optional", + "ownerRole": "Organization Owner", + "ownerRoleDescription": "This role is granted to the organization’s creator, providing full administrative privileges" } \ No newline at end of file diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index def1290..5303005 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -422,5 +422,8 @@ "registeredStatus": "Зарегистрирован", "notRegisteredStatus": "Не зарегистрирован", - "noRecipientSelected": "Получатель не выбран" + "noRecipientSelected": "Получатель не выбран", + + "ownerRole": "Владелец организации", + "ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права" } \ No newline at end of file diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index 2963427..3354d40 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -11,7 +11,6 @@ import 'package:pshared/config/constants.dart'; import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/locale.dart'; import 'package:pshared/provider/organizations.dart'; -import 'package:pshared/provider/pfe/provider.dart'; import 'package:pweb/app/app.dart'; import 'package:pweb/app/timeago.dart'; @@ -26,7 +25,6 @@ import 'package:pweb/providers/upload_history.dart'; import 'package:pweb/providers/wallets.dart'; import 'package:pweb/providers/wallet_transactions.dart'; import 'package:pweb/services/amplitude.dart'; -import 'package:pweb/services/auth.dart'; import 'package:pweb/services/operations.dart'; import 'package:pweb/services/payments/payment_methods.dart'; import 'package:pweb/services/payments/upload_history.dart'; @@ -56,19 +54,18 @@ void main() async { runApp( MultiProvider( providers: [ - Provider( - create: (_) => AuthenticationService(), - ), - ChangeNotifierProxyProvider( - create: (context) => TwoFactorProvider( - context.read(), - ), - update: (context, authService, previous) => TwoFactorProvider(authService), - ), ChangeNotifierProvider(create: (_) => LocaleProvider(null)), ChangeNotifierProvider(create: (_) => AccountProvider()), + ChangeNotifierProxyProvider( + create: (context) => TwoFactorProvider( + accountProvider: context.read(), + ), + update: (context, accountProvider, previous) => TwoFactorProvider( + accountProvider: accountProvider, + ), + ), ChangeNotifierProvider(create: (_) => OrganizationsProvider()), - ChangeNotifierProvider(create: (_) => PfeProvider()), + ChangeNotifierProvider(create: (_) => AccountProvider()), ChangeNotifierProvider(create: (_) => CarouselIndexProvider()), ChangeNotifierProvider( diff --git a/frontend/pweb/lib/pages/2fa/resend.dart b/frontend/pweb/lib/pages/2fa/resend.dart index 57bde0d..13eb971 100644 --- a/frontend/pweb/lib/pages/2fa/resend.dart +++ b/frontend/pweb/lib/pages/2fa/resend.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/providers/two_factor.dart'; class ResendCodeButton extends StatelessWidget { @@ -12,9 +15,7 @@ class ResendCodeButton extends StatelessWidget { final localizations = AppLocalizations.of(context)!; return TextButton( - onPressed: () { - // TODO: Add resend logic - }, + onPressed: () => context.read().resendCode(), style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: const Size(0, 0), @@ -28,4 +29,4 @@ class ResendCodeButton extends StatelessWidget { child: Text(localizations.twoFactorResend), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/login/form.dart b/frontend/pweb/lib/pages/login/form.dart index e221ea6..bf230b1 100644 --- a/frontend/pweb/lib/pages/login/form.dart +++ b/frontend/pweb/lib/pages/login/form.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/pfe/provider.dart'; +import 'package:pshared/provider/account.dart'; +import 'package:pshared/provider/locale.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/pages/login/buttons.dart'; @@ -34,17 +35,22 @@ class _LoginFormState extends State { final ValueNotifier _isPasswordAcceptable = ValueNotifier(false); Future _login(BuildContext context, VoidCallback onLogin, void Function(Object e) onError) async { - final pfeProvider = Provider.of(context, listen: false); + final provider = Provider.of(context, listen: false); try { - // final account = await pfeProvider.login( - // email: _usernameController.text, - // password: _passwordController.text, - // ); - onLogin(); + final outcome = await provider.login( + email: _usernameController.text, + password: _passwordController.text, + locale: context.read().locale.languageCode, + ); + if (outcome.isPending) { + navigateAndReplace(context, Pages.sfactor); + } else { + onLogin(); + } return 'ok'; } catch (e) { - onError(pfeProvider.error == null ? e : pfeProvider.error!); + onError(provider.error ?? e); } return null; } @@ -89,7 +95,7 @@ class _LoginFormState extends State { onSignUp: () => navigate(context, Pages.signup), login: () => _login( context, - () => navigateAndReplace(context, Pages.sfactor), + () => navigateAndReplace(context, Pages.dashboard), (e) => postNotifyUserOfErrorX( context: context, errorSituation: AppLocalizations.of(context)!.errorLogin, diff --git a/frontend/pweb/lib/pages/signup/form/state.dart b/frontend/pweb/lib/pages/signup/form/state.dart index a25ec84..38f651d 100644 --- a/frontend/pweb/lib/pages/signup/form/state.dart +++ b/frontend/pweb/lib/pages/signup/form/state.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_timezone/flutter_timezone.dart'; + import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/pfe/provider.dart'; +import 'package:pshared/api/requests/login_data.dart'; +import 'package:pshared/models/describable.dart'; +import 'package:pshared/provider/account.dart'; +import 'package:pshared/provider/locale.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/pages/signup/form/content.dart'; @@ -31,7 +36,7 @@ class SignUpFormState extends State { VoidCallback onSignUp, void Function(Object e) onError, ) async { - final pfeProvider = Provider.of(context, listen: false); + final provider = Provider.of(context, listen: false); setState(() { _autoValidateMode = true; @@ -42,20 +47,34 @@ class SignUpFormState extends State { } try { - // final account = await pfeProvider.signUp( - // companyName: controllers.companyName.text.trim(), - // description: controllers.description.text.trim().isEmpty - // ? null - // : controllers.description.text.trim(), - // firstName: controllers.firstName.text.trim(), - // lastName: controllers.lastName.text.trim(), - // email: controllers.email.text.trim(), - // password: controllers.password.text, - // ); + final orgDescription = controllers.description.text.trim(); + final locs = AppLocalizations.of(context)!; + final locale = context.read().locale; + final timezone = await FlutterTimezone.getLocalTimezone(locale.toString()); + await provider.signup( + account: AccountData.build( + login: LoginData.build( + login: controllers.email.text.trim(), + password: controllers.password.text, + locale: locale.toLanguageTag(), + ), + name: controllers.password.text, + lastName: controllers.lastName.text.trim(), + ), + organization: newDescribable( + name: controllers.companyName.text.trim(), + description: orgDescription.isEmpty ? null : orgDescription, + ), + timezone: timezone.identifier, + ownerRole: newDescribable( + name: locs.ownerRole, + description: locs.ownerRoleDescription, + ), + ); onSignUp(); return 'ok'; } catch (e) { - onError(pfeProvider.error ?? e); + onError(provider.error ?? e); } return null; } diff --git a/frontend/pweb/lib/providers/two_factor.dart b/frontend/pweb/lib/providers/two_factor.dart index c5a74e4..f23bb52 100644 --- a/frontend/pweb/lib/providers/two_factor.dart +++ b/frontend/pweb/lib/providers/two_factor.dart @@ -1,38 +1,69 @@ import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; -import 'package:pweb/services/auth.dart'; - +import 'package:pshared/models/auth/pending_login.dart'; +import 'package:pshared/provider/account.dart'; +import 'package:pshared/service/account.dart'; class TwoFactorProvider extends ChangeNotifier { - final AuthenticationService _authService; + static final _logger = Logger('provider.two_factor'); + final AccountProvider _accountProvider; - TwoFactorProvider(this._authService); + TwoFactorProvider({required AccountProvider accountProvider}) : _accountProvider = accountProvider; bool _isSubmitting = false; bool _hasError = false; bool _verificationSuccess = false; + String? _errorMessage; bool get isSubmitting => _isSubmitting; bool get hasError => _hasError; bool get verificationSuccess => _verificationSuccess; + String? get errorMessage => _errorMessage; + PendingLogin? get pendingLogin => _accountProvider.pendingLogin; Future submitCode(String code) async { _isSubmitting = true; _hasError = false; + _errorMessage = null; _verificationSuccess = false; notifyListeners(); try { - final success = await _authService.verifyTwoFactorCode(code); - if (success) { - _verificationSuccess = true; + final pending = _accountProvider.pendingLogin; + if (pending == null) { + throw Exception('No pending login available'); } + final account = await AccountService.confirmLoginCode( + pending: pending, + code: code, + ); + _accountProvider.completePendingLogin(account); + _verificationSuccess = true; } catch (e) { _hasError = true; + _errorMessage = e.toString(); + _logger.warning('Failed to verify code', e); } finally { _isSubmitting = false; notifyListeners(); } } -} \ No newline at end of file + + Future resendCode() async { + final pending = _accountProvider.pendingLogin; + if (pending == null) { + _logger.warning('No pending login to resend code for'); + return; + } + try { + await AccountService.resendLoginCode(pending); + } catch (e) { + _logger.warning('Failed to resend login code', e); + _hasError = true; + _errorMessage = e.toString(); + notifyListeners(); + } + } +} diff --git a/frontend/pweb/lib/services/auth.dart b/frontend/pweb/lib/services/auth.dart deleted file mode 100644 index 95a75e7..0000000 --- a/frontend/pweb/lib/services/auth.dart +++ /dev/null @@ -1,12 +0,0 @@ - -class AuthenticationService { - Future verifyTwoFactorCode(String code) async { - await Future.delayed(const Duration(seconds: 2)); - - if (code == '000000') { - return true; - } else { - throw Exception('Wrong Code'); //TODO Localize - } - } -} diff --git a/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift index 79f5652..33de092 100644 --- a/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,7 +8,6 @@ import Foundation import amplitude_flutter import file_selector_macos import flutter_timezone -import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -18,7 +17,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AmplitudeFlutterPlugin.register(with: registry.registrar(forPlugin: "AmplitudeFlutterPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/frontend/pweb/pubspec.lock b/frontend/pweb/pubspec.lock deleted file mode 100644 index 3cd1a40..0000000 --- a/frontend/pweb/pubspec.lock +++ /dev/null @@ -1,1334 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d - url: "https://pub.dev" - source: hosted - version: "91.0.0" - amplitude_flutter: - dependency: "direct main" - description: - name: amplitude_flutter - sha256: "9c07360f23bd82f889c1b61573c7015f1b4cdf2a3bc032bd7f005cb07ba2f763" - url: "https://pub.dev" - source: hosted - version: "4.3.9" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 - url: "https://pub.dev" - source: hosted - version: "8.4.1" - appflowy_board: - dependency: "direct main" - description: - name: appflowy_board - sha256: "4dc5ce013913723ca330db350df154abdf1315285bcf61a35d65471e9ea00517" - url: "https://pub.dev" - source: hosted - version: "0.1.2" - archive: - dependency: transitive - description: - name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" - url: "https://pub.dev" - source: hosted - version: "4.0.7" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - badges: - dependency: "direct main" - description: - name: badges - sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - base58check: - dependency: transitive - description: - name: base58check - sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - bech32: - dependency: transitive - description: - name: bech32 - sha256: "156cbace936f7720c79a79d16a03efad343b1ef17106716e04b8b8e39f99f7f7" - url: "https://pub.dev" - source: hosted - version: "0.2.2" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - build: - dependency: transitive - description: - name: build - sha256: dfb67ccc9a78c642193e0c2d94cb9e48c2c818b3178a86097d644acdcde6a8d9 - url: "https://pub.dev" - source: hosted - version: "4.0.2" - build_config: - dependency: transitive - description: - name: build_config - sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - build_daemon: - dependency: transitive - description: - name: build_daemon - sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 - url: "https://pub.dev" - source: hosted - version: "4.1.1" - build_runner: - dependency: "direct dev" - description: - name: build_runner - sha256: "7b5b569f3df370590a85029148d6fc66c7d0201fc6f1847c07dd85d365ae9fcd" - url: "https://pub.dev" - source: hosted - version: "2.10.3" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d - url: "https://pub.dev" - source: hosted - version: "8.12.0" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" - source: hosted - version: "4.1.1" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" - source: hosted - version: "0.4.2" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" - url: "https://pub.dev" - source: hosted - version: "4.11.0" - collection: - dependency: "direct main" - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - country_flags: - dependency: "direct main" - description: - name: country_flags - sha256: "714f2d415e74828eb08787d552a05e94cdf2cbe0607a5656f3e70087cd7bb7e0" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "942a4791cd385a68ccb3b32c71c427aba508a1bb949b86dff2adbe4049f16239" - url: "https://pub.dev" - source: hosted - version: "0.3.5" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" - source: hosted - version: "1.0.8" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b - url: "https://pub.dev" - source: hosted - version: "3.1.3" - dotted_border: - dependency: "direct main" - description: - name: dotted_border - sha256: "99b091ec6891ba0c5331fdc2b502993c7c108f898995739a73c6845d71dad70c" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - email_validator: - dependency: "direct main" - description: - name: email_validator - sha256: b19aa5d92fdd76fbc65112060c94d45ba855105a28bb6e462de7ff03b12fa1fb - url: "https://pub.dev" - source: hosted - version: "3.0.0" - equatable: - dependency: transitive - description: - name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" - url: "https://pub.dev" - source: hosted - version: "2.0.7" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - fancy_password_field: - dependency: "direct main" - description: - name: fancy_password_field - sha256: ff2bd9daecfc09d00c978657642774d11320020678e589bdb5469b5079385448 - url: "https://pub.dev" - source: hosted - version: "2.0.8" - ffi: - dependency: transitive - description: - name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - file_selector_linux: - dependency: transitive - description: - name: file_selector_linux - sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" - url: "https://pub.dev" - source: hosted - version: "0.9.3+2" - file_selector_macos: - dependency: transitive - description: - name: file_selector_macos - sha256: "88707a3bec4b988aaed3b4df5d7441ee4e987f20b286cddca5d6a8270cab23f2" - url: "https://pub.dev" - source: hosted - version: "0.9.4+5" - file_selector_platform_interface: - dependency: transitive - description: - name: file_selector_platform_interface - sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" - url: "https://pub.dev" - source: hosted - version: "2.7.0" - file_selector_windows: - dependency: transitive - description: - name: file_selector_windows - sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" - url: "https://pub.dev" - source: hosted - version: "0.9.3+4" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - fl_chart: - dependency: "direct main" - description: - name: fl_chart - sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_cache_manager: - dependency: transitive - description: - name: flutter_cache_manager - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - flutter_highlight: - dependency: transitive - description: - name: flutter_highlight - sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" - url: "https://pub.dev" - source: hosted - version: "0.7.0" - flutter_launcher_icons: - dependency: "direct main" - description: - name: flutter_launcher_icons - sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" - url: "https://pub.dev" - source: hosted - version: "0.14.4" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_multi_formatter: - dependency: "direct main" - description: - name: flutter_multi_formatter - sha256: "29d9b3d30a985f5a9c3dd52b4e25e64b9a20ebdcf4d9fed0c71e653406598604" - url: "https://pub.dev" - source: hosted - version: "2.13.10" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687" - url: "https://pub.dev" - source: hosted - version: "2.0.32" - flutter_settings_ui: - dependency: "direct main" - description: - name: flutter_settings_ui - sha256: dcc506fab724192594e5c232b6214a941abd6e7b5151626635b89258fadbc17c - url: "https://pub.dev" - source: hosted - version: "3.0.1" - flutter_svg: - dependency: transitive - description: - name: flutter_svg - sha256: "055de8921be7b8e8b98a233c7a5ef84b3a6fcc32f46f1ebf5b9bb3576d108355" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_timezone: - dependency: "direct main" - description: - name: flutter_timezone - sha256: "978192f2f9ea6d019a4de4f0211d76a9af955ca24865828fa98ca4e20cf0cb3c" - url: "https://pub.dev" - source: hosted - version: "5.0.1" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - font_awesome_flutter: - dependency: transitive - description: - name: font_awesome_flutter - sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0 - url: "https://pub.dev" - source: hosted - version: "10.12.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - go_router: - dependency: "direct main" - description: - name: go_router - sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104 - url: "https://pub.dev" - source: hosted - version: "17.0.0" - graphs: - dependency: transitive - description: - name: graphs - sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - highlight: - dependency: transitive - description: - name: highlight - sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" - url: "https://pub.dev" - source: hosted - version: "0.7.0" - http: - dependency: transitive - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - icann_tlds: - dependency: "direct main" - description: - name: icann_tlds - sha256: "88522e8ee7e757163184fa985d50f05b72dc03f25bcdb9ceb143f5d946b7d80f" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - image: - dependency: transitive - description: - name: image - sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" - url: "https://pub.dev" - source: hosted - version: "4.5.4" - image_picker: - dependency: "direct main" - description: - name: image_picker - sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - sha256: ca2a3b04d34e76157e9ae680ef16014fb4c2d20484e78417eaed6139330056f6 - url: "https://pub.dev" - source: hosted - version: "0.8.13+7" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - sha256: e675c22790bcc24e9abd455deead2b7a88de4b79f7327a281812f14de1a56f58 - url: "https://pub.dev" - source: hosted - version: "0.8.13+1" - image_picker_linux: - dependency: transitive - description: - name: image_picker_linux - sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" - url: "https://pub.dev" - source: hosted - version: "0.2.2" - image_picker_macos: - dependency: transitive - description: - name: image_picker_macos - sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" - url: "https://pub.dev" - source: hosted - version: "0.2.2+1" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" - url: "https://pub.dev" - source: hosted - version: "2.11.1" - image_picker_windows: - dependency: transitive - description: - name: image_picker_windows - sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae - url: "https://pub.dev" - source: hosted - version: "0.2.2" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - io: - dependency: transitive - description: - name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" - source: hosted - version: "1.0.5" - jovial_misc: - dependency: transitive - description: - name: jovial_misc - sha256: "4301011027d87b8b919cb862db84071a34448eadbb32cc8d40fe505424dfe69a" - url: "https://pub.dev" - source: hosted - version: "0.9.2" - jovial_svg: - dependency: "direct main" - description: - name: jovial_svg - sha256: "08dd24b800d48796c9c0227acb96eb00c6cacccb1d7de58d79fc924090049868" - url: "https://pub.dev" - source: hosted - version: "1.1.28" - json_annotation: - dependency: "direct main" - description: - name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" - source: hosted - version: "4.9.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 - url: "https://pub.dev" - source: hosted - version: "6.0.0" - logging: - dependency: "direct main" - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - markdown: - dependency: transitive - description: - name: markdown - sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" - url: "https://pub.dev" - source: hosted - version: "7.3.0" - markdown_widget: - dependency: "direct main" - description: - name: markdown_widget - sha256: b52c13d3ee4d0e60c812e15b0593f142a3b8a2003cde1babb271d001a1dbdc1c - url: "https://pub.dev" - source: hosted - version: "2.3.2+8" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.dev" - source: hosted - version: "1.16.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - octo_image: - dependency: transitive - description: - name: octo_image - sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" - password_strength: - dependency: transitive - description: - name: password_strength - sha256: "0e51e3d864e37873a1347e658147f88b66e141ee36c58e19828dc5637961e1ce" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - path_provider: - dependency: transitive - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16 - url: "https://pub.dev" - source: hosted - version: "2.2.20" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" - url: "https://pub.dev" - source: hosted - version: "7.0.1" - pin_code_fields: - dependency: "direct main" - description: - name: pin_code_fields - sha256: "4c0db7fbc889e622e7c71ea54b9ee624bb70c7365b532abea0271b17ea75b729" - url: "https://pub.dev" - source: hosted - version: "8.0.1" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pool: - dependency: transitive - description: - name: pool - sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" - url: "https://pub.dev" - source: hosted - version: "1.5.2" - posix: - dependency: transitive - description: - name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" - url: "https://pub.dev" - source: hosted - version: "6.0.3" - provider: - dependency: "direct main" - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - pshared: - dependency: "direct main" - description: - path: "../pshared" - relative: true - source: path - version: "1.0.0" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.dev" - source: hosted - version: "1.5.0" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - scroll_to_index: - dependency: transitive - description: - name: scroll_to_index - sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 - url: "https://pub.dev" - source: hosted - version: "3.0.1" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" - url: "https://pub.dev" - source: hosted - version: "12.0.1" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" - url: "https://pub.dev" - source: hosted - version: "2.5.3" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713" - url: "https://pub.dev" - source: hosted - version: "2.4.15" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b" - url: "https://pub.dev" - source: hosted - version: "2.5.5" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shelf: - dependency: transitive - description: - name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.dev" - source: hosted - version: "1.4.2" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 - url: "https://pub.dev" - source: hosted - version: "2.4.2+2" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" - source: hosted - version: "2.1.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - syncfusion_flutter_charts: - dependency: "direct main" - description: - name: syncfusion_flutter_charts - sha256: b303d874106a8d21484428d1bed6372deba0b8a10ef1072ca5fa854409c1d06a - url: "https://pub.dev" - source: hosted - version: "31.2.10" - syncfusion_flutter_core: - dependency: transitive - description: - name: syncfusion_flutter_core - sha256: "8118f13264d1401a7085d12a0aaeac1ebd5cd939046b8c565d195879646daad6" - url: "https://pub.dev" - source: hosted - version: "31.2.10" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 - url: "https://pub.dev" - source: hosted - version: "3.4.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" - url: "https://pub.dev" - source: hosted - version: "0.7.6" - timeago: - dependency: "direct main" - description: - name: timeago - sha256: b05159406a97e1cbb2b9ee4faa9fb096fe0e2dfcd8b08fcd2a00553450d3422e - url: "https://pub.dev" - source: hosted - version: "3.7.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - url_launcher: - dependency: transitive - description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" - source: hosted - version: "6.3.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9" - url: "https://pub.dev" - source: hosted - version: "6.3.24" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9" - url: "https://pub.dev" - source: hosted - version: "6.3.5" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9" - url: "https://pub.dev" - source: hosted - version: "3.2.4" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" - url: "https://pub.dev" - source: hosted - version: "3.1.4" - uuid: - dependency: transitive - description: - name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 - url: "https://pub.dev" - source: hosted - version: "4.5.2" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 - url: "https://pub.dev" - source: hosted - version: "1.1.19" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" - source: hosted - version: "1.1.13" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc - url: "https://pub.dev" - source: hosted - version: "1.1.19" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - visibility_detector: - dependency: transitive - description: - name: visibility_detector - sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 - url: "https://pub.dev" - source: hosted - version: "0.4.0+2" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" - url: "https://pub.dev" - source: hosted - version: "1.1.4" - web: - dependency: "direct main" - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" - source: hosted - version: "3.0.3" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" - source: hosted - version: "5.15.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" - source: hosted - version: "6.6.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.1" diff --git a/infra/vault/docker-compose.yml b/infra/vault/docker-compose.yml new file mode 100644 index 0000000..d4f2a9a --- /dev/null +++ b/infra/vault/docker-compose.yml @@ -0,0 +1,51 @@ +networks: + cicd: + external: true + +volumes: + vault1_data: + vault2_data: + vault3_data: + +services: + vault: + image: hashicorp/vault:latest + cap_add: [ "IPC_LOCK" ] + ulimits: { memlock: { soft: -1, hard: -1 } } + environment: { VAULT_ADDR: "http://127.0.0.1:8200" } + command: vault server -config=/vault/config/vault.hcl + volumes: + - vault1_data:/vault/file + - ./config/vault1.hcl:/vault/config/vault.hcl:ro + networks: [ cicd ] + labels: + - "traefik.enable=true" + - "traefik.docker.network=cicd" + - "traefik.http.routers.vault.rule=Host(`vault.sendico.io`)" + - "traefik.http.routers.vault.entrypoints=websecure" + - "traefik.http.routers.vault.tls.certresolver=letsencrypt" + - "traefik.http.routers.vault.middlewares=secure-headers@file" + - "traefik.http.services.vault.loadbalancer.server.port=8200" + - "traefik.http.services.vault.loadbalancer.server.scheme=http" + + vault2: + image: hashicorp/vault:latest + cap_add: [ "IPC_LOCK" ] + ulimits: { memlock: { soft: -1, hard: -1 } } + environment: { VAULT_ADDR: "http://127.0.0.1:8200" } + command: vault server -config=/vault/config/vault.hcl + volumes: + - vault2_data:/vault/file + - ./config/vault2.hcl:/vault/config/vault.hcl:ro + networks: [ cicd ] + + vault3: + image: hashicorp/vault:latest + cap_add: [ "IPC_LOCK" ] + ulimits: { memlock: { soft: -1, hard: -1 } } + environment: { VAULT_ADDR: "http://127.0.0.1:8200" } + command: vault server -config=/vault/config/vault.hcl + volumes: + - vault3_data:/vault/file + - ./config/vault3.hcl:/vault/config/vault.hcl:ro + networks: [ cicd ] \ No newline at end of file diff --git a/version b/version index ac346d4..b621109 100644 --- a/version +++ b/version @@ -1 +1 @@ -2.0.856 \ No newline at end of file +2.0.857 \ No newline at end of file