fixed verification code
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
This commit is contained in:
@@ -43,7 +43,7 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -63,7 +63,7 @@ require (
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
|
||||
@@ -100,8 +100,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
|
||||
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -241,8 +241,8 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -27,7 +27,7 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -48,7 +48,7 @@ require (
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/protobuf v1.36.11
|
||||
|
||||
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -191,8 +191,8 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -20,7 +20,7 @@ require (
|
||||
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -41,7 +41,7 @@ require (
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
|
||||
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -191,8 +191,8 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -25,7 +25,7 @@ require (
|
||||
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -45,7 +45,7 @@ require (
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
|
||||
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -191,8 +191,8 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -26,7 +26,7 @@ require (
|
||||
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -46,7 +46,7 @@ require (
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
)
|
||||
|
||||
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -191,8 +191,8 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -16,7 +16,7 @@ require (
|
||||
github.com/casbin/govaluate v1.10.0 // indirect
|
||||
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.2.0 // indirect
|
||||
|
||||
@@ -49,8 +49,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260204112742-a1cdb34ff7e1 // indirect
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260208002143-2551aa251e34 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||
@@ -53,7 +53,7 @@ require (
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
@@ -81,7 +81,7 @@ require (
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
|
||||
@@ -6,8 +6,8 @@ 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-20260204112742-a1cdb34ff7e1 h1:LyoFl70WFSqEQSOky2dlIhjrm5bcliiM7v7e9Y7IFxc=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260204112742-a1cdb34ff7e1/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260208002143-2551aa251e34 h1:AyAPL6pTcPPpfZsNtOTFhxyOokKBLnrbbaV42g6Z9v0=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260208002143-2551aa251e34/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=
|
||||
@@ -159,8 +159,8 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -341,8 +341,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -337,7 +337,7 @@ func (w *inMemoryWallets) List(ctx context.Context, filter model.ManagedWalletFi
|
||||
continue
|
||||
}
|
||||
}
|
||||
if wallet.Network != filter.Network {
|
||||
if filter.Network != "" && wallet.Network != filter.Network {
|
||||
continue
|
||||
}
|
||||
if filter.TokenSymbol != "" && !strings.EqualFold(wallet.TokenSymbol, filter.TokenSymbol) {
|
||||
|
||||
@@ -24,7 +24,7 @@ require (
|
||||
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -46,7 +46,7 @@ require (
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
)
|
||||
|
||||
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -193,8 +193,8 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -43,7 +43,7 @@ require (
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
)
|
||||
|
||||
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -191,8 +191,8 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -24,7 +24,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260204112742-a1cdb34ff7e1 // indirect
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260208002143-2551aa251e34 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||
@@ -57,7 +57,7 @@ require (
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
@@ -89,7 +89,7 @@ require (
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
|
||||
@@ -6,8 +6,8 @@ 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-20260204112742-a1cdb34ff7e1 h1:LyoFl70WFSqEQSOky2dlIhjrm5bcliiM7v7e9Y7IFxc=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260204112742-a1cdb34ff7e1/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260208002143-2551aa251e34 h1:AyAPL6pTcPPpfZsNtOTFhxyOokKBLnrbbaV42g6Z9v0=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260208002143-2551aa251e34/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=
|
||||
@@ -166,8 +166,8 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -360,8 +360,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -26,7 +26,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -47,7 +47,7 @@ require (
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
)
|
||||
|
||||
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -193,8 +193,8 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -27,7 +27,7 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-test/deep v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
@@ -49,7 +49,7 @@ require (
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
|
||||
@@ -63,8 +63,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -208,8 +208,8 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (a *NotificationAPI) onAccount(context context.Context, account *model.Account) error {
|
||||
func (a *NotificationAPI) onAccount(context context.Context, account *model.Account, token string) error {
|
||||
var link string
|
||||
var err error
|
||||
if link, err = a.dp.GetFullLink("verify", account.VerifyToken); err != nil {
|
||||
if link, err = a.dp.GetFullLink("verify", token); err != nil {
|
||||
a.logger.Warn("Failed to generate verification link", zap.Error(err), zap.String("login", account.Login))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -188,10 +188,9 @@ func TestOnAccount_ValidAccount_SendsWelcomeEmail(t *testing.T) {
|
||||
Locale: "en-US",
|
||||
},
|
||||
},
|
||||
VerifyToken: "test-verify-token",
|
||||
}
|
||||
|
||||
err := api.onAccount(context.Background(), account)
|
||||
err := api.onAccount(context.Background(), account, "test-verify-token")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
@@ -242,10 +241,9 @@ func TestOnAccount_LinkGenerationFails_ReturnsError(t *testing.T) {
|
||||
Locale: "en-US",
|
||||
},
|
||||
},
|
||||
VerifyToken: "test-verify-token",
|
||||
}
|
||||
|
||||
err := api.onAccount(context.Background(), account)
|
||||
err := api.onAccount(context.Background(), account, "test-verify-token")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected error from link generation failure")
|
||||
@@ -285,10 +283,9 @@ func TestOnAccount_SendFails_ReturnsError(t *testing.T) {
|
||||
Locale: "en-US",
|
||||
},
|
||||
},
|
||||
VerifyToken: "test-verify-token",
|
||||
}
|
||||
|
||||
err := api.onAccount(context.Background(), account)
|
||||
err := api.onAccount(context.Background(), account, "test-verify-token")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected error from send failure")
|
||||
|
||||
@@ -56,7 +56,7 @@ require (
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
@@ -104,7 +104,7 @@ require (
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
)
|
||||
|
||||
@@ -70,8 +70,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -238,8 +238,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
|
||||
@@ -196,6 +196,9 @@ func updateExecutionStepsFromGatewayExecution(
|
||||
}
|
||||
|
||||
setExecutionStepStatus(execStep, status)
|
||||
if exec.Error != "" && execStep.Error == "" {
|
||||
execStep.Error = strings.TrimSpace(exec.Error)
|
||||
}
|
||||
|
||||
log.Debug("Execution step state updated",
|
||||
zap.Int("step_index", idx),
|
||||
|
||||
@@ -60,9 +60,17 @@ func TestGatewayExecutionRejectedFailsPayment(t *testing.T) {
|
||||
|
||||
payment := &paymodel.Payment{
|
||||
PaymentRef: "pi-2", State: paymodel.PaymentStateSubmitted, IdempotencyKey: "idem-1",
|
||||
PaymentPlan: &paymodel.PaymentPlan{
|
||||
Steps: []*paymodel.PaymentStep{
|
||||
{StepID: "crypto_send"},
|
||||
},
|
||||
},
|
||||
ExecutionPlan: &paymodel.ExecutionPlan{
|
||||
Steps: []*paymodel.ExecutionStep{
|
||||
{OperationRef: "s1", State: paymodel.OperationStatePlanned, TransferRef: "trn-1"}}}}
|
||||
{Code: "crypto_send", OperationRef: "s1", State: paymodel.OperationStateWaiting, TransferRef: "trn-1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := store.Create(context.Background(), payment); err != nil {
|
||||
t.Fatalf("failed to seed payment: %v", err)
|
||||
@@ -74,9 +82,11 @@ func TestGatewayExecutionRejectedFailsPayment(t *testing.T) {
|
||||
}
|
||||
|
||||
exec := &model.PaymentGatewayExecution{
|
||||
PaymentRef: "pi-2",
|
||||
TransferRef: "trn-1",
|
||||
Status: rail.OperationResultFailed,
|
||||
PaymentRef: "pi-2",
|
||||
OperationRef: "s1",
|
||||
TransferRef: "trn-1",
|
||||
Status: rail.OperationResultFailed,
|
||||
Error: "execution_plan_failed",
|
||||
}
|
||||
|
||||
if err := svc.onGatewayExecution(context.Background(), exec); err != nil {
|
||||
|
||||
@@ -81,6 +81,7 @@ func (p *paymentExecutor) pickIndependentSteps(
|
||||
|
||||
if blocked {
|
||||
lg.Debug("Step permanently blocked by dependency failure")
|
||||
setExecutionStepStatus(execStep, model.OperationStateCancelled)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -66,8 +66,8 @@ func TestReleasePaymentHold_RejectsLegacyLedgerRelease(t *testing.T) {
|
||||
|
||||
store.payments[payment.PaymentRef] = payment
|
||||
|
||||
execPlan := ensureExecutionPlanForPlan(payment, payment.PaymentPlan)
|
||||
steps := executionStepsByCode(execPlan)
|
||||
payment.ExecutionPlan = ensureExecutionPlanForPlan(payment, payment.PaymentPlan)
|
||||
steps := executionStepsByCode(payment.ExecutionPlan)
|
||||
blockStep := steps["ledger_block"]
|
||||
if blockStep == nil {
|
||||
t.Fatalf("expected block step in execution plan")
|
||||
|
||||
@@ -152,6 +152,12 @@ func BadRequest(logger mlogger.Logger, source mservice.Type, err, hint string) h
|
||||
}
|
||||
}
|
||||
|
||||
func Gone(logger mlogger.Logger, source mservice.Type, err, hint string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
errorf(logger, w, r, source, http.StatusGone, err, hint)
|
||||
}
|
||||
}
|
||||
|
||||
func BadQueryParam(logger mlogger.Logger, source mservice.Type, param string, err error) http.HandlerFunc {
|
||||
return BadRequest(logger, source, "invalid_query_parameter", fmt.Sprintf("Failed to parse '%s': %v", param, err))
|
||||
}
|
||||
|
||||
@@ -12,6 +12,5 @@ import (
|
||||
type DB interface {
|
||||
template.DB[*model.Account]
|
||||
GetByEmail(ctx context.Context, email string) (*model.Account, error)
|
||||
GetByToken(ctx context.Context, email string) (*model.Account, error)
|
||||
GetAccountsByRefs(ctx context.Context, orgRef bson.ObjectID, refs []bson.ObjectID) ([]model.Account, error)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/db/refreshtokens"
|
||||
"github.com/tech/sendico/pkg/db/role"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"github.com/tech/sendico/pkg/db/verification"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
)
|
||||
@@ -30,6 +31,7 @@ type Factory interface {
|
||||
NewInvitationsDB() (invitation.DB, error)
|
||||
NewRecipientsDB() (recipient.DB, error)
|
||||
NewPaymentMethodsDB() (paymethod.DB, error)
|
||||
NewVerificationsDB() (verification.DB, error)
|
||||
|
||||
NewRolesDB() (role.DB, error)
|
||||
NewPoliciesDB() (policy.DB, error)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package accountdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
func (db *AccountDB) GetByToken(ctx context.Context, email string) (*model.Account, error) {
|
||||
var account model.Account
|
||||
return &account, db.FindOne(ctx, repository.Query().Filter(repository.Field("verifyToken"), email), &account)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/db/internal/mongo/refreshtokensdb"
|
||||
"github.com/tech/sendico/pkg/db/internal/mongo/rolesdb"
|
||||
"github.com/tech/sendico/pkg/db/internal/mongo/transactionimp"
|
||||
"github.com/tech/sendico/pkg/db/internal/mongo/verificationimp"
|
||||
"github.com/tech/sendico/pkg/db/invitation"
|
||||
"github.com/tech/sendico/pkg/db/organization"
|
||||
"github.com/tech/sendico/pkg/db/paymethod"
|
||||
@@ -32,6 +33,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
"github.com/tech/sendico/pkg/db/role"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"github.com/tech/sendico/pkg/db/verification"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
@@ -246,6 +248,10 @@ func (db *DB) NewRolesDB() (role.DB, error) {
|
||||
return rolesdb.Create(db.logger, db.db())
|
||||
}
|
||||
|
||||
func (db *DB) NewVerificationsDB() (verification.DB, error) {
|
||||
return verificationimp.Create(db.logger, db.db(), db.TransactionFactory())
|
||||
}
|
||||
|
||||
func (db *DB) TransactionFactory() transaction.Factory {
|
||||
return transactionimp.CreateFactory(db.client)
|
||||
}
|
||||
|
||||
81
api/pkg/db/internal/mongo/verificationimp/consume.go
Normal file
81
api/pkg/db/internal/mongo/verificationimp/consume.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package verificationimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
"github.com/tech/sendico/pkg/db/verification"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (db *verificationDB) Consume(
|
||||
ct context.Context,
|
||||
rawToken string,
|
||||
) (*model.VerificationToken, error) {
|
||||
|
||||
hash := tokenHash(rawToken)
|
||||
now := time.Now().UTC()
|
||||
|
||||
// 1) Find token by hash (do NOT filter by usedAt/expiresAt here),
|
||||
// otherwise you can't distinguish "used/expired" from "not found".
|
||||
filter := repository.Query().And(
|
||||
repository.Filter("verifyTokenHash", hash),
|
||||
)
|
||||
|
||||
t, e := db.tf.CreateTransaction().Execute(
|
||||
ct,
|
||||
func(ctx context.Context) (any, error) {
|
||||
var existing model.VerificationToken
|
||||
if err := db.DBImp.FindOne(ctx, filter, &existing); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
db.Logger.Debug("Token hash not found", zap.Error(err), zap.String("hash", hash))
|
||||
return nil, verification.ErorrTokenNotFound()
|
||||
}
|
||||
db.Logger.Warn("Failed to check token", zap.Error(err), zap.String("hash", hash))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2) Semantic checks
|
||||
if existing.UsedAt != nil {
|
||||
db.Logger.Debug(
|
||||
"Token has already been used",
|
||||
zap.String("hash", hash),
|
||||
zap.Time("used_at", *existing.UsedAt),
|
||||
)
|
||||
return nil, verification.ErorrTokenAlreadyUsed()
|
||||
}
|
||||
|
||||
if !existing.ExpiresAt.After(now) { // includes equal time edge-case
|
||||
db.Logger.Debug(
|
||||
"Token has already expired",
|
||||
zap.String("hash", hash),
|
||||
zap.Time("expired_at", existing.ExpiresAt),
|
||||
)
|
||||
return nil, verification.ErorrTokenExpired()
|
||||
}
|
||||
|
||||
// 3) Mark as used
|
||||
existing.UsedAt = &now
|
||||
if err := db.DBImp.Update(ctx, &existing); err != nil {
|
||||
db.Logger.Warn("Failed to consume token", zap.Error(err), zap.String("hash", hash))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &existing, nil
|
||||
},
|
||||
)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
res, ok := t.(*model.VerificationToken)
|
||||
if !ok {
|
||||
return nil, merrors.Internal("unexpected token type")
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
96
api/pkg/db/internal/mongo/verificationimp/create.go
Normal file
96
api/pkg/db/internal/mongo/verificationimp/create.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package verificationimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const verificationTokenBytes = 32
|
||||
|
||||
func newVerificationToken(
|
||||
accountRef bson.ObjectID,
|
||||
purpose model.VerificationPurpose,
|
||||
target string,
|
||||
ttl time.Duration,
|
||||
) (*model.VerificationToken, string, error) {
|
||||
|
||||
raw := make([]byte, verificationTokenBytes)
|
||||
if _, err := rand.Read(raw); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
rawToken := base64.RawURLEncoding.EncodeToString(raw)
|
||||
|
||||
hashStr := tokenHash(rawToken)
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
token := &model.VerificationToken{
|
||||
AccountRef: accountRef,
|
||||
Purpose: purpose,
|
||||
Target: target,
|
||||
VerifyTokenHash: hashStr,
|
||||
UsedAt: nil,
|
||||
ExpiresAt: now.Add(ttl),
|
||||
}
|
||||
|
||||
return token, rawToken, nil
|
||||
}
|
||||
|
||||
func (db *verificationDB) Create(
|
||||
ctx context.Context,
|
||||
accountRef bson.ObjectID,
|
||||
purpose model.VerificationPurpose,
|
||||
target string,
|
||||
ttl time.Duration,
|
||||
) (string, error) {
|
||||
|
||||
logFields := []zap.Field{
|
||||
zap.String("purpose", string(purpose)), zap.Duration("ttl", ttl),
|
||||
mzap.AccRef(accountRef), zap.String("target", target),
|
||||
}
|
||||
|
||||
token, raw, err := newVerificationToken(accountRef, purpose, target, ttl)
|
||||
if err != nil {
|
||||
db.Logger.Warn("Failed to generate verification token", append(logFields, zap.Error(err))...)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Invalidate any active tokens for the same (accountRef, purpose, target).
|
||||
now := time.Now().UTC()
|
||||
invalidated, err := db.DBImp.PatchMany(ctx,
|
||||
repository.Query().And(
|
||||
repository.Filter("accountRef", accountRef),
|
||||
repository.Filter("purpose", purpose),
|
||||
repository.Filter("target", target),
|
||||
repository.Filter("usedAt", nil),
|
||||
repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now),
|
||||
),
|
||||
repository.Patch().Set(repository.Field("usedAt"), now),
|
||||
)
|
||||
if err != nil {
|
||||
db.Logger.Warn("Failed to invalidate previous tokens", append(logFields, zap.Error(err))...)
|
||||
return "", err
|
||||
}
|
||||
if invalidated > 0 {
|
||||
db.Logger.Debug("Invalidated previous tokens", append(logFields, zap.Int("count", invalidated))...)
|
||||
}
|
||||
|
||||
if err := db.DBImp.Create(ctx, token); err != nil {
|
||||
db.Logger.Warn("Failed to persist verification token", append(logFields, zap.Error(err))...)
|
||||
return "", err
|
||||
}
|
||||
|
||||
db.Logger.Debug("Verification token created", append(logFields, zap.String("hash", token.VerifyTokenHash))...)
|
||||
|
||||
return raw, nil
|
||||
}
|
||||
49
api/pkg/db/internal/mongo/verificationimp/db.go
Normal file
49
api/pkg/db/internal/mongo/verificationimp/db.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package verificationimp
|
||||
|
||||
import (
|
||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||
"github.com/tech/sendico/pkg/db/template"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type verificationDB struct {
|
||||
template.DBImp[*model.VerificationToken]
|
||||
tf transaction.Factory
|
||||
}
|
||||
|
||||
func Create(
|
||||
logger mlogger.Logger,
|
||||
db *mongo.Database,
|
||||
tf transaction.Factory,
|
||||
) (*verificationDB, error) {
|
||||
p := &verificationDB{
|
||||
DBImp: *template.Create[*model.VerificationToken](logger, mservice.VerificationTokens, db),
|
||||
tf: tf,
|
||||
}
|
||||
|
||||
if err := p.Repository.CreateIndex(&ri.Definition{
|
||||
Keys: []ri.Key{{Field: "verifyTokenHash", Sort: ri.Asc}},
|
||||
Unique: true,
|
||||
Name: "unique_token_hash",
|
||||
}); err != nil {
|
||||
p.Logger.Error("Failed to create unique verifyTokenHash index", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ttl := int32(2678400) // 30 days
|
||||
if err := p.Repository.CreateIndex(&ri.Definition{
|
||||
Keys: []ri.Key{{Field: "expiresAt", Sort: ri.Asc}},
|
||||
TTL: &ttl,
|
||||
Name: "ttl_expires_at",
|
||||
}); err != nil {
|
||||
p.Logger.Error("Failed to create TTL index on expiresAt", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
11
api/pkg/db/internal/mongo/verificationimp/hash.go
Normal file
11
api/pkg/db/internal/mongo/verificationimp/hash.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package verificationimp
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
func tokenHash(rawToken string) string {
|
||||
hash := sha256.Sum256([]byte(rawToken))
|
||||
return base64.RawURLEncoding.EncodeToString(hash[:])
|
||||
}
|
||||
621
api/pkg/db/internal/mongo/verificationimp/verification_test.go
Normal file
621
api/pkg/db/internal/mongo/verificationimp/verification_test.go
Normal file
@@ -0,0 +1,621 @@
|
||||
package verificationimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||
rd "github.com/tech/sendico/pkg/db/repository/decoder"
|
||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/db/template"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"github.com/tech/sendico/pkg/db/verification"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func newTestVerificationDB(t *testing.T) *verificationDB {
|
||||
t.Helper()
|
||||
repo := newMemoryTokenRepository()
|
||||
logger := zap.NewNop()
|
||||
return &verificationDB{
|
||||
DBImp: template.DBImp[*model.VerificationToken]{
|
||||
Logger: logger,
|
||||
Repository: repo,
|
||||
},
|
||||
tf: &passthroughTxFactory{},
|
||||
}
|
||||
}
|
||||
|
||||
// passthroughTxFactory executes callbacks directly without a real transaction.
|
||||
type passthroughTxFactory struct{}
|
||||
|
||||
func (*passthroughTxFactory) CreateTransaction() transaction.Transaction { return &passthroughTx{} }
|
||||
|
||||
type passthroughTx struct{}
|
||||
|
||||
func (*passthroughTx) Execute(ctx context.Context, cb transaction.Callback) (any, error) {
|
||||
return cb(ctx)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// in-memory repository for VerificationToken
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type memoryTokenRepository struct {
|
||||
mu sync.Mutex
|
||||
data map[bson.ObjectID]*model.VerificationToken
|
||||
order []bson.ObjectID
|
||||
seq int
|
||||
}
|
||||
|
||||
func newMemoryTokenRepository() *memoryTokenRepository {
|
||||
return &memoryTokenRepository{data: make(map[bson.ObjectID]*model.VerificationToken)}
|
||||
}
|
||||
|
||||
func (m *memoryTokenRepository) Insert(_ context.Context, obj storable.Storable, _ builder.Query) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
tok, ok := obj.(*model.VerificationToken)
|
||||
if !ok {
|
||||
return merrors.InvalidDataType("expected VerificationToken")
|
||||
}
|
||||
id := tok.GetID()
|
||||
if id == nil || *id == bson.NilObjectID {
|
||||
m.seq++
|
||||
tok.SetID(bson.NewObjectID())
|
||||
id = tok.GetID()
|
||||
}
|
||||
if _, exists := m.data[*id]; exists {
|
||||
return merrors.DataConflict("token already exists")
|
||||
}
|
||||
m.data[*id] = cloneToken(tok)
|
||||
m.order = append(m.order, *id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryTokenRepository) Get(_ context.Context, id bson.ObjectID, result storable.Storable) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
tok, ok := m.data[id]
|
||||
if !ok {
|
||||
return merrors.ErrNoData
|
||||
}
|
||||
dst := result.(*model.VerificationToken)
|
||||
*dst = *cloneToken(tok)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryTokenRepository) FindOneByFilter(_ context.Context, query builder.Query, result storable.Storable) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, id := range m.order {
|
||||
tok := m.data[id]
|
||||
if tok != nil && matchToken(query, tok) {
|
||||
dst := result.(*model.VerificationToken)
|
||||
*dst = *cloneToken(tok)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return merrors.ErrNoData
|
||||
}
|
||||
|
||||
func (m *memoryTokenRepository) Update(_ context.Context, obj storable.Storable) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
tok := obj.(*model.VerificationToken)
|
||||
id := tok.GetID()
|
||||
if id == nil {
|
||||
return merrors.InvalidArgument("id required")
|
||||
}
|
||||
if _, exists := m.data[*id]; !exists {
|
||||
return merrors.ErrNoData
|
||||
}
|
||||
m.data[*id] = cloneToken(tok)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryTokenRepository) PatchMany(_ context.Context, filter builder.Query, patch builder.Patch) (int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
patchDoc := patch.Build()
|
||||
count := 0
|
||||
for _, id := range m.order {
|
||||
tok := m.data[id]
|
||||
if tok != nil && matchToken(filter, tok) {
|
||||
applyPatch(tok, patchDoc)
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// stubs — not exercised by verification DB but required by the interface
|
||||
|
||||
func (m *memoryTokenRepository) Aggregate(context.Context, builder.Pipeline, rd.DecodingFunc) error {
|
||||
return merrors.NotImplemented("not needed")
|
||||
}
|
||||
func (m *memoryTokenRepository) InsertMany(ctx context.Context, objs []storable.Storable) error {
|
||||
for _, o := range objs {
|
||||
if err := m.Insert(ctx, o, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *memoryTokenRepository) FindManyByFilter(context.Context, builder.Query, rd.DecodingFunc) error {
|
||||
return merrors.NotImplemented("not needed")
|
||||
}
|
||||
func (m *memoryTokenRepository) Patch(context.Context, bson.ObjectID, builder.Patch) error {
|
||||
return merrors.NotImplemented("not needed")
|
||||
}
|
||||
func (m *memoryTokenRepository) Delete(_ context.Context, id bson.ObjectID) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.data, id)
|
||||
return nil
|
||||
}
|
||||
func (m *memoryTokenRepository) DeleteMany(context.Context, builder.Query) error {
|
||||
return merrors.NotImplemented("not needed")
|
||||
}
|
||||
func (m *memoryTokenRepository) CreateIndex(*ri.Definition) error { return nil }
|
||||
func (m *memoryTokenRepository) ListIDs(context.Context, builder.Query) ([]bson.ObjectID, error) {
|
||||
return nil, merrors.NotImplemented("not needed")
|
||||
}
|
||||
func (m *memoryTokenRepository) ListPermissionBound(context.Context, builder.Query) ([]model.PermissionBoundStorable, error) {
|
||||
return nil, merrors.NotImplemented("not needed")
|
||||
}
|
||||
func (m *memoryTokenRepository) ListAccountBound(context.Context, builder.Query) ([]model.AccountBoundStorable, error) {
|
||||
return nil, merrors.NotImplemented("not needed")
|
||||
}
|
||||
func (m *memoryTokenRepository) Collection() string { return mservice.VerificationTokens }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// bson.D query evaluation for VerificationToken
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// tokenFieldValue returns the stored value for a given BSON field name.
|
||||
func tokenFieldValue(tok *model.VerificationToken, field string) any {
|
||||
switch field {
|
||||
case "verifyTokenHash":
|
||||
return tok.VerifyTokenHash
|
||||
case "usedAt":
|
||||
return tok.UsedAt
|
||||
case "expiresAt":
|
||||
return tok.ExpiresAt
|
||||
case "accountRef":
|
||||
return tok.AccountRef
|
||||
case "purpose":
|
||||
return tok.Purpose
|
||||
case "target":
|
||||
return tok.Target
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// matchToken evaluates a bson.D filter against a token.
|
||||
func matchToken(query builder.Query, tok *model.VerificationToken) bool {
|
||||
if query == nil {
|
||||
return true
|
||||
}
|
||||
return matchBsonD(query.BuildQuery(), tok)
|
||||
}
|
||||
|
||||
func matchBsonD(filter bson.D, tok *model.VerificationToken) bool {
|
||||
for _, elem := range filter {
|
||||
if !matchElem(elem, tok) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func matchElem(elem bson.E, tok *model.VerificationToken) bool {
|
||||
switch elem.Key {
|
||||
|
||||
case "$and":
|
||||
arr, ok := elem.Value.(bson.A)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, sub := range arr {
|
||||
d, ok := sub.(bson.D)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !matchBsonD(d, tok) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
default:
|
||||
// Either a direct field match or a comparison operator doc.
|
||||
stored := tokenFieldValue(tok, elem.Key)
|
||||
|
||||
// Check for operator document like {$gt: value}
|
||||
if opDoc, ok := elem.Value.(bson.M); ok {
|
||||
return matchOperator(stored, opDoc)
|
||||
}
|
||||
|
||||
// Direct equality (including nil check).
|
||||
return valuesEqual(stored, elem.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func matchOperator(stored any, ops bson.M) bool {
|
||||
for op, cmpVal := range ops {
|
||||
switch op {
|
||||
case "$gt":
|
||||
if !timeGt(stored, cmpVal) {
|
||||
return false
|
||||
}
|
||||
case "$lt":
|
||||
if !timeLt(stored, cmpVal) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func valuesEqual(a, b any) bool {
|
||||
// nil checks: usedAt == nil
|
||||
if b == nil {
|
||||
return a == nil || a == (*time.Time)(nil)
|
||||
}
|
||||
switch av := a.(type) {
|
||||
case *time.Time:
|
||||
if av == nil {
|
||||
return b == nil
|
||||
}
|
||||
if bv, ok := b.(*time.Time); ok {
|
||||
return av.Equal(*bv)
|
||||
}
|
||||
return false
|
||||
case bson.ObjectID:
|
||||
if bv, ok := b.(bson.ObjectID); ok {
|
||||
return av == bv
|
||||
}
|
||||
return false
|
||||
case model.VerificationPurpose:
|
||||
if bv, ok := b.(model.VerificationPurpose); ok {
|
||||
return av == bv
|
||||
}
|
||||
return false
|
||||
case string:
|
||||
if bv, ok := b.(string); ok {
|
||||
return av == bv
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func timeGt(stored, cmpVal any) bool {
|
||||
st, ok := toTime(stored)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
ct, ok := toTime(cmpVal)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return st.After(ct)
|
||||
}
|
||||
|
||||
func timeLt(stored, cmpVal any) bool {
|
||||
st, ok := toTime(stored)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
ct, ok := toTime(cmpVal)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return st.Before(ct)
|
||||
}
|
||||
|
||||
func toTime(v any) (time.Time, bool) {
|
||||
switch tv := v.(type) {
|
||||
case time.Time:
|
||||
return tv, true
|
||||
case *time.Time:
|
||||
if tv == nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return *tv, true
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
// applyPatch applies $set operations from a patch bson.D to a token.
|
||||
func applyPatch(tok *model.VerificationToken, patchDoc bson.D) {
|
||||
for _, op := range patchDoc {
|
||||
if op.Key != "$set" {
|
||||
continue
|
||||
}
|
||||
fields, ok := op.Value.(bson.D)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, f := range fields {
|
||||
switch f.Key {
|
||||
case "usedAt":
|
||||
if t, ok := f.Value.(time.Time); ok {
|
||||
tok.UsedAt = &t
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cloneToken(src *model.VerificationToken) *model.VerificationToken {
|
||||
dst := *src
|
||||
if src.UsedAt != nil {
|
||||
t := *src.UsedAt
|
||||
dst.UsedAt = &t
|
||||
}
|
||||
return &dst
|
||||
}
|
||||
|
||||
// allTokens returns every stored token for inspection in tests.
|
||||
func (m *memoryTokenRepository) allTokens() []*model.VerificationToken {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
out := make([]*model.VerificationToken, 0, len(m.data))
|
||||
for _, id := range m.order {
|
||||
if tok, ok := m.data[id]; ok {
|
||||
out = append(out, cloneToken(tok))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCreate_ReturnsRawToken(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
raw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, raw)
|
||||
}
|
||||
|
||||
func TestCreate_TokenCanBeConsumed(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
raw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
tok, err := db.Consume(ctx, raw)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, accountRef, tok.AccountRef)
|
||||
assert.Equal(t, model.PurposePasswordReset, tok.Purpose)
|
||||
assert.NotNil(t, tok.UsedAt)
|
||||
}
|
||||
|
||||
func TestConsume_ReturnsCorrectFields(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
raw, err := db.Create(ctx, accountRef, model.PurposeEmailChange, "new@example.com", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
tok, err := db.Consume(ctx, raw)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, accountRef, tok.AccountRef)
|
||||
assert.Equal(t, model.PurposeEmailChange, tok.Purpose)
|
||||
assert.Equal(t, "new@example.com", tok.Target)
|
||||
}
|
||||
|
||||
func TestConsume_SecondConsumeFailsAlreadyUsed(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
raw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Consume(ctx, raw)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Consume(ctx, raw)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed),
|
||||
"second consume should fail because usedAt is set")
|
||||
}
|
||||
|
||||
func TestConsume_ExpiredTokenFails(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
// Create with a TTL that is already in the past.
|
||||
raw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", -time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Consume(ctx, raw)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, verification.ErrTokenExpired),
|
||||
"expired token should not be consumable")
|
||||
}
|
||||
|
||||
func TestConsume_UnknownTokenFails(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := db.Consume(ctx, "nonexistent-token-value")
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, verification.ErrTokenNotFound))
|
||||
}
|
||||
|
||||
func TestCreate_InvalidatesPreviousToken(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
oldRaw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
newRaw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, oldRaw, newRaw, "new token should differ from old one")
|
||||
|
||||
// Old token is no longer consumable.
|
||||
_, err = db.Consume(ctx, oldRaw)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed),
|
||||
"old token should be invalidated (usedAt set) after new token creation")
|
||||
|
||||
// New token works fine.
|
||||
tok, err := db.Consume(ctx, newRaw)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, accountRef, tok.AccountRef)
|
||||
}
|
||||
|
||||
func TestCreate_InvalidatesMultiplePreviousTokens(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
first, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
second, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
third, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Consume(ctx, first)
|
||||
assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed), "first should be invalidated")
|
||||
_, err = db.Consume(ctx, second)
|
||||
assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed), "second should be invalidated")
|
||||
|
||||
tok, err := db.Consume(ctx, third)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, accountRef, tok.AccountRef)
|
||||
}
|
||||
|
||||
func TestCreate_DifferentPurposeNotInvalidated(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
resetRaw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Creating an activation token should NOT invalidate the password-reset token.
|
||||
_, err = db.Create(ctx, accountRef, model.PurposeAccountActivation, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
tok, err := db.Consume(ctx, resetRaw)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, model.PurposePasswordReset, tok.Purpose)
|
||||
}
|
||||
|
||||
func TestCreate_DifferentTargetNotInvalidated(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
firstRaw, err := db.Create(ctx, accountRef, model.PurposeEmailChange, "a@example.com", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Creating a token for a different target email should NOT invalidate the first.
|
||||
_, err = db.Create(ctx, accountRef, model.PurposeEmailChange, "b@example.com", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
tok, err := db.Consume(ctx, firstRaw)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "a@example.com", tok.Target)
|
||||
}
|
||||
|
||||
func TestCreate_DifferentAccountNotInvalidated(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
account1 := bson.NewObjectID()
|
||||
account2 := bson.NewObjectID()
|
||||
|
||||
raw1, err := db.Create(ctx, account1, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Create(ctx, account2, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
tok, err := db.Consume(ctx, raw1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, account1, tok.AccountRef)
|
||||
}
|
||||
|
||||
func TestCreate_AlreadyUsedTokenNotInvalidatedAgain(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
// Create and consume first token.
|
||||
raw1, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
_, err = db.Consume(ctx, raw1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create second — the already-consumed token should have usedAt set,
|
||||
// so the invalidation query (usedAt == nil) should skip it.
|
||||
// This tests that the PatchMany filter correctly excludes already-used tokens.
|
||||
raw2, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
tok, err := db.Consume(ctx, raw2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, accountRef, tok.AccountRef)
|
||||
}
|
||||
|
||||
func TestCreate_ExpiredTokenNotInvalidated(t *testing.T) {
|
||||
db := newTestVerificationDB(t)
|
||||
ctx := context.Background()
|
||||
accountRef := bson.NewObjectID()
|
||||
|
||||
// Create a token that is already expired.
|
||||
_, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", -time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a fresh one — invalidation should skip the expired token (expiresAt > now filter).
|
||||
raw2, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
tok, err := db.Consume(ctx, raw2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, accountRef, tok.AccountRef)
|
||||
}
|
||||
|
||||
func TestTokenHash_Deterministic(t *testing.T) {
|
||||
h1 := tokenHash("same-input")
|
||||
h2 := tokenHash("same-input")
|
||||
assert.Equal(t, h1, h2)
|
||||
}
|
||||
|
||||
func TestTokenHash_DifferentInputs(t *testing.T) {
|
||||
h1 := tokenHash("input-a")
|
||||
h2 := tokenHash("input-b")
|
||||
assert.NotEqual(t, h1, h2)
|
||||
}
|
||||
28
api/pkg/db/verification/errors.go
Normal file
28
api/pkg/db/verification/errors.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package verification
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTokenNotFound = errors.New("vtNotFound")
|
||||
ErrTokenAlreadyUsed = errors.New("vtAlreadyUsed")
|
||||
ErrTokenExpired = errors.New("vtExpired")
|
||||
)
|
||||
|
||||
func wrap(err error, msg string) error {
|
||||
return fmt.Errorf("%w: %s", err, msg)
|
||||
}
|
||||
|
||||
func ErorrTokenNotFound() error {
|
||||
return wrap(ErrTokenNotFound, "verification token not found")
|
||||
}
|
||||
|
||||
func ErorrTokenAlreadyUsed() error {
|
||||
return wrap(ErrTokenAlreadyUsed, "verification token has already been used")
|
||||
}
|
||||
|
||||
func ErorrTokenExpired() error {
|
||||
return wrap(ErrTokenExpired, "verification token expired")
|
||||
}
|
||||
21
api/pkg/db/verification/verification.go
Normal file
21
api/pkg/db/verification/verification.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package verification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type DB interface {
|
||||
// template.DB[*model.VerificationToken]
|
||||
Create(
|
||||
ctx context.Context,
|
||||
accountRef bson.ObjectID,
|
||||
purpose model.VerificationPurpose,
|
||||
target string,
|
||||
ttl time.Duration,
|
||||
) (rawToken string, err error)
|
||||
Consume(ctx context.Context, rawToken string) (*model.VerificationToken, error)
|
||||
}
|
||||
@@ -45,7 +45,7 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -89,7 +89,7 @@ require (
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
|
||||
@@ -70,8 +70,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rH
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -242,8 +242,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
|
||||
@@ -32,7 +32,7 @@ func InvalidArgument(msg string, argumentNames ...string) error {
|
||||
return fmt.Errorf("%w: %s", ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...))
|
||||
}
|
||||
|
||||
var ErrDataConflict = errors.New("DataConflict")
|
||||
var ErrDataConflict = errors.New("dataConflict")
|
||||
|
||||
func DataConflict(msg string) error {
|
||||
return fmt.Errorf("%w: %s", ErrDataConflict, msg)
|
||||
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
|
||||
type AccountNotification struct {
|
||||
messaging.Envelope
|
||||
accountRef bson.ObjectID
|
||||
accountRef bson.ObjectID
|
||||
verificationToken string
|
||||
}
|
||||
|
||||
func (acn *AccountNotification) Serialize() ([]byte, error) {
|
||||
var msg gmessaging.AccountCreatedEvent
|
||||
msg.AccountRef = acn.accountRef.Hex()
|
||||
msg.VerificationToken = acn.verificationToken
|
||||
data, err := proto.Marshal(&msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -29,9 +31,10 @@ func NewAccountNotification(action nm.NotificationAction) model.NotificationEven
|
||||
return model.NewNotification(mservice.Accounts, action)
|
||||
}
|
||||
|
||||
func NewAccountImp(sender string, accountRef bson.ObjectID, action nm.NotificationAction) messaging.Envelope {
|
||||
func NewAccountImp(sender string, accountRef bson.ObjectID, action nm.NotificationAction, verificationToken string) messaging.Envelope {
|
||||
return &AccountNotification{
|
||||
Envelope: messaging.CreateEnvelope(sender, NewAccountNotification(action)),
|
||||
accountRef: accountRef,
|
||||
Envelope: messaging.CreateEnvelope(sender, NewAccountNotification(action)),
|
||||
accountRef: accountRef,
|
||||
verificationToken: verificationToken,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +34,13 @@ func (acnp *AccoountNotificaionProcessor) Process(ctx context.Context, envelope
|
||||
acnp.logger.Warn("Failed to restore object ID", zap.Error(err), zap.String("topic", acnp.event.ToString()), zap.String("account_ref", msg.AccountRef))
|
||||
return err
|
||||
}
|
||||
verificationToken := msg.GetVerificationToken()
|
||||
var account model.Account
|
||||
if err := acnp.db.Get(ctx, accountRef, &account); err != nil {
|
||||
acnp.logger.Warn("Failed to fetch account", zap.Error(err), zap.String("topic", acnp.event.ToString()), zap.String("account_ref", msg.AccountRef))
|
||||
return err
|
||||
}
|
||||
return acnp.handler(ctx, &account)
|
||||
return acnp.handler(ctx, &account, verificationToken)
|
||||
}
|
||||
|
||||
func (acnp *AccoountNotificaionProcessor) GetSubject() model.NotificationEvent {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
messaging "github.com/tech/sendico/pkg/messaging/envelope"
|
||||
gmessaging "github.com/tech/sendico/pkg/generated/gmessaging"
|
||||
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"
|
||||
@@ -16,10 +16,10 @@ type NResultNotification struct {
|
||||
|
||||
func (nrn *NResultNotification) Serialize() ([]byte, error) {
|
||||
msg := gmessaging.NotificationSentEvent{
|
||||
UserID: nrn.result.UserID,
|
||||
UserId: nrn.result.UserID,
|
||||
Channel: nrn.result.Channel,
|
||||
Locale: nrn.result.Locale,
|
||||
TemplateID: nrn.result.TemplateID,
|
||||
TemplateId: nrn.result.TemplateID,
|
||||
Status: &gmessaging.OperationResult{
|
||||
IsSuccessful: nrn.result.Result.IsSuccessful,
|
||||
ErrorDescription: nrn.result.Result.Error,
|
||||
|
||||
@@ -3,8 +3,8 @@ package notifications
|
||||
import (
|
||||
"context"
|
||||
|
||||
me "github.com/tech/sendico/pkg/messaging/envelope"
|
||||
gmessaging "github.com/tech/sendico/pkg/generated/gmessaging"
|
||||
me "github.com/tech/sendico/pkg/messaging/envelope"
|
||||
nh "github.com/tech/sendico/pkg/messaging/notifications/notification/handler"
|
||||
np "github.com/tech/sendico/pkg/messaging/notifications/processor"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
@@ -27,10 +27,10 @@ func (nrp *NResultNotificaionProcessor) Process(ctx context.Context, envelope me
|
||||
}
|
||||
nresult := &model.NotificationResult{
|
||||
AmpliEvent: model.AmpliEvent{
|
||||
UserID: msg.UserID,
|
||||
UserID: msg.UserId,
|
||||
},
|
||||
Channel: msg.Channel,
|
||||
TemplateID: msg.TemplateID,
|
||||
TemplateID: msg.TemplateId,
|
||||
Locale: msg.Locale,
|
||||
Result: model.OperationResult{
|
||||
IsSuccessful: msg.Status.IsSuccessful,
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func Account(sender string, accountRef bson.ObjectID, action nm.NotificationAction) messaging.Envelope {
|
||||
return an.NewAccountImp(sender, accountRef, action)
|
||||
func Account(sender string, accountRef bson.ObjectID, action nm.NotificationAction, verificationToken string) messaging.Envelope {
|
||||
return an.NewAccountImp(sender, accountRef, action, verificationToken)
|
||||
}
|
||||
|
||||
func AccountCreated(sender string, accountRef bson.ObjectID) messaging.Envelope {
|
||||
return Account(sender, accountRef, nm.NACreated)
|
||||
func AccountCreated(sender string, accountRef bson.ObjectID, verificationToken string) messaging.Envelope {
|
||||
return Account(sender, accountRef, nm.NACreated, verificationToken)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ import (
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type AccountHandler = func(context.Context, *model.Account) error
|
||||
type AccountHandler = func(context.Context, *model.Account, string) error
|
||||
|
||||
type PasswordResetHandler = func(context.Context, *model.Account, string) error
|
||||
|
||||
@@ -9,6 +9,14 @@ import (
|
||||
|
||||
type Filter int
|
||||
|
||||
type AccountStatus string
|
||||
|
||||
const (
|
||||
AccountPendingVerification AccountStatus = "pending_verification"
|
||||
AccountActive AccountStatus = "active"
|
||||
AccountBlocked AccountStatus = "blocked"
|
||||
)
|
||||
|
||||
type AccountBase struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
ArchivableBase `bson:",inline" json:",inline"`
|
||||
@@ -27,11 +35,21 @@ type AccountPublic struct {
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
AccountPublic `bson:",inline" json:",inline"`
|
||||
EmailBackup string `bson:"emailBackup" json:"emailBackup"`
|
||||
Password string `bson:"password" json:"password"`
|
||||
ResetPasswordToken string `bson:"resetPasswordToken" json:"resetPasswordToken"`
|
||||
VerifyToken string `bson:"verifyToken" json:"verifyToken"`
|
||||
AccountPublic `bson:",inline" json:",inline"`
|
||||
Password string `bson:"password" json:"-"` // password hash
|
||||
Status AccountStatus `bson:"status" json:"-"`
|
||||
}
|
||||
|
||||
func (a *Account) Copy() *Account {
|
||||
return &Account{
|
||||
AccountPublic: a.AccountPublic,
|
||||
Password: a.Password,
|
||||
Status: a.Status,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Account) IsActive() bool {
|
||||
return a.Status == AccountActive
|
||||
}
|
||||
|
||||
func (a *Account) HashPassword() error {
|
||||
|
||||
30
api/pkg/model/verificaton.go
Normal file
30
api/pkg/model/verificaton.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type VerificationPurpose string
|
||||
|
||||
const (
|
||||
PurposeAccountActivation VerificationPurpose = "account_activation"
|
||||
PurposeEmailChange VerificationPurpose = "email_change"
|
||||
PurposePasswordReset VerificationPurpose = "password_reset"
|
||||
PurposeSensitiveAction VerificationPurpose = "sensitive_action"
|
||||
PurposeMagicLogin VerificationPurpose = "magic_login"
|
||||
)
|
||||
|
||||
type VerificationToken struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
ArchivableBase `bson:",inline" json:",inline"`
|
||||
|
||||
Target string `bson:"target,omitempty" json:"target"`
|
||||
AccountRef bson.ObjectID `bson:"accountRef" json:"accountRef"`
|
||||
Purpose VerificationPurpose `bson:"purpose" json:"purpose"`
|
||||
VerifyTokenHash string `bson:"verifyTokenHash" json:"-"`
|
||||
UsedAt *time.Time `bson:"usedAt,omitempty" json:"-"`
|
||||
ExpiresAt time.Time `bson:"expiresAt" json:"-"`
|
||||
}
|
||||
@@ -50,6 +50,7 @@ const (
|
||||
Storage Type = "storage" // Represents statuses of tasks or projects
|
||||
TgSettle Type = "tgsettle_gateway" // Represents tg settlement gateway
|
||||
Tenants Type = "tenants" // Represents tenants managed in the system
|
||||
VerificationTokens Type = "verification_tokens" //Represents verification tokens managed in the system
|
||||
Wallets Type = "wallets" // Represents workflows for tasks or projects
|
||||
Workflows Type = "workflows" // Represents workflows for tasks or projects
|
||||
)
|
||||
|
||||
@@ -13,3 +13,7 @@ func ObjRef(name string, objRef bson.ObjectID) zap.Field {
|
||||
func StorableRef(obj storable.Storable) zap.Field {
|
||||
return ObjRef(obj.Collection()+"_ref", *obj.GetID())
|
||||
}
|
||||
|
||||
func AccRef(accountRef bson.ObjectID) zap.Field {
|
||||
return ObjRef("account_ref", accountRef)
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ syntax = "proto3";
|
||||
option go_package = "github.com/tech/sendico/pkg/generated/gmessaging";
|
||||
|
||||
message AccountCreatedEvent {
|
||||
string AccountRef = 1;
|
||||
string account_ref = 1;
|
||||
string verification_token = 2;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ option go_package = "github.com/tech/sendico/pkg/generated/gmessaging";
|
||||
import "operation_result.proto";
|
||||
|
||||
message NotificationSentEvent {
|
||||
string UserID = 1;
|
||||
string TemplateID = 2;
|
||||
string Channel = 3;
|
||||
string Locale = 4;
|
||||
OperationResult Status = 5;
|
||||
string user_id = 1;
|
||||
string template_id = 2;
|
||||
string channel = 3;
|
||||
string locale = 4;
|
||||
OperationResult status = 5;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@ syntax = "proto3";
|
||||
option go_package = "github.com/tech/sendico/pkg/generated/gmessaging";
|
||||
|
||||
message ObjectUpdatedEvent {
|
||||
string ObjectRef = 1;
|
||||
string ActorAccountRef = 2;
|
||||
string object_ref = 1;
|
||||
string actor_account_ref = 2;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@ syntax = "proto3";
|
||||
option go_package = "github.com/tech/sendico/pkg/generated/gmessaging";
|
||||
|
||||
message OperationResult {
|
||||
bool IsSuccessful = 1;
|
||||
string ErrorDescription = 2;
|
||||
bool is_successful = 1;
|
||||
string error_description = 2;
|
||||
}
|
||||
@@ -3,6 +3,6 @@ syntax = "proto3";
|
||||
option go_package = "github.com/tech/sendico/pkg/generated/gmessaging";
|
||||
|
||||
message PasswordResetEvent {
|
||||
string AccountRef = 1;
|
||||
string ResetToken = 2;
|
||||
string account_ref = 1;
|
||||
string reset_token = 2;
|
||||
}
|
||||
@@ -84,7 +84,7 @@ require (
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||
@@ -136,7 +136,7 @@ require (
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
)
|
||||
|
||||
@@ -123,8 +123,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -332,8 +332,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
@@ -13,13 +14,12 @@ import (
|
||||
"github.com/tech/sendico/pkg/db/account"
|
||||
"github.com/tech/sendico/pkg/db/organization"
|
||||
"github.com/tech/sendico/pkg/db/policy"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"github.com/tech/sendico/pkg/db/verification"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
"github.com/tech/sendico/server/internal/mutil/flrstring"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -31,9 +31,9 @@ type service struct {
|
||||
enforcer auth.Enforcer
|
||||
roleManager management.Role
|
||||
config *middleware.PasswordConfig
|
||||
tf transaction.Factory
|
||||
|
||||
policyDB policy.DB
|
||||
vdb verification.DB
|
||||
}
|
||||
|
||||
func validateUserRequest(u *model.Account) error {
|
||||
@@ -112,7 +112,6 @@ func (s *service) ValidateAccount(acct *model.Account) error {
|
||||
return err
|
||||
}
|
||||
|
||||
acct.VerifyToken = flrstring.CreateRandString(s.config.TokenLength)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -121,32 +120,50 @@ func (s *service) CreateAccount(
|
||||
org *model.Organization,
|
||||
acct *model.Account,
|
||||
roleDescID bson.ObjectID,
|
||||
) error {
|
||||
) (string, error) {
|
||||
if org == nil {
|
||||
return merrors.InvalidArgument("Organization must not be nil")
|
||||
return "", merrors.InvalidArgument("Organization must not be nil")
|
||||
}
|
||||
if acct == nil || len(acct.Login) == 0 {
|
||||
return merrors.InvalidArgument("Account must have a non-empty login")
|
||||
return "", merrors.InvalidArgument("Account must have a non-empty login")
|
||||
}
|
||||
if roleDescID == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("Role description must be provided")
|
||||
return "", merrors.InvalidArgument("Role description must be provided")
|
||||
}
|
||||
|
||||
// 1) Create the account
|
||||
acct.Status = model.AccountPendingVerification
|
||||
if err := s.accountDB.Create(ctx, acct); err != nil {
|
||||
if errors.Is(err, merrors.ErrDataConflict) {
|
||||
s.logger.Info("Username is already taken", zap.String("login", acct.Login))
|
||||
} else {
|
||||
s.logger.Warn("Failed to signup a user", zap.Error(err), zap.String("login", acct.Login))
|
||||
}
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 2) Add to organization
|
||||
if err := s.JoinOrganization(ctx, org, acct, roleDescID); err != nil {
|
||||
s.logger.Warn("Failed to register new organization member", zap.Error(err), mzap.StorableRef(acct))
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
return nil
|
||||
|
||||
// 3) Issue verification token
|
||||
return s.VerifyAccount(ctx, acct)
|
||||
}
|
||||
|
||||
func (s *service) VerifyAccount(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
) (verificationToken string, err error) {
|
||||
verificationToken, err = s.vdb.Create(ctx, *acct.GetID(), model.PurposeAccountActivation, "", time.Duration(time.Hour*24))
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to create verification token for new account", zap.Error(err), mzap.StorableRef(acct))
|
||||
return "", err
|
||||
}
|
||||
|
||||
return verificationToken, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *service) DeleteAccount(
|
||||
@@ -213,20 +230,16 @@ func (s *service) RemoveAccountFromOrganization(
|
||||
func (s *service) ResetPassword(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
) error {
|
||||
acct.ResetPasswordToken = flrstring.CreateRandString(s.config.TokenLength)
|
||||
return s.accountDB.Update(ctx, acct)
|
||||
) (string, error) {
|
||||
return s.vdb.Create(ctx, *acct.GetID(), model.PurposePasswordReset, "", time.Duration(time.Hour*1))
|
||||
}
|
||||
|
||||
func (s *service) UpdateLogin(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
newLogin string,
|
||||
) error {
|
||||
acct.EmailBackup = acct.Login
|
||||
acct.Login = newLogin
|
||||
acct.VerifyToken = flrstring.CreateRandString(s.config.TokenLength)
|
||||
return s.accountDB.Update(ctx, acct)
|
||||
) (string, error) {
|
||||
return s.vdb.Create(ctx, *acct.GetID(), model.PurposeEmailChange, newLogin, time.Duration(time.Hour*1))
|
||||
}
|
||||
|
||||
func (s *service) JoinOrganization(
|
||||
@@ -311,27 +324,19 @@ func (s *service) DeleteOrganization(
|
||||
s.logger.Info("Starting organization deletion", mzap.StorableRef(org))
|
||||
|
||||
// Use transaction to ensure atomicity
|
||||
_, err := s.tf.CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
|
||||
// 8. Delete all roles and role descriptions in the organization
|
||||
if err := s.deleteOrganizationRoles(ctx, org.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 8. Delete all roles and role descriptions in the organization
|
||||
if err := s.deleteOrganizationRoles(ctx, org.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 9. Delete all policies in the organization
|
||||
if err := s.deleteOrganizationPolicies(ctx, org.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 9. Delete all policies in the organization
|
||||
if err := s.deleteOrganizationPolicies(ctx, org.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 10. Finally, delete the organization itself
|
||||
if err := s.orgDB.Delete(ctx, bson.NilObjectID, org.ID); err != nil {
|
||||
s.logger.Warn("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
|
||||
// 10. Finally, delete the organization itself
|
||||
if err := s.orgDB.Delete(ctx, bson.NilObjectID, org.ID); err != nil {
|
||||
s.logger.Warn("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -347,23 +352,14 @@ func (s *service) DeleteAll(
|
||||
s.logger.Info("Starting complete deletion (organization + account)",
|
||||
mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
|
||||
|
||||
// Use transaction to ensure atomicity
|
||||
_, err := s.tf.CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
|
||||
// 1. First delete the organization and all its data
|
||||
if err := s.DeleteOrganization(ctx, org); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 1. First delete the organization and all its data
|
||||
if err := s.DeleteOrganization(ctx, org); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Then delete the account
|
||||
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
|
||||
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to delete all data", zap.Error(err), mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
|
||||
// 2. Then delete the account
|
||||
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
|
||||
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -390,7 +386,6 @@ func NewAccountService(
|
||||
enforcer: enforcer,
|
||||
roleManager: ra,
|
||||
config: config,
|
||||
tf: dbf.TransactionFactory(),
|
||||
}
|
||||
var err error
|
||||
if res.accountDB, err = dbf.NewAccountDB(); err != nil {
|
||||
@@ -407,6 +402,9 @@ func NewAccountService(
|
||||
logger.Warn("Failed to create policies database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.vdb, err = dbf.NewVerificationsDB(); err != nil {
|
||||
logger.Warn("Failed to create verification database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -113,8 +113,6 @@ func TestValidateAccount(t *testing.T) {
|
||||
|
||||
// Password should be hashed after validation
|
||||
assert.NotEqual(t, originalPassword, account.Password)
|
||||
assert.NotEmpty(t, account.VerifyToken)
|
||||
assert.Equal(t, config.TokenLength, len(account.VerifyToken))
|
||||
})
|
||||
|
||||
t.Run("AccountMissingName", func(t *testing.T) {
|
||||
@@ -245,54 +243,3 @@ func TestPasswordConfiguration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestTokenGeneration verifies that verification tokens are generated with correct length
|
||||
func TestTokenGeneration(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
tokenLength int
|
||||
}{
|
||||
{"Short", 8},
|
||||
{"Medium", 32},
|
||||
{"Long", 64},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
config := &apiconfig.PasswordConfig{
|
||||
Check: apiconfig.PasswordChecks{
|
||||
MinLength: 8,
|
||||
Digit: true,
|
||||
Upper: true,
|
||||
Lower: true,
|
||||
Special: true,
|
||||
},
|
||||
TokenLength: tc.tokenLength,
|
||||
}
|
||||
|
||||
logger := zap.NewNop() // Use no-op logger for tests
|
||||
service := &service{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
AccountBase: model.AccountBase{
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
},
|
||||
Password: "TestPassword123!",
|
||||
}
|
||||
|
||||
err := service.ValidateAccount(account)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.tokenLength, len(account.VerifyToken))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ type AccountService interface {
|
||||
ResetPassword(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
) error
|
||||
) (verificationToken string, err error)
|
||||
|
||||
// CreateAccount will:
|
||||
// 1) create the account
|
||||
@@ -46,7 +46,12 @@ type AccountService interface {
|
||||
org *model.Organization,
|
||||
acct *model.Account,
|
||||
roleDescID bson.ObjectID,
|
||||
) error
|
||||
) (verificationToken string, err error)
|
||||
|
||||
VerifyAccount(
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
) (verificationToken string, err error)
|
||||
|
||||
JoinOrganization(
|
||||
ctx context.Context,
|
||||
@@ -59,7 +64,7 @@ type AccountService interface {
|
||||
ctx context.Context,
|
||||
acct *model.Account,
|
||||
newLogin string,
|
||||
) error
|
||||
) (verificationToken string, err error)
|
||||
|
||||
// DeleteAccount deletes the account and removes it from the org.
|
||||
DeleteAccount(
|
||||
|
||||
@@ -33,7 +33,7 @@ func (pr *PublicRouter) logUserIn(ctx context.Context, _ *http.Request, req *sre
|
||||
return response.Internal(pr.logger, pr.service, err)
|
||||
}
|
||||
|
||||
if account.VerifyToken != "" {
|
||||
if !account.IsActive() {
|
||||
return response.Forbidden(pr.logger, pr.service, "account_not_verified", "Account verification required")
|
||||
}
|
||||
|
||||
|
||||
@@ -43,12 +43,8 @@ func (a *AccountAPI) getProfile(_ *http.Request, u *model.Account, token *srespo
|
||||
return sresponse.Account(a.logger, u, token)
|
||||
}
|
||||
|
||||
func (a *AccountAPI) reportTokenNotFound() http.HandlerFunc {
|
||||
return response.NotFound(a.logger, a.Name(), "No account found associated with given verifcation token")
|
||||
}
|
||||
|
||||
func (a *AccountAPI) sendWelcomeEmail(account *model.Account) error {
|
||||
if err := a.producer.SendMessage(an.AccountCreated(a.Name(), *account.GetID())); err != nil {
|
||||
func (a *AccountAPI) sendWelcomeEmail(account *model.Account, token string) error {
|
||||
if err := a.producer.SendMessage(an.AccountCreated(a.Name(), *account.GetID(), token)); err != nil {
|
||||
a.logger.Warn("Failed to send account creation notification", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
@@ -63,27 +59,23 @@ func (a *AccountAPI) sendVerificationMail(r *http.Request, paramGetter func(ctx
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
// Get the account
|
||||
// accnt, err := a.db.GetByEmail(ctx, paramGetter(u))
|
||||
// if err != nil || accnt == nil {
|
||||
// a.logger.Warn("Failed to ger user from db with", zap.Error(err), mzap.StorableRef(u))
|
||||
// return response.Internal(a.logger, a.Name(), err)
|
||||
// }
|
||||
accnt, err := paramGetter(r.Context(), a.db, u)
|
||||
ctx := r.Context()
|
||||
accnt, err := paramGetter(ctx, a.db, u)
|
||||
if err != nil || accnt == nil {
|
||||
a.logger.Warn("Failed to ger user from db with", zap.Error(err), mzap.StorableRef(u))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
if accnt.VerifyToken == "" {
|
||||
a.logger.Debug("Verification token is empty", zap.Error(err), mzap.StorableRef(u))
|
||||
return a.reportTokenNotFound()
|
||||
token, err := a.accService.VerifyAccount(ctx, accnt)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to create verification token for account", zap.Error(err), mzap.StorableRef(accnt))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
// Send welcome email
|
||||
if err = a.sendWelcomeEmail(accnt); err != nil {
|
||||
if err = a.sendWelcomeEmail(accnt, token); err != nil {
|
||||
a.logger.Warn("Failed to send verification email",
|
||||
zap.Error(err), mzap.StorableRef(u), zap.String("email", accnt.Login))
|
||||
zap.Error(err), mzap.StorableRef(accnt), zap.String("email", accnt.Login))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
return response.Success(a.logger)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package accountapiimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
@@ -108,8 +109,14 @@ func (a *AccountAPI) deleteAll(r *http.Request, account *model.Account, token *s
|
||||
}
|
||||
|
||||
// Delete everything (organization + account)
|
||||
if err := a.accService.DeleteAll(ctx, &org, account.ID); err != nil {
|
||||
a.logger.Error("Failed to delete all data", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account))
|
||||
if _, err := a.tf.CreateTransaction().Execute(ctx, func(c context.Context) (any, error) {
|
||||
if err := a.accService.DeleteAll(c, &org, account.ID); err != nil {
|
||||
a.logger.Warn("Failed to delete all data", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account))
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}); err != nil {
|
||||
a.logger.Warn("Failed to execute delete transaction", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -15,19 +16,30 @@ func (a *AccountAPI) verify(r *http.Request) http.HandlerFunc {
|
||||
token := mutil.GetToken(r)
|
||||
// Get user
|
||||
ctx := r.Context()
|
||||
user, err := a.db.GetByToken(ctx, token)
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
a.logger.Debug("Verification token not found", zap.Error(err))
|
||||
return a.reportTokenNotFound()
|
||||
}
|
||||
// Delete verification token to confirm account
|
||||
t, err := a.vdb.Consume(ctx, token)
|
||||
if err != nil {
|
||||
a.logger.Debug("Failed to consume verification token", zap.Error(err))
|
||||
return a.mapTokenErrorToResponse(err)
|
||||
}
|
||||
|
||||
if t.Purpose != model.PurposeAccountActivation {
|
||||
a.logger.Warn("Invalid token purpose", zap.String("expected", string(model.PurposeAccountActivation)), zap.String("actual", string(t.Purpose)))
|
||||
return response.DataConflict(a.logger, a.Name(), "Invalid token purpose")
|
||||
}
|
||||
|
||||
var user model.Account
|
||||
if err := a.db.Get(ctx, t.AccountRef, &user); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
a.logger.Debug("Verified user not found", zap.Error(err))
|
||||
return response.NotFound(a.logger, a.Name(), "User not found")
|
||||
}
|
||||
a.logger.Warn("Failed to fetch account", zap.Error(err))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
// Delete verification token to confirm account
|
||||
user.VerifyToken = ""
|
||||
if err = a.db.Update(ctx, user); err != nil {
|
||||
user.Status = model.AccountActive
|
||||
if err = a.db.Update(ctx, &user); err != nil {
|
||||
a.logger.Warn("Failed to save account while verifying account", zap.Error(err))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ func (a *AccountAPI) updateEmployee(r *http.Request, account *model.Account, tok
|
||||
}
|
||||
if acc.Login != u.Login {
|
||||
// Change email address
|
||||
if err := a.accService.UpdateLogin(ctx, &acc, u.Login); err != nil {
|
||||
verificationToken, err := a.accService.UpdateLogin(ctx, &acc, u.Login)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrDataConflict) {
|
||||
a.logger.Debug("Duplicate login, denying change...", zap.Error(err), mzap.ObjRef("employee_ref", u.ID))
|
||||
return a.reportDuplicateEmail()
|
||||
@@ -65,7 +66,7 @@ func (a *AccountAPI) updateEmployee(r *http.Request, account *model.Account, tok
|
||||
}
|
||||
|
||||
// Send verification email
|
||||
if err = a.sendWelcomeEmail(&acc); err != nil {
|
||||
if err = a.sendWelcomeEmail(&acc, verificationToken); err != nil {
|
||||
a.logger.Warn("Failed to send verification email", zap.Error(err), mzap.StorableRef(&acc))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
@@ -84,13 +84,14 @@ func (a *AccountAPI) forgotPassword(r *http.Request) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// Generate reset password token
|
||||
if err := a.accService.ResetPassword(ctx, user); err != nil {
|
||||
verificationToken, err := a.accService.ResetPassword(ctx, user)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to generate reset password token", zap.Error(err), mzap.StorableRef(user))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
// Send reset password email
|
||||
if err = a.sendPasswordResetEmail(user, user.ResetPasswordToken); err != nil {
|
||||
if err = a.sendPasswordResetEmail(user, verificationToken); err != nil {
|
||||
a.logger.Warn("Failed to send reset password email", zap.Error(err), mzap.StorableRef(user))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
@@ -127,15 +128,20 @@ func (a *AccountAPI) resetPassword(r *http.Request) http.HandlerFunc {
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
// Validate reset token
|
||||
if user.ResetPasswordToken == "" {
|
||||
a.logger.Debug("No reset token found for user", mzap.StorableRef(&user))
|
||||
return response.BadRequest(a.logger, a.Name(), "no_reset_token", "No password reset token found for this user")
|
||||
t, err := a.vdb.Consume(ctx, token)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to consume password reset token", zap.Error(err), zap.String("token", token))
|
||||
return a.mapTokenErrorToResponse(err)
|
||||
}
|
||||
|
||||
if user.ResetPasswordToken != token {
|
||||
a.logger.Debug("Reset token mismatch", mzap.StorableRef(&user))
|
||||
return response.BadRequest(a.logger, a.Name(), "invalid_token", "Invalid or expired reset token")
|
||||
if t.Purpose != model.PurposePasswordReset {
|
||||
a.logger.Warn("Invalid token purpose for password reset", zap.String("expected", string(model.PurposePasswordReset)), zap.String("actual", string(t.Purpose)))
|
||||
return response.DataConflict(a.logger, a.Name(), "Invalid token purpose")
|
||||
}
|
||||
|
||||
if t.AccountRef != accountRef {
|
||||
a.logger.Warn("Token account reference does not match request account reference", zap.String("token_account_ref", t.AccountRef.Hex()), zap.String("request_account_ref", accountRef.Hex()))
|
||||
return response.DataConflict(a.logger, a.Name(), "Token does not match account")
|
||||
}
|
||||
|
||||
// Parse new password from request body
|
||||
@@ -145,11 +151,6 @@ func (a *AccountAPI) resetPassword(r *http.Request) http.HandlerFunc {
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
if req.Password == "" {
|
||||
a.logger.Debug("New password is empty")
|
||||
return response.BadRequest(a.logger, a.Name(), "empty_password", "New password cannot be empty")
|
||||
}
|
||||
|
||||
// Validate new password
|
||||
if err := a.accService.ValidatePassword(req.Password, nil); err != nil {
|
||||
a.logger.Debug("Password validation failed", zap.Error(err), mzap.StorableRef(&user))
|
||||
@@ -172,7 +173,6 @@ func (a *AccountAPI) resetPassword(r *http.Request) http.HandlerFunc {
|
||||
func (a *AccountAPI) resetPasswordTransactionBody(ctx context.Context, user *model.Account, newPassword string) (any, error) {
|
||||
// Update user with new password and clear reset token
|
||||
user.Password = newPassword
|
||||
user.ResetPasswordToken = "" // Clear the token after use
|
||||
|
||||
// Hash the new password
|
||||
if err := user.HashPassword(); err != nil {
|
||||
|
||||
@@ -4,92 +4,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// TestPasswordResetTokenGeneration tests the token generation logic
|
||||
func TestPasswordResetTokenGeneration(t *testing.T) {
|
||||
// Test that ResetPassword service method generates a token
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
AccountBase: model.AccountBase{
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Initially no reset token
|
||||
assert.Empty(t, account.ResetPasswordToken, "Account should not have reset token initially")
|
||||
|
||||
// Simulate what ResetPassword service method does
|
||||
account.ResetPasswordToken = "generated-token-123"
|
||||
assert.NotEmpty(t, account.ResetPasswordToken, "Reset token should be generated")
|
||||
assert.Equal(t, "generated-token-123", account.ResetPasswordToken, "Reset token should match generated value")
|
||||
}
|
||||
|
||||
// TestPasswordResetTokenValidation tests token validation logic
|
||||
func TestPasswordResetTokenValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
storedToken string
|
||||
providedToken string
|
||||
shouldBeValid bool
|
||||
}{
|
||||
{
|
||||
name: "ValidToken_ShouldMatch",
|
||||
storedToken: "valid-token-123",
|
||||
providedToken: "valid-token-123",
|
||||
shouldBeValid: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidToken_ShouldNotMatch",
|
||||
storedToken: "valid-token-123",
|
||||
providedToken: "invalid-token-456",
|
||||
shouldBeValid: false,
|
||||
},
|
||||
{
|
||||
name: "EmptyStoredToken_ShouldBeInvalid",
|
||||
storedToken: "",
|
||||
providedToken: "any-token",
|
||||
shouldBeValid: false,
|
||||
},
|
||||
{
|
||||
name: "EmptyProvidedToken_ShouldBeInvalid",
|
||||
storedToken: "valid-token-123",
|
||||
providedToken: "",
|
||||
shouldBeValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
AccountPublic: model.AccountPublic{
|
||||
AccountBase: model.AccountBase{
|
||||
Describable: model.Describable{
|
||||
Name: "Test User",
|
||||
},
|
||||
},
|
||||
UserDataBase: model.UserDataBase{
|
||||
Login: "test@example.com",
|
||||
},
|
||||
},
|
||||
ResetPasswordToken: tt.storedToken,
|
||||
}
|
||||
|
||||
// Test token validation logic (what the resetPassword handler does)
|
||||
isValid := account.ResetPasswordToken != "" && account.ResetPasswordToken == tt.providedToken
|
||||
assert.Equal(t, tt.shouldBeValid, isValid, "Token validation should match expected result")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPasswordResetFlowLogic tests the logical flow without database dependencies
|
||||
func TestPasswordResetFlowLogic(t *testing.T) {
|
||||
t.Run("CompleteFlow", func(t *testing.T) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/db/policy"
|
||||
"github.com/tech/sendico/pkg/db/refreshtokens"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"github.com/tech/sendico/pkg/db/verification"
|
||||
"github.com/tech/sendico/pkg/domainprovider"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/messaging"
|
||||
@@ -36,6 +37,7 @@ type AccountAPI struct {
|
||||
tf transaction.Factory
|
||||
rtdb refreshtokens.DB
|
||||
plcdb policy.DB
|
||||
vdb verification.DB
|
||||
domain domainprovider.DomainProvider
|
||||
avatars mservice.MicroService
|
||||
producer messaging.Producer
|
||||
@@ -91,6 +93,10 @@ func CreateAPI(a eapi.API) (*AccountAPI, error) {
|
||||
p.logger.Error("Failed to create policies database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if p.vdb, err = a.DBFactory().NewVerificationsDB(); err != nil {
|
||||
p.logger.Error("Failed to create verification database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.domain = a.DomainProvider()
|
||||
p.producer = a.Register().Messaging().Producer()
|
||||
|
||||
@@ -91,7 +91,8 @@ func (a *AccountAPI) signup(r *http.Request) http.HandlerFunc {
|
||||
return response.BadPayload(a.logger, a.Name(), res)
|
||||
}
|
||||
|
||||
if err := a.executeSignupTransaction(r.Context(), &sr, newAccount); err != nil {
|
||||
verificationToken, err := a.executeSignupTransaction(r.Context(), &sr, newAccount)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrDataConflict) {
|
||||
a.logger.Warn("Failed to register account", zap.Error(err), zap.String("login", newAccount.Login))
|
||||
return response.DataConflict(a.logger, "user_already_registered", "User has already been registered")
|
||||
@@ -100,7 +101,7 @@ func (a *AccountAPI) signup(r *http.Request) http.HandlerFunc {
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
if err := a.sendWelcomeEmail(newAccount); err != nil {
|
||||
if err := a.sendWelcomeEmail(newAccount, verificationToken); err != nil {
|
||||
a.logger.Warn("Failed to send welcome email", zap.Error(err), mzap.StorableRef(newAccount))
|
||||
}
|
||||
|
||||
@@ -127,11 +128,14 @@ func (a *AccountAPI) signupAvailability(r *http.Request) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
func (a *AccountAPI) executeSignupTransaction(ctxt context.Context, sr *srequest.Signup, newAccount *model.Account) (string, error) {
|
||||
res, err := a.tf.CreateTransaction().Execute(ctxt, func(ctx context.Context) (any, error) {
|
||||
return a.signupTransactionBody(ctx, sr, newAccount)
|
||||
})
|
||||
return err
|
||||
if token, ok := res.(string); token != "" || ok {
|
||||
return token, err
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (a *AccountAPI) signupTransactionBody(ctx context.Context, sr *srequest.Signup, newAccount *model.Account) (any, error) {
|
||||
@@ -165,13 +169,14 @@ func (a *AccountAPI) signupTransactionBody(ctx context.Context, sr *srequest.Sig
|
||||
}
|
||||
a.logger.Info("Organization owner role permissions granted", mzap.StorableRef(org), zap.String("account", sr.Account.Login))
|
||||
|
||||
if err := a.accService.CreateAccount(ctx, org, newAccount, roleDescription.ID); err != nil {
|
||||
token, err := a.accService.CreateAccount(ctx, org, newAccount, roleDescription.ID)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to create account", zap.Error(err), zap.String("login", newAccount.Login))
|
||||
return nil, err
|
||||
}
|
||||
a.logger.Info("Organization owner account registered", mzap.StorableRef(org), zap.String("account", sr.Account.Login))
|
||||
|
||||
return nil, nil
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (a *AccountAPI) grantAllPermissions(ctx context.Context, organizationRef bson.ObjectID, roleID bson.ObjectID, newAccount *model.Account) error {
|
||||
|
||||
31
api/server/internal/server/accountapiimp/token.go
Normal file
31
api/server/internal/server/accountapiimp/token.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package accountapiimp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/db/verification"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (a *AccountAPI) mapTokenErrorToResponse(err error) http.HandlerFunc {
|
||||
if errors.Is(err, verification.ErrTokenNotFound) {
|
||||
a.logger.Debug("Verification token not found during consume", zap.Error(err))
|
||||
return response.NotFound(a.logger, a.Name(), "No account found associated with given verifcation token")
|
||||
}
|
||||
if errors.Is(err, verification.ErrTokenExpired) {
|
||||
a.logger.Debug("Verification token expired during consume", zap.Error(err))
|
||||
return response.Gone(a.logger, a.Name(), "token_expired", "verification token has expired")
|
||||
}
|
||||
if errors.Is(err, verification.ErrTokenAlreadyUsed) {
|
||||
a.logger.Debug("Verification token already used during consume", zap.Error(err))
|
||||
return response.DataConflict(a.logger, a.Name(), "verification token has already been used")
|
||||
}
|
||||
if err != nil {
|
||||
a.logger.Warn("Uenxpected error during token verification", zap.Error(err))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
a.logger.Debug("No token verification error found")
|
||||
return response.Success(a.logger)
|
||||
}
|
||||
@@ -31,7 +31,8 @@ func (a *AccountAPI) updateProfile(r *http.Request, account *model.Account, toke
|
||||
|
||||
if account.Login != u.Login {
|
||||
// Change email address
|
||||
if err := a.accService.UpdateLogin(ctx, account, u.Login); err != nil {
|
||||
verificationToken, err := a.accService.UpdateLogin(ctx, account, u.Login)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrDataConflict) {
|
||||
a.logger.Debug("Duplicate login, denying change...", zap.Error(err), mzap.StorableRef(u))
|
||||
return a.reportDuplicateEmail()
|
||||
@@ -41,7 +42,7 @@ func (a *AccountAPI) updateProfile(r *http.Request, account *model.Account, toke
|
||||
}
|
||||
|
||||
// Send verification email
|
||||
if err = a.sendWelcomeEmail(account); err != nil {
|
||||
if err = a.sendWelcomeEmail(account, verificationToken); err != nil {
|
||||
a.logger.Warn("Failed to send verification email", zap.Error(err), mzap.StorableRef(account))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
@@ -49,8 +50,7 @@ func (a *AccountAPI) updateProfile(r *http.Request, account *model.Account, toke
|
||||
} else {
|
||||
// Save the user
|
||||
u.Password = account.Password
|
||||
u.ResetPasswordToken = account.ResetPasswordToken
|
||||
u.VerifyToken = account.VerifyToken
|
||||
u.Status = account.Status
|
||||
if err = a.db.Update(ctx, u); err != nil {
|
||||
a.logger.Warn("Failed to save account", zap.Error(err), mzap.StorableRef(u))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
an "github.com/tech/sendico/pkg/messaging/notifications/account"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
@@ -38,6 +39,14 @@ func (a *InvitationAPI) doAccept(ctx context.Context, invitationRef bson.ObjectI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *InvitationAPI) sendWelcomeEmail(account *model.Account, token string) error {
|
||||
if err := a.producer.SendMessage(an.AccountCreated(a.Name(), *account.GetID(), token)); err != nil {
|
||||
a.Logger.Warn("Failed to send account creation notification", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *InvitationAPI) getPendingInvitation(ctx context.Context, invitationRef bson.ObjectID) (*model.Invitation, error) {
|
||||
a.Logger.Debug("Fetching invitation", mzap.ObjRef("invitation_ref", invitationRef))
|
||||
var inv model.Invitation
|
||||
@@ -79,10 +88,17 @@ func (a *InvitationAPI) fetchOrCreateAccount(ctx context.Context, org *model.Org
|
||||
return nil, err
|
||||
}
|
||||
// creates account and joins organization
|
||||
if err := a.accService.CreateAccount(ctx, org, account, inv.RoleRef); err != nil {
|
||||
token, err := a.accService.CreateAccount(ctx, org, account, inv.RoleRef)
|
||||
if err != nil {
|
||||
a.Logger.Warn("Failed to create account", zap.Error(err), zap.String("email", inv.Content.Email))
|
||||
return nil, err
|
||||
}
|
||||
// Send welcome email
|
||||
if err = a.sendWelcomeEmail(account, token); err != nil {
|
||||
a.Logger.Warn("Failed to send welcome email for new account created via invitation",
|
||||
zap.Error(err), zap.String("email", inv.Content.Email))
|
||||
return nil, err
|
||||
}
|
||||
return account, nil
|
||||
} else if err != nil {
|
||||
a.Logger.Warn("Failed to fetch account by email", zap.Error(err), zap.String("email", inv.Content.Email))
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/db/invitation"
|
||||
"github.com/tech/sendico/pkg/db/organization"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/accountservice"
|
||||
@@ -25,6 +26,7 @@ type InvitationAPI struct {
|
||||
adb account.DB
|
||||
odb organization.DB
|
||||
accService accountservice.AccountService
|
||||
producer messaging.Producer
|
||||
}
|
||||
|
||||
func (a *InvitationAPI) Name() mservice.Type {
|
||||
@@ -41,8 +43,9 @@ func CreateAPI(a eapi.API) (*InvitationAPI, error) {
|
||||
}
|
||||
|
||||
res := &InvitationAPI{
|
||||
irh: mutil.CreatePH("invitation"),
|
||||
tf: a.DBFactory().TransactionFactory(),
|
||||
irh: mutil.CreatePH("invitation"),
|
||||
tf: a.DBFactory().TransactionFactory(),
|
||||
producer: a.Register().Messaging().Producer(),
|
||||
}
|
||||
|
||||
p, err := papitemplate.CreateAPI(a, dbFactory, mservice.Organizations, mservice.Invitations)
|
||||
|
||||
Reference in New Issue
Block a user