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

This commit is contained in:
Stephan D
2026-02-09 16:43:25 +01:00
83 changed files with 1331 additions and 415 deletions

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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=

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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=

View File

@@ -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) {

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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

View File

@@ -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=

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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

View File

@@ -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=

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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),

View File

@@ -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 {

View File

@@ -81,6 +81,7 @@ func (p *paymentExecutor) pickIndependentSteps(
if blocked {
lg.Debug("Step permanently blocked by dependency failure")
setExecutionStepStatus(execStep, model.OperationStateCancelled)
continue
}

View File

@@ -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")

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)
}

View 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
}

View 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
}

View 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
}

View 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[:])
}

View 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)
}

View 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")
}

View 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)
}

View File

@@ -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

View File

@@ -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=

View File

@@ -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)

View File

@@ -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,
}
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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 {

View 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:"-"`
}

View File

@@ -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
)

View File

@@ -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)
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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
}

View File

@@ -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))
})
}
}

View File

@@ -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(

View File

@@ -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")
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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 {

View 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)
}

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)

View File

@@ -7,13 +7,13 @@ tmp_dir = "tmp"
entrypoint = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_dir = ["assets", "tmp", "vendor", "testdata", "storage"]
exclude_file = []
exclude_regex = ["_test.go", "_templ.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_dir = ["/src/api/pkg"]
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"

View File

@@ -17,7 +17,7 @@ RUN bash ci/scripts/proto/generate.sh
# Copy service dependencies (needed for go.mod replace directives)
COPY api/ledger ./api/ledger
COPY api/payments/orchestrator ./api/payments/orchestrator
COPY api/gateway/chain ./api/gateway/chain
COPY api/gateway/tron ./api/gateway/tron
COPY api/billing/fees ./api/billing/fees
COPY api/fx/oracle ./api/fx/oracle
COPY api/fx/storage ./api/fx/storage
@@ -38,7 +38,7 @@ COPY --from=builder /src/api/pkg ./api/pkg
# Copy service dependencies
COPY --from=builder /src/api/ledger ./api/ledger
COPY --from=builder /src/api/payments/orchestrator ./api/payments/orchestrator
COPY --from=builder /src/api/gateway/chain ./api/gateway/chain
COPY --from=builder /src/api/gateway/tron ./api/gateway/tron
COPY --from=builder /src/api/billing/fees ./api/billing/fees
COPY --from=builder /src/api/fx/oracle ./api/fx/oracle
COPY --from=builder /src/api/fx/storage ./api/fx/storage