New code verification service
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
This commit is contained in:
@@ -7,10 +7,10 @@ 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.20
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.24
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2
|
||||
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
|
||||
@@ -23,7 +23,7 @@ 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
|
||||
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
|
||||
@@ -40,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.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 // 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
|
||||
@@ -104,7 +105,7 @@ 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.3 // 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
|
||||
@@ -126,7 +127,7 @@ require (
|
||||
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
|
||||
|
||||
@@ -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.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64=
|
||||
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.2 h1:DhdbtDl4FdNlj31+xiRXANxEE+eC7n8JQz+/ilwQ8Uc=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/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=
|
||||
@@ -196,8 +198,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.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q=
|
||||
github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||
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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
31
api/server/interface/api/sresponse/login_pending.go
Normal file
31
api/server/interface/api/sresponse/login_pending.go
Normal file
@@ -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,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
11
api/server/interface/services/confirmation/confirmation.go
Normal file
11
api/server/interface/services/confirmation/confirmation.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/interface/services/account"
|
||||
"github.com/tech/sendico/server/interface/services/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"
|
||||
@@ -76,6 +77,7 @@ 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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -133,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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
65
api/server/internal/api/routers/tokens/tokens.go
Normal file
65
api/server/internal/api/routers/tokens/tokens.go
Normal file
@@ -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
|
||||
}
|
||||
48
api/server/internal/server/confirmationimp/request.go
Normal file
48
api/server/internal/server/confirmationimp/request.go
Normal file
@@ -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),
|
||||
})
|
||||
}
|
||||
56
api/server/internal/server/confirmationimp/resend.go
Normal file
56
api/server/internal/server/confirmationimp/resend.go
Normal file
@@ -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),
|
||||
})
|
||||
}
|
||||
158
api/server/internal/server/confirmationimp/service.go
Normal file
158
api/server/internal/server/confirmationimp/service.go
Normal file
@@ -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
|
||||
}
|
||||
181
api/server/internal/server/confirmationimp/store.go
Normal file
181
api/server/internal/server/confirmationimp/store.go
Normal file
@@ -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
|
||||
}
|
||||
23
api/server/internal/server/confirmationimp/types.go
Normal file
23
api/server/internal/server/confirmationimp/types.go
Normal file
@@ -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"`
|
||||
}
|
||||
88
api/server/internal/server/confirmationimp/verify.go
Normal file
88
api/server/internal/server/confirmationimp/verify.go
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user