Merge pull request 'otp-450' (#451) from otp-450 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful

Reviewed-on: #451
This commit was merged in pull request #451.
This commit is contained in:
2026-02-10 01:12:05 +00:00
120 changed files with 1892 additions and 1394 deletions

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/billing/documents
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
@@ -60,11 +60,11 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.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
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -221,16 +221,16 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -249,8 +249,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -258,8 +258,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/billing/fees
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
@@ -45,11 +45,11 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.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
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/protobuf v1.36.11
)

View File

@@ -172,15 +172,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -199,8 +199,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -208,8 +208,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/discovery
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../pkg
@@ -38,12 +38,12 @@ require (
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.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
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -172,15 +172,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -199,8 +199,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -208,8 +208,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/fx/ingestor
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
@@ -13,7 +13,7 @@ require (
github.com/tech/sendico/fx/storage v0.0.0
github.com/tech/sendico/pkg v0.1.0
go.uber.org/zap v1.27.1
golang.org/x/net v0.49.0
golang.org/x/net v0.50.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -43,11 +43,11 @@ require (
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sync v0.19.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
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -172,15 +172,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -199,8 +199,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -208,8 +208,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/fx/oracle
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
@@ -43,10 +43,10 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.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
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
)

View File

@@ -172,15 +172,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -199,8 +199,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -208,8 +208,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/fx/storage
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
@@ -23,8 +23,8 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -134,8 +134,8 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -150,16 +150,16 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.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=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/gateway/chain
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
@@ -77,12 +77,12 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
)

View File

@@ -318,8 +318,8 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -327,8 +327,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -349,8 +349,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -360,8 +360,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/gateway/mntx
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
@@ -43,10 +43,10 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.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
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
)

View File

@@ -174,15 +174,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -201,8 +201,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -210,8 +210,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/gateway/tgsettle
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
@@ -40,10 +40,10 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.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
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
)

View File

@@ -172,15 +172,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -199,8 +199,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -208,8 +208,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/gateway/tron
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
@@ -85,13 +85,13 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
)

View File

@@ -334,8 +334,8 @@ go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -344,8 +344,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -368,8 +368,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -379,10 +379,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -44,10 +44,10 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.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
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
)

View File

@@ -174,15 +174,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -201,8 +201,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -210,8 +210,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -31,7 +31,7 @@ func (s *Service) blockAccountResponder(_ context.Context, req *ledgerv1.BlockAc
return nil, err
}
logger := s.logger.With(mzap.ObjRef("account_ref", accountRef))
logger := s.logger.With(mzap.AccRef(accountRef))
account, err := s.storage.Accounts().Get(ctx, accountRef)
if err != nil {
@@ -61,7 +61,7 @@ func (s *Service) blockAccountResponder(_ context.Context, req *ledgerv1.BlockAc
}
if account.Status == pmodel.LedgerAccountStatusFrozen {
logger.Debug("account already frozen", mzap.ObjRef("account_ref", accountRef))
logger.Debug("account already frozen", mzap.AccRef(accountRef))
return &ledgerv1.BlockAccountResponse{Account: toProtoAccount(account)}, nil
}
@@ -71,7 +71,7 @@ func (s *Service) blockAccountResponder(_ context.Context, req *ledgerv1.BlockAc
}
account.Status = pmodel.LedgerAccountStatusFrozen
logger.Info("account blocked (frozen)", mzap.ObjRef("account_ref", accountRef))
logger.Info("account blocked (frozen)", mzap.AccRef(accountRef))
return &ledgerv1.BlockAccountResponse{Account: toProtoAccount(account)}, nil
}
}
@@ -94,7 +94,7 @@ func (s *Service) unblockAccountResponder(_ context.Context, req *ledgerv1.Unblo
return nil, err
}
logger := s.logger.With(mzap.ObjRef("account_ref", accountRef))
logger := s.logger.With(mzap.AccRef(accountRef))
account, err := s.storage.Accounts().Get(ctx, accountRef)
if err != nil {
@@ -124,7 +124,7 @@ func (s *Service) unblockAccountResponder(_ context.Context, req *ledgerv1.Unblo
}
if account.Status == pmodel.LedgerAccountStatusActive {
logger.Debug("account already active", mzap.ObjRef("account_ref", accountRef))
logger.Debug("account already active", mzap.AccRef(accountRef))
return &ledgerv1.UnblockAccountResponse{Account: toProtoAccount(account)}, nil
}
@@ -134,7 +134,7 @@ func (s *Service) unblockAccountResponder(_ context.Context, req *ledgerv1.Unblo
}
account.Status = pmodel.LedgerAccountStatusActive
logger.Info("account unblocked (active)", mzap.ObjRef("account_ref", accountRef))
logger.Info("account unblocked (active)", mzap.AccRef(accountRef))
return &ledgerv1.UnblockAccountResponse{Account: toProtoAccount(account)}, nil
}
}

View File

@@ -197,7 +197,7 @@ func (s *Service) upsertBalances(ctx context.Context, lines []*model.PostingLine
for accountRef, delta := range balanceDeltas {
account := accounts[accountRef]
if account == nil {
s.logger.Warn("account cache missing for balance update", mzap.ObjRef("account_ref", accountRef))
s.logger.Warn("account cache missing for balance update", mzap.AccRef(accountRef))
return merrors.Internal("account cache missing for balance update")
}
@@ -205,7 +205,7 @@ func (s *Service) upsertBalances(ctx context.Context, lines []*model.PostingLine
if err != nil && !errors.Is(err, storage.ErrBalanceNotFound) {
s.logger.Warn("failed to fetch account balance",
zap.Error(err),
mzap.ObjRef("account_ref", accountRef))
mzap.AccRef(accountRef))
return merrors.Internal("failed to update balance")
}
@@ -238,7 +238,7 @@ func (s *Service) upsertBalances(ctx context.Context, lines []*model.PostingLine
}
if err := balancesStore.Upsert(ctx, newBalance); err != nil {
s.logger.Warn("failed to upsert account balance", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
s.logger.Warn("failed to upsert account balance", zap.Error(err), mzap.AccRef(accountRef))
return merrors.Internal("failed to update balance")
}
}

View File

@@ -124,14 +124,14 @@ func (a *accountsStore) Get(ctx context.Context, accountRef bson.ObjectID) (*pkm
result := &pkm.LedgerAccount{}
if err := a.repo.Get(ctx, accountRef, result); err != nil {
if errors.Is(err, merrors.ErrNoData) {
a.logger.Debug("Account not found", mzap.ObjRef("account_ref", accountRef))
a.logger.Debug("Account not found", mzap.AccRef(accountRef))
return nil, storage.ErrAccountNotFound
}
a.logger.Warn("Failed to get account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
a.logger.Warn("Failed to get account", zap.Error(err), mzap.AccRef(accountRef))
return nil, err
}
a.logger.Debug("Account loaded", mzap.ObjRef("account_ref", accountRef), zap.String("account_code", result.AccountCode))
a.logger.Debug("Account loaded", mzap.AccRef(accountRef), zap.String("account_code", result.AccountCode))
return result, nil
}
@@ -325,10 +325,10 @@ func (a *accountsStore) UpdateStatus(ctx context.Context, accountRef bson.Object
patch := repository.Patch().Set(repository.Field("status"), status)
if err := a.repo.Patch(ctx, accountRef, patch); err != nil {
a.logger.Warn("Failed to update account status", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
a.logger.Warn("Failed to update account status", zap.Error(err), mzap.AccRef(accountRef))
return err
}
a.logger.Debug("Account status updated", mzap.ObjRef("account_ref", accountRef), zap.String("status", string(status)))
a.logger.Debug("Account status updated", mzap.AccRef(accountRef), zap.String("status", string(status)))
return nil
}

View File

@@ -56,14 +56,14 @@ func (b *balancesStore) Get(ctx context.Context, accountRef bson.ObjectID) (*mod
result := &model.AccountBalance{}
if err := b.repo.FindOneByFilter(ctx, query, result); err != nil {
if errors.Is(err, merrors.ErrNoData) {
b.logger.Debug("balance not found", mzap.ObjRef("account_ref", accountRef))
b.logger.Debug("balance not found", mzap.AccRef(accountRef))
return nil, storage.ErrBalanceNotFound
}
b.logger.Warn("failed to get balance", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
b.logger.Warn("failed to get balance", zap.Error(err), mzap.AccRef(accountRef))
return nil, err
}
b.logger.Debug("balance loaded", mzap.ObjRef("account_ref", accountRef),
b.logger.Debug("balance loaded", mzap.AccRef(accountRef),
zap.String("balance", result.Balance))
return result, nil
}

View File

@@ -130,10 +130,10 @@ func (p *postingLinesStore) ListByAccount(ctx context.Context, accountRef bson.O
return nil
})
if err != nil {
p.logger.Warn("failed to list posting lines by account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
p.logger.Warn("failed to list posting lines by account", zap.Error(err), mzap.AccRef(accountRef))
return nil, err
}
p.logger.Debug("listed posting lines by account", zap.Int("count", len(lines)), mzap.ObjRef("account_ref", accountRef))
p.logger.Debug("listed posting lines by account", zap.Int("count", len(lines)), mzap.AccRef(accountRef))
return lines, nil
}

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/notification
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../pkg
@@ -14,7 +14,7 @@ require (
github.com/xhit/go-simple-mail/v2 v2.16.0
go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1
golang.org/x/text v0.33.0
golang.org/x/text v0.34.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -46,11 +46,11 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.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/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -189,15 +189,15 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -216,8 +216,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -225,8 +225,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.uber.org/zap"
)
@@ -11,7 +12,7 @@ func (a *NotificationAPI) onAccount(context context.Context, account *model.Acco
var link string
var err error
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))
a.logger.Warn("Failed to generate verification link", zap.Error(err), mzap.Login(account))
return err
}
mr := a.client.MailBuilder().
@@ -21,9 +22,9 @@ func (a *NotificationAPI) onAccount(context context.Context, account *model.Acco
AddButton(link).
SetTemplateID("welcome")
if err := a.client.Send(mr); err != nil {
a.logger.Warn("Failed to send verification email", zap.Error(err), zap.String("login", account.Login))
a.logger.Warn("Failed to send verification email", zap.Error(err), mzap.Login(account))
return err
}
a.logger.Info("Verification email sent", zap.String("login", account.Login))
a.logger.Info("Verification email sent", mzap.Login(account))
return nil
}

View File

@@ -5,23 +5,24 @@ import (
"strings"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.uber.org/zap"
)
func (a *NotificationAPI) onConfirmationCode(ctx context.Context, account *model.Account, destination string, target model.ConfirmationTarget, code string) error {
func (a *NotificationAPI) onConfirmationCode(ctx context.Context, account *model.Account, target string, purpose model.VerificationPurpose, code string) error {
builder := a.client.MailBuilder().
AddRecipient(account.Name, strings.TrimSpace(destination)).
AddRecipient(account.Name, strings.TrimSpace(target)).
SetAccountID(account.ID.Hex()).
SetLocale(account.Locale).
SetTemplateID("confirmation-code").
AddData("Name", account.Name).
AddData("Code", code).
AddData("Target", string(target))
AddData("Target", model.VPToString(purpose))
if err := a.client.Send(builder); err != nil {
a.logger.Warn("Failed to send confirmation code email", zap.Error(err), zap.String("login", account.Login))
a.logger.Warn("Failed to send confirmation code email", zap.Error(err), mzap.Login(account))
return err
}
a.logger.Info("Confirmation code email sent", zap.String("login", account.Login), zap.String("destination", destination), zap.String("target", string(target)))
a.logger.Info("Confirmation code email sent", mzap.Login(account), zap.String("destination", target), zap.String("target", string(purpose)))
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.uber.org/zap"
)
@@ -11,7 +12,7 @@ func (a *NotificationAPI) onPasswordReset(context context.Context, account *mode
var link string
var err error
if link, err = a.dp.GetFullLink("password", "reset", account.ID.Hex(), resetToken); err != nil {
a.logger.Warn("Failed to generate password reset link", zap.Error(err), zap.String("login", account.Login))
a.logger.Warn("Failed to generate password reset link", zap.Error(err), mzap.Login(account))
return err
}
mr := a.client.MailBuilder().
@@ -22,9 +23,9 @@ func (a *NotificationAPI) onPasswordReset(context context.Context, account *mode
AddData("URL", link).
SetTemplateID("reset-password")
if err := a.client.Send(mr); err != nil {
a.logger.Warn("Failed to send password reset email", zap.Error(err), zap.String("login", account.Login))
a.logger.Warn("Failed to send password reset email", zap.Error(err), mzap.Login(account))
return err
}
a.logger.Info("Password reset email sent", zap.String("login", account.Login))
a.logger.Info("Password reset email sent", mzap.Login(account))
return nil
}

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/payments/orchestrator
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
@@ -101,10 +101,10 @@ require (
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.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
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
)

View File

@@ -205,8 +205,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -217,8 +217,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -242,14 +242,14 @@ 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=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -266,8 +266,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -192,6 +192,10 @@ func Forbidden(logger mlogger.Logger, source mservice.Type, errType, hint string
return Error(logger, source, http.StatusForbidden, errType, hint)
}
func TooManyRequests(logger mlogger.Logger, source mservice.Type, hint string) http.HandlerFunc {
return Error(logger, source, http.StatusTooManyRequests, "too_many_requests", hint)
}
func LicenseRequired(logger mlogger.Logger, source mservice.Type, hint string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
errorf(logger, w, r, source, http.StatusPaymentRequired, "license_required", hint)

View File

@@ -44,7 +44,7 @@ func (db *ArchivableDBImp[T]) SetArchived(ctx context.Context, accountRef, objec
// Check permissions using enforceObject helper
if err := enforceObjectByRef(ctx, db.dbImp, db.enforcer, model.ActionUpdate, accountRef, objectRef); err != nil {
db.logger.Warn("Failed to enforce object permission", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
return err
}
@@ -52,7 +52,7 @@ func (db *ArchivableDBImp[T]) SetArchived(ctx context.Context, accountRef, objec
obj := db.createEmpty()
if err := db.dbImp.Get(ctx, objectRef, obj); err != nil {
db.logger.Warn("Failed to get object for setting archived status", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
return err
}
@@ -60,7 +60,7 @@ func (db *ArchivableDBImp[T]) SetArchived(ctx context.Context, accountRef, objec
archivable := db.getArchivable(obj)
currentArchived := archivable.IsArchived()
if currentArchived == archived {
db.logger.Debug("No change needed - same archived status", mzap.ObjRef("account_ref", accountRef),
db.logger.Debug("No change needed - same archived status", mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
return nil // No change needed
}
@@ -69,11 +69,11 @@ func (db *ArchivableDBImp[T]) SetArchived(ctx context.Context, accountRef, objec
patch := repository.Patch().Set(repository.IsArchivedField(), archived)
if err := db.dbImp.Patch(ctx, objectRef, patch); err != nil {
db.logger.Warn("Failed to set archived status on object", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
return err
}
db.logger.Debug("Successfully set archived status on object", mzap.ObjRef("account_ref", accountRef),
db.logger.Debug("Successfully set archived status on object", mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived))
return nil
}
@@ -82,14 +82,14 @@ func (db *ArchivableDBImp[T]) SetArchived(ctx context.Context, accountRef, objec
func (db *ArchivableDBImp[T]) IsArchived(ctx context.Context, accountRef, objectRef bson.ObjectID) (bool, error) {
// // Check permissions using single Enforce
if err := enforceObjectByRef(ctx, db.dbImp, db.enforcer, model.ActionRead, accountRef, objectRef); err != nil {
db.logger.Debug("Permission denied for checking archived status", mzap.ObjRef("account_ref", accountRef),
db.logger.Debug("Permission denied for checking archived status", mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.String("action", string(model.ActionRead)))
return false, merrors.AccessDenied("read", "object", objectRef)
}
obj := db.createEmpty()
if err := db.dbImp.Get(ctx, objectRef, obj); err != nil {
db.logger.Warn("Failed to get object for checking archived status", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
return false, err
}
archivable := db.getArchivable(obj)

View File

@@ -33,13 +33,13 @@ func (db *ProtectedDBImp[T]) enforce(ctx context.Context, action model.Action, o
if err != nil {
db.DBImp.Logger.Warn("Failed to enforce permission",
zap.Error(err), mzap.ObjRef("permission_ref", object.GetPermissionRef()),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", object.GetOrganizationRef()),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", object.GetOrganizationRef()),
mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
return err
}
if !res {
db.DBImp.Logger.Debug("Access denied", mzap.ObjRef("permission_ref", object.GetPermissionRef()),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", object.GetOrganizationRef()),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", object.GetOrganizationRef()),
mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
return merrors.AccessDenied(db.Collection, string(action), objectRef)
}
@@ -47,7 +47,7 @@ func (db *ProtectedDBImp[T]) enforce(ctx context.Context, action model.Action, o
}
func (db *ProtectedDBImp[T]) Create(ctx context.Context, accountRef, organizationRef bson.ObjectID, object T) error {
db.DBImp.Logger.Debug("Attempting to create object", mzap.ObjRef("account_ref", accountRef),
db.DBImp.Logger.Debug("Attempting to create object", mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection)))
if object.GetPermissionRef() == bson.NilObjectID {
@@ -60,12 +60,12 @@ func (db *ProtectedDBImp[T]) Create(ctx context.Context, accountRef, organizatio
}
if err := db.DBImp.Create(ctx, object); err != nil {
db.DBImp.Logger.Warn("Failed to create object", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
db.DBImp.Logger.Warn("Failed to create object", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection)))
return err
}
db.DBImp.Logger.Debug("Successfully created object", mzap.ObjRef("account_ref", accountRef),
db.DBImp.Logger.Debug("Successfully created object", mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection)))
return nil
}
@@ -75,7 +75,7 @@ func (db *ProtectedDBImp[T]) InsertMany(ctx context.Context, accountRef, organiz
return nil
}
db.DBImp.Logger.Debug("Attempting to insert many objects", mzap.ObjRef("account_ref", accountRef),
db.DBImp.Logger.Debug("Attempting to insert many objects", mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection)),
zap.Int("count", len(objects)))
@@ -92,13 +92,13 @@ func (db *ProtectedDBImp[T]) InsertMany(ctx context.Context, accountRef, organiz
}
if err := db.DBImp.InsertMany(ctx, objects); err != nil {
db.DBImp.Logger.Warn("Failed to insert many objects", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
db.DBImp.Logger.Warn("Failed to insert many objects", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection)),
zap.Int("count", len(objects)))
return err
}
db.DBImp.Logger.Debug("Successfully inserted many objects", mzap.ObjRef("account_ref", accountRef),
db.DBImp.Logger.Debug("Successfully inserted many objects", mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection)),
zap.Int("count", len(objects)))
return nil
@@ -108,57 +108,57 @@ func (db *ProtectedDBImp[T]) enforceObject(ctx context.Context, action model.Act
l, err := db.ListIDs(ctx, action, accountRef, repository.IDFilter(objectRef))
if err != nil {
db.DBImp.Logger.Warn("Error occured while checking access rights", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
return err
}
if len(l) == 0 {
db.DBImp.Logger.Debug("Access denied", zap.String("action", string(action)), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
db.DBImp.Logger.Debug("Access denied", zap.String("action", string(action)), mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
return merrors.AccessDenied(db.Collection, string(action), objectRef)
}
return nil
}
func (db *ProtectedDBImp[T]) Get(ctx context.Context, accountRef, objectRef bson.ObjectID, result T) error {
db.DBImp.Logger.Debug("Attempting to get object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
db.DBImp.Logger.Debug("Attempting to get object", mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
if err := db.enforceObject(ctx, model.ActionRead, accountRef, objectRef); err != nil {
return err
}
if err := db.DBImp.Get(ctx, objectRef, result); err != nil {
db.DBImp.Logger.Warn("Failed to get object", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
db.DBImp.Logger.Warn("Failed to get object", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection)))
return err
}
db.DBImp.Logger.Debug("Successfully retrieved object",
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", result.GetOrganizationRef()),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", result.GetOrganizationRef()),
mzap.StorableRef(result), mzap.ObjRef("permission_ref", result.GetPermissionRef()))
return nil
}
func (db *ProtectedDBImp[T]) Update(ctx context.Context, accountRef bson.ObjectID, object T) error {
db.DBImp.Logger.Debug("Attempting to update object", mzap.ObjRef("account_ref", accountRef), mzap.StorableRef(object))
db.DBImp.Logger.Debug("Attempting to update object", mzap.AccRef(accountRef), mzap.StorableRef(object))
if err := db.enforceObject(ctx, model.ActionUpdate, accountRef, *object.GetID()); err != nil {
return err
}
if err := db.DBImp.Update(ctx, object); err != nil {
db.DBImp.Logger.Warn("Failed to update object", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
db.DBImp.Logger.Warn("Failed to update object", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", object.GetOrganizationRef()), mzap.StorableRef(object))
return err
}
db.DBImp.Logger.Debug("Successfully updated object",
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", object.GetOrganizationRef()),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", object.GetOrganizationRef()),
mzap.StorableRef(object), mzap.ObjRef("permission_ref", object.GetPermissionRef()))
return nil
}
func (db *ProtectedDBImp[T]) Delete(ctx context.Context, accountRef, objectRef bson.ObjectID) error {
db.DBImp.Logger.Debug("Attempting to delete object",
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
if err := db.enforceObject(ctx, model.ActionDelete, accountRef, objectRef); err != nil {
return err
@@ -166,12 +166,12 @@ func (db *ProtectedDBImp[T]) Delete(ctx context.Context, accountRef, objectRef b
if err := db.DBImp.Delete(ctx, objectRef); err != nil {
db.DBImp.Logger.Warn("Failed to delete object", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
return err
}
db.DBImp.Logger.Debug("Successfully deleted object",
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
return nil
}
@@ -182,17 +182,17 @@ func (db *ProtectedDBImp[T]) ListIDs(
query builder.Query,
) ([]bson.ObjectID, error) {
db.DBImp.Logger.Debug("Attempting to list object IDs",
mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection)), zap.Any("filter", query.BuildQuery()))
mzap.AccRef(accountRef), zap.String("collection", string(db.Collection)), zap.Any("filter", query.BuildQuery()))
// 1. Fetch all candidate IDs from the underlying DB
allIDs, err := db.DBImp.ListPermissionBound(ctx, query)
if err != nil {
db.DBImp.Logger.Warn("Failed to list object IDs", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
db.DBImp.Logger.Warn("Failed to list object IDs", zap.Error(err), mzap.AccRef(accountRef),
zap.String("collection", string(db.Collection)), zap.String("action", string(action)))
return nil, err
}
if len(allIDs) == 0 {
db.DBImp.Logger.Debug("No objects found matching filter", mzap.ObjRef("account_ref", accountRef),
db.DBImp.Logger.Debug("No objects found matching filter", mzap.AccRef(accountRef),
zap.String("collection", string(db.Collection)), zap.Any("filter", query.BuildQuery()))
return []bson.ObjectID{}, merrors.NoData(fmt.Sprintf("no %s found", db.Collection))
}
@@ -207,7 +207,7 @@ func (db *ProtectedDBImp[T]) ListIDs(
// If the error is something other than AccessDenied, we want to fail
db.DBImp.Logger.Warn("Error while enforcing read permission", zap.Error(enforceErr),
mzap.ObjRef("permission_ref", desc.GetPermissionRef()), zap.String("action", string(action)),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", desc.GetOrganizationRef()),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", desc.GetOrganizationRef()),
mzap.ObjRef("object_ref", *desc.GetID()), zap.String("collection", string(db.Collection)),
)
return nil, enforceErr
@@ -216,7 +216,7 @@ func (db *ProtectedDBImp[T]) ListIDs(
}
db.DBImp.Logger.Debug("Successfully enforced read permission on IDs", zap.Int("fetched_count", len(allIDs)),
zap.Int("allowed_count", len(allowedIDs)), mzap.ObjRef("account_ref", accountRef),
zap.Int("allowed_count", len(allowedIDs)), mzap.AccRef(accountRef),
zap.String("collection", string(db.Collection)), zap.String("action", string(action)))
// 3. Return only the IDs that passed permission checks
@@ -270,7 +270,7 @@ func CreateDBImp[T model.PermissionBoundStorable](
func (db *ProtectedDBImp[T]) Patch(ctx context.Context, accountRef, objectRef bson.ObjectID, patch builder.Patch) error {
db.DBImp.Logger.Debug("Attempting to patch object",
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
if err := db.enforceObject(ctx, model.ActionUpdate, accountRef, objectRef); err != nil {
return err
@@ -278,18 +278,18 @@ func (db *ProtectedDBImp[T]) Patch(ctx context.Context, accountRef, objectRef bs
if err := db.DBImp.Repository.Patch(ctx, objectRef, patch); err != nil {
db.DBImp.Logger.Warn("Failed to patch object", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
return err
}
db.DBImp.Logger.Debug("Successfully patched object",
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
return nil
}
func (db *ProtectedDBImp[T]) PatchMany(ctx context.Context, accountRef bson.ObjectID, query builder.Query, patch builder.Patch) (int, error) {
db.DBImp.Logger.Debug("Attempting to patch many objects",
mzap.ObjRef("account_ref", accountRef), zap.Any("filter", query.BuildQuery()))
mzap.AccRef(accountRef), zap.Any("filter", query.BuildQuery()))
ids, err := db.ListIDs(ctx, model.ActionUpdate, accountRef, query)
if err != nil {
@@ -309,11 +309,11 @@ func (db *ProtectedDBImp[T]) PatchMany(ctx context.Context, accountRef bson.Obje
modified, err := db.DBImp.Repository.PatchMany(ctx, finalQuery, patch)
if err != nil {
db.DBImp.Logger.Warn("Failed to patch many objects", zap.Error(err),
mzap.ObjRef("account_ref", accountRef))
mzap.AccRef(accountRef))
return 0, err
}
db.DBImp.Logger.Debug("Successfully patched many objects",
mzap.ObjRef("account_ref", accountRef), zap.Int("modified_count", modified))
mzap.AccRef(accountRef), zap.Int("modified_count", modified))
return modified, nil
}

View File

@@ -43,13 +43,13 @@ func (db *AccountBoundDBImp[T]) enforce(ctx context.Context, action model.Action
if err != nil {
db.Logger.Warn("Failed to enforce permission",
zap.Error(err), mzap.ObjRef("permission_ref", db.PermissionRef),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef),
zap.String("action", string(action)))
return err
}
if !res {
db.Logger.Debug("Access denied", mzap.ObjRef("permission_ref", db.PermissionRef),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef),
zap.String("action", string(action)))
return merrors.AccessDenied(db.Collection, string(action), bson.NilObjectID)
}
@@ -73,13 +73,13 @@ func (db *AccountBoundDBImp[T]) enforceInterface(ctx context.Context, action mod
if err != nil {
db.Logger.Warn("Failed to enforce permission",
zap.Error(err), mzap.ObjRef("permission_ref", db.PermissionRef),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef),
zap.String("action", string(action)))
return err
}
if !res {
db.Logger.Debug("Access denied", mzap.ObjRef("permission_ref", db.PermissionRef),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef),
zap.String("action", string(action)))
return merrors.AccessDenied(db.Collection, string(action), bson.NilObjectID)
}
@@ -88,7 +88,7 @@ func (db *AccountBoundDBImp[T]) enforceInterface(ctx context.Context, action mod
func (db *AccountBoundDBImp[T]) Create(ctx context.Context, accountRef bson.ObjectID, object T) error {
orgRef := object.GetOrganizationRef()
db.Logger.Debug("Attempting to create object", mzap.ObjRef("account_ref", accountRef),
db.Logger.Debug("Attempting to create object", mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", orgRef), zap.String("collection", string(db.Collection)))
// Check organization update permission for create operations
@@ -97,22 +97,22 @@ func (db *AccountBoundDBImp[T]) Create(ctx context.Context, accountRef bson.Obje
}
if err := db.DBImp.Create(ctx, object); err != nil {
db.Logger.Warn("Failed to create object", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
db.Logger.Warn("Failed to create object", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", orgRef), zap.String("collection", string(db.Collection)))
return err
}
db.Logger.Debug("Successfully created object", mzap.ObjRef("account_ref", accountRef),
db.Logger.Debug("Successfully created object", mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", orgRef), zap.String("collection", string(db.Collection)))
return nil
}
func (db *AccountBoundDBImp[T]) Get(ctx context.Context, accountRef, objectRef bson.ObjectID, result T) error {
db.Logger.Debug("Attempting to get object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
db.Logger.Debug("Attempting to get object", mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
// First get the object to check its organization
if err := db.DBImp.Get(ctx, objectRef, result); err != nil {
db.Logger.Warn("Failed to get object", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
db.Logger.Warn("Failed to get object", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection)))
return err
}
@@ -122,13 +122,13 @@ func (db *AccountBoundDBImp[T]) Get(ctx context.Context, accountRef, objectRef b
return err
}
db.Logger.Debug("Successfully retrieved object", mzap.ObjRef("account_ref", accountRef),
db.Logger.Debug("Successfully retrieved object", mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", result.GetOrganizationRef()), zap.String("collection", string(db.Collection)))
return nil
}
func (db *AccountBoundDBImp[T]) Update(ctx context.Context, accountRef bson.ObjectID, object T) error {
db.Logger.Debug("Attempting to update object", mzap.ObjRef("account_ref", accountRef), mzap.StorableRef(object))
db.Logger.Debug("Attempting to update object", mzap.AccRef(accountRef), mzap.StorableRef(object))
// Check organization update permission
if err := db.enforce(ctx, model.ActionUpdate, object, accountRef); err != nil {
@@ -136,18 +136,18 @@ func (db *AccountBoundDBImp[T]) Update(ctx context.Context, accountRef bson.Obje
}
if err := db.DBImp.Update(ctx, object); err != nil {
db.Logger.Warn("Failed to update object", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
db.Logger.Warn("Failed to update object", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", object.GetOrganizationRef()), mzap.StorableRef(object))
return err
}
db.Logger.Debug("Successfully updated object", mzap.ObjRef("account_ref", accountRef),
db.Logger.Debug("Successfully updated object", mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", object.GetOrganizationRef()), mzap.StorableRef(object))
return nil
}
func (db *AccountBoundDBImp[T]) Patch(ctx context.Context, accountRef, objectRef bson.ObjectID, patch builder.Patch) error {
db.Logger.Debug("Attempting to patch object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
db.Logger.Debug("Attempting to patch object", mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
// First get the object to check its organization
objs, err := db.DBImp.Repository.ListAccountBound(ctx, repository.IDFilter(objectRef))
@@ -156,7 +156,7 @@ func (db *AccountBoundDBImp[T]) Patch(ctx context.Context, accountRef, objectRef
return err
}
if len(objs) == 0 {
db.Logger.Debug("Permission denied for deletion", mzap.ObjRef("object_ref", objectRef), mzap.ObjRef("account_ref", accountRef))
db.Logger.Debug("Permission denied for deletion", mzap.ObjRef("object_ref", objectRef), mzap.AccRef(accountRef))
return merrors.AccessDenied(db.Collection, string(model.ActionDelete), objectRef)
}
@@ -166,17 +166,17 @@ func (db *AccountBoundDBImp[T]) Patch(ctx context.Context, accountRef, objectRef
}
if err := db.DBImp.Patch(ctx, objectRef, patch); err != nil {
db.Logger.Warn("Failed to patch object", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
db.Logger.Warn("Failed to patch object", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection)))
return err
}
db.Logger.Debug("Successfully patched object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
db.Logger.Debug("Successfully patched object", mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
return nil
}
func (db *AccountBoundDBImp[T]) Delete(ctx context.Context, accountRef, objectRef bson.ObjectID) error {
db.Logger.Debug("Attempting to delete object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
db.Logger.Debug("Attempting to delete object", mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
// First get the object to check its organization
objs, err := db.DBImp.Repository.ListAccountBound(ctx, repository.IDFilter(objectRef))
@@ -185,7 +185,7 @@ func (db *AccountBoundDBImp[T]) Delete(ctx context.Context, accountRef, objectRe
return err
}
if len(objs) == 0 {
db.Logger.Debug("Permission denied for deletion", mzap.ObjRef("object_ref", objectRef), mzap.ObjRef("account_ref", accountRef))
db.Logger.Debug("Permission denied for deletion", mzap.ObjRef("object_ref", objectRef), mzap.AccRef(accountRef))
return merrors.AccessDenied(db.Collection, string(model.ActionDelete), objectRef)
}
// Check organization update permission for delete operations
@@ -194,29 +194,29 @@ func (db *AccountBoundDBImp[T]) Delete(ctx context.Context, accountRef, objectRe
}
if err := db.DBImp.Delete(ctx, objectRef); err != nil {
db.Logger.Warn("Failed to delete object", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
db.Logger.Warn("Failed to delete object", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection)))
return err
}
db.Logger.Debug("Successfully deleted object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
db.Logger.Debug("Successfully deleted object", mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
return nil
}
func (db *AccountBoundDBImp[T]) DeleteMany(ctx context.Context, accountRef bson.ObjectID, query builder.Query) error {
db.Logger.Debug("Attempting to delete many objects", mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection)))
db.Logger.Debug("Attempting to delete many objects", mzap.AccRef(accountRef), zap.String("collection", string(db.Collection)))
// Get all candidate objects for batch permission checking
allObjects, err := db.DBImp.Repository.ListPermissionBound(ctx, query)
if err != nil {
db.Logger.Warn("Failed to list objects for delete many", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
db.Logger.Warn("Failed to list objects for delete many", zap.Error(err), mzap.AccRef(accountRef))
return err
}
// Use batch enforcement for efficiency
allowedResults, err := db.Enforcer.EnforceBatch(ctx, allObjects, accountRef, model.ActionUpdate)
if err != nil {
db.Logger.Warn("Failed to enforce batch permissions for delete many", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
db.Logger.Warn("Failed to enforce batch permissions for delete many", zap.Error(err), mzap.AccRef(accountRef))
return err
}
@@ -229,27 +229,27 @@ func (db *AccountBoundDBImp[T]) DeleteMany(ctx context.Context, accountRef bson.
}
if len(allowedIDs) == 0 {
db.Logger.Debug("No objects allowed for deletion", mzap.ObjRef("account_ref", accountRef))
db.Logger.Debug("No objects allowed for deletion", mzap.AccRef(accountRef))
return nil
}
// Delete only the allowed objects
allowedQuery := query.And(repository.Query().In(repository.IDField(), allowedIDs))
if err := db.DBImp.DeleteMany(ctx, allowedQuery); err != nil {
db.Logger.Warn("Failed to delete many objects", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
db.Logger.Warn("Failed to delete many objects", zap.Error(err), mzap.AccRef(accountRef))
return err
}
db.Logger.Debug("Successfully deleted many objects", mzap.ObjRef("account_ref", accountRef), zap.Int("count", len(allowedIDs)))
db.Logger.Debug("Successfully deleted many objects", mzap.AccRef(accountRef), zap.Int("count", len(allowedIDs)))
return nil
}
func (db *AccountBoundDBImp[T]) FindOne(ctx context.Context, accountRef bson.ObjectID, query builder.Query, result T) error {
db.Logger.Debug("Attempting to find one object", mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection)))
db.Logger.Debug("Attempting to find one object", mzap.AccRef(accountRef), zap.String("collection", string(db.Collection)))
// For FindOne, we need to check read permission after finding the object
if err := db.DBImp.FindOne(ctx, query, result); err != nil {
db.Logger.Warn("Failed to find one object", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
db.Logger.Warn("Failed to find one object", zap.Error(err), mzap.AccRef(accountRef))
return err
}
@@ -258,25 +258,25 @@ func (db *AccountBoundDBImp[T]) FindOne(ctx context.Context, accountRef bson.Obj
return err
}
db.Logger.Debug("Successfully found one object", mzap.ObjRef("account_ref", accountRef),
db.Logger.Debug("Successfully found one object", mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", result.GetOrganizationRef()))
return nil
}
func (db *AccountBoundDBImp[T]) ListIDs(ctx context.Context, accountRef bson.ObjectID, query builder.Query) ([]bson.ObjectID, error) {
db.Logger.Debug("Attempting to list object IDs", mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection)))
db.Logger.Debug("Attempting to list object IDs", mzap.AccRef(accountRef), zap.String("collection", string(db.Collection)))
// Get all candidate objects for batch permission checking
allObjects, err := db.DBImp.Repository.ListPermissionBound(ctx, query)
if err != nil {
db.Logger.Warn("Failed to list objects for ID filtering", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
db.Logger.Warn("Failed to list objects for ID filtering", zap.Error(err), mzap.AccRef(accountRef))
return nil, err
}
// Use batch enforcement for efficiency
allowedResults, err := db.Enforcer.EnforceBatch(ctx, allObjects, accountRef, model.ActionRead)
if err != nil {
db.Logger.Warn("Failed to enforce batch permissions for ID listing", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
db.Logger.Warn("Failed to enforce batch permissions for ID listing", zap.Error(err), mzap.AccRef(accountRef))
return nil, err
}
@@ -289,12 +289,12 @@ func (db *AccountBoundDBImp[T]) ListIDs(ctx context.Context, accountRef bson.Obj
}
db.Logger.Debug("Successfully filtered object IDs", zap.Int("total_count", len(allObjects)),
zap.Int("allowed_count", len(allowedIDs)), mzap.ObjRef("account_ref", accountRef))
zap.Int("allowed_count", len(allowedIDs)), mzap.AccRef(accountRef))
return allowedIDs, nil
}
func (db *AccountBoundDBImp[T]) ListAccountBound(ctx context.Context, accountRef, organizationRef bson.ObjectID, query builder.Query) ([]model.AccountBoundStorable, error) {
db.Logger.Debug("Attempting to list account bound objects", mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection)))
db.Logger.Debug("Attempting to list account bound objects", mzap.AccRef(accountRef), zap.String("collection", string(db.Collection)))
// Build query to find objects where accountRef matches OR is null/absent
accountQuery := repository.WithOrg(accountRef, organizationRef)
@@ -305,7 +305,7 @@ func (db *AccountBoundDBImp[T]) ListAccountBound(ctx context.Context, accountRef
// Get all candidate objects
allObjects, err := db.DBImp.Repository.ListAccountBound(ctx, finalQuery)
if err != nil {
db.Logger.Warn("Failed to list account bound objects", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
db.Logger.Warn("Failed to list account bound objects", zap.Error(err), mzap.AccRef(accountRef))
return nil, err
}
@@ -323,18 +323,18 @@ func (db *AccountBoundDBImp[T]) ListAccountBound(ctx context.Context, accountRef
}
db.Logger.Debug("Successfully filtered account bound objects", zap.Int("total_count", len(allObjects)),
zap.Int("allowed_count", len(allowedObjects)), mzap.ObjRef("account_ref", accountRef))
zap.Int("allowed_count", len(allowedObjects)), mzap.AccRef(accountRef))
return allowedObjects, nil
}
func (db *AccountBoundDBImp[T]) GetByAccountRef(ctx context.Context, accountRef bson.ObjectID, result T) error {
db.Logger.Debug("Attempting to get object by account ref", mzap.ObjRef("account_ref", accountRef))
db.Logger.Debug("Attempting to get object by account ref", mzap.AccRef(accountRef))
// Build query to find objects where accountRef matches OR is null/absent
query := repository.WithoutOrg(accountRef)
if err := db.DBImp.FindOne(ctx, query, result); err != nil {
db.Logger.Warn("Failed to get object by account ref", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
db.Logger.Warn("Failed to get object by account ref", zap.Error(err), mzap.AccRef(accountRef))
return err
}
@@ -343,13 +343,13 @@ func (db *AccountBoundDBImp[T]) GetByAccountRef(ctx context.Context, accountRef
return err
}
db.Logger.Debug("Successfully retrieved object by account ref", mzap.ObjRef("account_ref", accountRef),
db.Logger.Debug("Successfully retrieved object by account ref", mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", result.GetOrganizationRef()))
return nil
}
func (db *AccountBoundDBImp[T]) DeleteByAccountRef(ctx context.Context, accountRef bson.ObjectID) error {
db.Logger.Debug("Attempting to delete objects by account ref", mzap.ObjRef("account_ref", accountRef))
db.Logger.Debug("Attempting to delete objects by account ref", mzap.AccRef(accountRef))
// Build query to find objects where accountRef matches OR is null/absent
query := repository.WithoutOrg(accountRef)
@@ -357,7 +357,7 @@ func (db *AccountBoundDBImp[T]) DeleteByAccountRef(ctx context.Context, accountR
// Get all candidate objects for individual permission checking
allObjects, err := db.DBImp.Repository.ListAccountBound(ctx, query)
if err != nil {
db.Logger.Warn("Failed to list objects for delete by account ref", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
db.Logger.Warn("Failed to list objects for delete by account ref", zap.Error(err), mzap.AccRef(accountRef))
return err
}
@@ -375,18 +375,18 @@ func (db *AccountBoundDBImp[T]) DeleteByAccountRef(ctx context.Context, accountR
}
if len(allowedIDs) == 0 {
db.Logger.Debug("No objects allowed for deletion by account ref", mzap.ObjRef("account_ref", accountRef))
db.Logger.Debug("No objects allowed for deletion by account ref", mzap.AccRef(accountRef))
return nil
}
// Delete only the allowed objects
allowedQuery := query.And(repository.Query().In(repository.IDField(), allowedIDs))
if err := db.DBImp.DeleteMany(ctx, allowedQuery); err != nil {
db.Logger.Warn("Failed to delete objects by account ref", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
db.Logger.Warn("Failed to delete objects by account ref", zap.Error(err), mzap.AccRef(accountRef))
return err
}
db.Logger.Debug("Successfully deleted objects by account ref", mzap.ObjRef("account_ref", accountRef), zap.Int("count", len(allowedIDs)))
db.Logger.Debug("Successfully deleted objects by account ref", mzap.AccRef(accountRef), zap.Int("count", len(allowedIDs)))
return nil
}

View File

@@ -18,11 +18,11 @@ func enforceObject[T model.PermissionBoundStorable](ctx context.Context, db *tem
l, err := db.ListPermissionBound(ctx, query)
if err != nil {
db.Logger.Warn("Error occured while checking access rights", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), zap.String("action", string(action)))
mzap.AccRef(accountRef), zap.String("action", string(action)))
return err
}
if len(l) == 0 {
db.Logger.Debug("Access denied", mzap.ObjRef("account_ref", accountRef), zap.String("action", string(action)))
db.Logger.Debug("Access denied", mzap.AccRef(accountRef), zap.String("action", string(action)))
return merrors.AccessDenied(db.Repository.Collection(), string(action), bson.NilObjectID)
}
for _, item := range l {
@@ -34,11 +34,11 @@ func enforceObject[T model.PermissionBoundStorable](ctx context.Context, db *tem
res, err := enforcer.EnforceBatch(ctx, l, accountRef, action)
if err != nil {
db.Logger.Warn("Failed to enforce permission", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), zap.String("action", string(action)))
mzap.AccRef(accountRef), zap.String("action", string(action)))
}
for objectRef, hasPermission := range res {
if !hasPermission {
db.Logger.Info("Permission denied for object during reordering", mzap.ObjRef("account_ref", accountRef),
db.Logger.Info("Permission denied for object during reordering", mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.String("action", string(model.ActionUpdate)))
return merrors.AccessDenied(db.Repository.Collection(), string(action), objectRef)
}
@@ -50,11 +50,11 @@ func enforceObjectByRef[T model.PermissionBoundStorable](ctx context.Context, db
err := enforceObject(ctx, db, enforcer, action, accountRef, repository.IDFilter(objectRef))
if err != nil {
if errors.Is(err, merrors.ErrAccessDenied) {
db.Logger.Debug("Access denied", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
db.Logger.Debug("Access denied", mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
return merrors.AccessDenied(db.Repository.Collection(), string(action), objectRef)
} else {
db.Logger.Warn("Error occurred while checking permissions", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
}
}
return err

View File

@@ -46,7 +46,7 @@ func (db *indexableDBImp[T]) Reorder(ctx context.Context, accountRef, objectRef
obj := db.createEmpty()
if err := db.repo.Get(ctx, objectRef, obj); err != nil {
db.logger.Warn("Failed to get object for reordering", zap.Error(err), zap.Int("new_index", newIndex),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
return err
}
@@ -54,7 +54,7 @@ func (db *indexableDBImp[T]) Reorder(ctx context.Context, accountRef, objectRef
indexable := db.getIndexable(obj)
currentIndex := indexable.Index
if currentIndex == newIndex {
db.logger.Debug("No reordering needed - same index", mzap.ObjRef("account_ref", accountRef),
db.logger.Debug("No reordering needed - same index", mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex))
return nil // No change needed
}
@@ -72,13 +72,13 @@ func (db *indexableDBImp[T]) Reorder(ctx context.Context, accountRef, objectRef
objects, err := db.repo.ListPermissionBound(ctx, reorderFilter)
if err != nil {
db.logger.Warn("Failed to get affected objects for reordering (moving down)",
zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef),
zap.Error(err), mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef),
zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex))
return err
}
affectedObjects = append(affectedObjects, objects...)
db.logger.Debug("Found affected objects for moving down",
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(objects)))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(objects)))
} else {
// Moving up: items between newIndex and currentIndex-1 will be shifted down by +1
reorderFilter := filter.
@@ -89,12 +89,12 @@ func (db *indexableDBImp[T]) Reorder(ctx context.Context, accountRef, objectRef
objects, err := db.repo.ListPermissionBound(ctx, reorderFilter)
if err != nil {
db.logger.Warn("Failed to get affected objects for reordering (moving up)", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef),
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef),
zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex))
return err
}
affectedObjects = append(affectedObjects, objects...)
db.logger.Debug("Found affected objects for moving up", mzap.ObjRef("account_ref", accountRef),
db.logger.Debug("Found affected objects for moving up", mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(objects)))
}
@@ -102,7 +102,7 @@ func (db *indexableDBImp[T]) Reorder(ctx context.Context, accountRef, objectRef
targetObjects, err := db.repo.ListPermissionBound(ctx, repository.IDFilter(objectRef))
if err != nil {
db.logger.Warn("Failed to get target object for permission checking", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef))
return err
}
if len(targetObjects) > 0 {
@@ -110,27 +110,27 @@ func (db *indexableDBImp[T]) Reorder(ctx context.Context, accountRef, objectRef
}
// Check permissions for all affected objects using EnforceBatch
db.logger.Debug("Checking permissions for reordering", mzap.ObjRef("account_ref", accountRef),
db.logger.Debug("Checking permissions for reordering", mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(affectedObjects)),
zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex))
permissions, err := db.enforcer.EnforceBatch(ctx, affectedObjects, accountRef, model.ActionUpdate)
if err != nil {
db.logger.Warn("Failed to check permissions for reordering", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(affectedObjects)))
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(affectedObjects)))
return merrors.Internal("failed to check permissions for reordering")
}
// Verify all objects have update permission
for resObjectRef, hasPermission := range permissions {
if !hasPermission {
db.logger.Info("Permission denied for object during reordering", mzap.ObjRef("account_ref", accountRef),
db.logger.Info("Permission denied for object during reordering", mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.String("action", string(model.ActionUpdate)))
return merrors.AccessDenied(db.repo.Collection(), string(model.ActionUpdate), resObjectRef)
}
}
db.logger.Debug("All permissions granted, proceeding with reordering", mzap.ObjRef("account_ref", accountRef),
db.logger.Debug("All permissions granted, proceeding with reordering", mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.Int("permission_count", len(permissions)))
// All permissions checked, proceed with reordering
@@ -144,11 +144,11 @@ func (db *indexableDBImp[T]) Reorder(ctx context.Context, accountRef, objectRef
updatedCount, err := db.repo.PatchMany(ctx, reorderFilter, patch)
if err != nil {
db.logger.Warn("Failed to shift objects during reordering (moving down)", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef),
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef),
zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex), zap.Int("updated_count", updatedCount))
return err
}
db.logger.Debug("Successfully shifted objects (moving down)", mzap.ObjRef("account_ref", accountRef),
db.logger.Debug("Successfully shifted objects (moving down)", mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.Int("updated_count", updatedCount))
} else {
// Moving up: shift items between newIndex and currentIndex-1 down by +1
@@ -160,23 +160,23 @@ func (db *indexableDBImp[T]) Reorder(ctx context.Context, accountRef, objectRef
updatedCount, err := db.repo.PatchMany(ctx, reorderFilter, patch)
if err != nil {
db.logger.Warn("Failed to shift objects during reordering (moving up)", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef),
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef),
zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex), zap.Int("updated_count", updatedCount))
return err
}
db.logger.Debug("Successfully shifted objects (moving up)", mzap.ObjRef("account_ref", accountRef),
db.logger.Debug("Successfully shifted objects (moving up)", mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.Int("updated_count", updatedCount))
}
// Update the target object to new index
if err := db.repo.Patch(ctx, objectRef, repository.Patch().Set(repository.IndexField(), newIndex)); err != nil {
db.logger.Warn("Failed to update target object index", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
db.logger.Warn("Failed to update target object index", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("object_ref", objectRef), zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex))
return err
}
db.logger.Debug("Successfully reordered object with permission checking",
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("old_index", currentIndex),
mzap.AccRef(accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("old_index", currentIndex),
zap.Int("new_index", newIndex), zap.Int("affected_count", len(affectedObjects)))
return nil
}

View File

@@ -104,7 +104,7 @@ func (c *CasbinEnforcer) EnforceBatch(
ok, err := c.Enforce(ctx, desc.GetPermissionRef(), accountRef, desc.GetOrganizationRef(), *desc.GetID(), action)
if err != nil {
c.logger.Warn("Failed to enforce", zap.Error(err), mzap.ObjRef("permission_ref", desc.GetPermissionRef()),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", desc.GetOrganizationRef()),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", desc.GetOrganizationRef()),
mzap.ObjRef("object_ref", *desc.GetID()), zap.String("action", string(action)))
return nil, err
}
@@ -146,7 +146,7 @@ func (c *CasbinEnforcer) GetRoles(ctx context.Context, accountRef, orgRef bson.O
// GetPermissions retrieves all effective policies for the user within the domain.
func (c *CasbinEnforcer) GetPermissions(ctx context.Context, accountRef, orgRef bson.ObjectID) ([]model.Role, []model.Permission, error) {
c.logger.Debug("Fetching policies for user", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", orgRef))
c.logger.Debug("Fetching policies for user", mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", orgRef))
// Step 1: Retrieve all roles assigned to the user within the domain
roles, err := c.GetRoles(ctx, accountRef, orgRef)

View File

@@ -182,14 +182,14 @@ func (rm *RoleManager) Revoke(ctx context.Context, roleRef, accountRef, orgRef b
// logPolicyResult logs results for Assign and Revoke.
func (rm *RoleManager) logPolicyResult(action string, result bool, err error, roleRef, accountRef, orgRef bson.ObjectID) error {
if err != nil {
rm.logger.Warn("Failed to "+action+" role", zap.Error(err), mzap.ObjRef("role_ref", roleRef), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", orgRef))
rm.logger.Warn("Failed to "+action+" role", zap.Error(err), mzap.ObjRef("role_ref", roleRef), mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", orgRef))
return err
}
msg := "Role " + action + "ed successfully"
if !result {
msg = "Role already " + action + "ed"
}
rm.logger.Info(msg, mzap.ObjRef("role_ref", roleRef), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", orgRef))
rm.logger.Info(msg, mzap.ObjRef("role_ref", roleRef), mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", orgRef))
return nil
}

View File

@@ -48,24 +48,24 @@ func (n *Enforcer) Enforce(
action model.Action,
) (bool, error) {
if organizationRef.IsZero() {
n.logger.Warn("Missing organization context", mzap.ObjRef("account_ref", accountRef),
n.logger.Warn("Missing organization context", mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
mzap.ObjRef("object", objectRef), zap.String("action", string(action)))
return false, merrors.InvalidArgument("organization context missing", "organizationRef")
}
roleAssignments, err := n.rdb.Roles(ctx, accountRef, organizationRef)
if errors.Is(err, merrors.ErrNoData) {
n.logger.Debug("No roles defined for account", mzap.ObjRef("account_ref", accountRef))
n.logger.Debug("No roles defined for account", mzap.AccRef(accountRef))
return false, nil
}
if err != nil {
n.logger.Warn("Failed to fetch roles while checking permissions", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
n.logger.Warn("Failed to fetch roles while checking permissions", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
mzap.ObjRef("object", objectRef), zap.String("action", string(action)))
return false, err
}
if len(roleAssignments) == 0 {
n.logger.Warn("No roles found for account", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
n.logger.Warn("No roles found for account", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
return false, merrors.Internal("No roles found for account " + accountRef.Hex())
@@ -75,7 +75,7 @@ func (n *Enforcer) Enforce(
for _, roleAssignment := range roleAssignments {
policies, err := n.pdb.PoliciesForPermissionAction(ctx, roleAssignment.DescriptionRef, permissionRef, action)
if err != nil && !errors.Is(err, merrors.ErrNoData) {
n.logger.Warn("Failed to fetch permissions", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
n.logger.Warn("Failed to fetch permissions", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action)))
return false, err
@@ -137,7 +137,7 @@ func (n *Enforcer) EnforceBatch(
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
n.logger.Debug("No roles defined for account", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef))
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
// With no roles, mark all objects in this venue as denied.
for _, obj := range objs {
results[*obj.GetID()] = false
@@ -146,7 +146,7 @@ func (n *Enforcer) EnforceBatch(
continue
}
n.logger.Warn("Failed to fetch roles", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef))
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
return nil, err
}
@@ -198,14 +198,14 @@ func (n *Enforcer) EnforceBatch(
// GetRoles retrieves all roles assigned to the user within the domain.
func (n *Enforcer) GetRoles(ctx context.Context, accountRef, organizationRef bson.ObjectID) ([]model.Role, error) {
n.logger.Debug("Fetching roles for user", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef))
n.logger.Debug("Fetching roles for user", mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
ra, err := n.rdb.Roles(ctx, accountRef, organizationRef)
if errors.Is(err, merrors.ErrNoData) {
n.logger.Debug("No roles assigned to user", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef))
n.logger.Debug("No roles assigned to user", mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
return []model.Role{}, nil
}
if err != nil {
n.logger.Warn("Failed to fetch roles", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef))
n.logger.Warn("Failed to fetch roles", zap.Error(err), mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
return nil, err
}
@@ -225,7 +225,7 @@ func (n *Enforcer) Reload() error {
// GetPermissions retrieves all effective policies for the user within the domain.
func (n *Enforcer) GetPermissions(ctx context.Context, accountRef, organizationRef bson.ObjectID) ([]model.Role, []model.Permission, error) {
n.logger.Debug("Fetching policies for user", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef))
n.logger.Debug("Fetching policies for user", mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
roles, err := n.GetRoles(ctx, accountRef, organizationRef)
if err != nil {

View File

@@ -117,14 +117,14 @@ func (rm *RoleManager) Revoke(ctx context.Context, roleRef, accountRef, organiza
// logPolicyResult logs results for Assign and Revoke.
func (rm *RoleManager) logPolicyResult(action string, result bool, err error, roleRef, accountRef, organizationRef bson.ObjectID) error {
if err != nil {
rm.logger.Warn("Failed to "+action+" role", zap.Error(err), mzap.ObjRef("role_ref", roleRef), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef))
rm.logger.Warn("Failed to "+action+" role", zap.Error(err), mzap.ObjRef("role_ref", roleRef), mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
return err
}
msg := "Role " + action + "ed successfully"
if !result {
msg = "Role already " + action + "ed"
}
rm.logger.Info(msg, mzap.ObjRef("role_ref", roleRef), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef))
rm.logger.Info(msg, mzap.ObjRef("role_ref", roleRef), mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
return nil
}

View File

@@ -1,16 +0,0 @@
package confirmation
import (
"context"
"github.com/tech/sendico/pkg/db/template"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
type DB interface {
template.DB[*model.ConfirmationCode]
FindActive(ctx context.Context, accountRef bson.ObjectID, destination string, target model.ConfirmationTarget, now int64) (*model.ConfirmationCode, error)
DeleteTuple(ctx context.Context, accountRef bson.ObjectID, destination string, target model.ConfirmationTarget) error
}

View File

@@ -4,7 +4,6 @@ import (
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/account"
"github.com/tech/sendico/pkg/db/chainassets"
"github.com/tech/sendico/pkg/db/confirmation"
mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo"
"github.com/tech/sendico/pkg/db/invitation"
"github.com/tech/sendico/pkg/db/organization"
@@ -22,7 +21,6 @@ import (
// Factory exposes high-level repositories used by application services.
type Factory interface {
NewRefreshTokensDB() (refreshtokens.DB, error)
NewConfirmationsDB() (confirmation.DB, error)
NewChainAsstesDB() (chainassets.DB, error)

View File

@@ -1,67 +0,0 @@
package confirmationdb
import (
"github.com/tech/sendico/pkg/db/confirmation"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/db/template"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.uber.org/zap"
)
const (
fieldAccountRef = "accountRef"
fieldDestination = "destination"
fieldTarget = "target"
fieldExpiresAt = "expiresAt"
fieldUsed = "used"
)
type ConfirmationDB struct {
template.DBImp[*model.ConfirmationCode]
}
func Create(logger mlogger.Logger, db *mongo.Database) (confirmation.DB, error) {
p := &ConfirmationDB{
DBImp: *template.Create[*model.ConfirmationCode](logger, mservice.Confirmations, db),
}
// Ensure one active code per account/destination/target.
if err := p.Repository.CreateIndex(&ri.Definition{
Keys: []ri.Key{
{Field: fieldAccountRef, Sort: ri.Asc},
{Field: fieldDestination, Sort: ri.Asc},
{Field: fieldTarget, Sort: ri.Asc},
},
Unique: true,
}); err != nil {
p.Logger.Error("Failed to create confirmation unique index", zap.Error(err))
return nil, err
}
// TTL on expiry.
ttl := int32(0)
if err := p.Repository.CreateIndex(&ri.Definition{
Keys: []ri.Key{
{Field: fieldExpiresAt, Sort: ri.Asc},
},
TTL: &ttl,
}); err != nil {
p.Logger.Error("Failed to create confirmation TTL index", zap.Error(err))
return nil, err
}
// Query helper indexes.
if err := p.Repository.CreateIndex(&ri.Definition{
Keys: []ri.Key{
{Field: fieldUsed, Sort: ri.Asc},
},
}); err != nil {
p.Logger.Error("Failed to create confirmation used index", zap.Error(err))
return nil, err
}
return p, nil
}

View File

@@ -1,17 +0,0 @@
package confirmationdb
import (
"context"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
func (db *ConfirmationDB) DeleteTuple(ctx context.Context, accountRef bson.ObjectID, destination string, target model.ConfirmationTarget) error {
query := repository.Query().
Filter(repository.Field(fieldAccountRef), accountRef).
Filter(repository.Field(fieldDestination), destination).
Filter(repository.Field(fieldTarget), target)
return db.DeleteMany(ctx, query)
}

View File

@@ -1,26 +0,0 @@
package confirmationdb
import (
"context"
"time"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/repository/builder"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
func (db *ConfirmationDB) FindActive(ctx context.Context, accountRef bson.ObjectID, destination string, target model.ConfirmationTarget, now int64) (*model.ConfirmationCode, error) {
var res model.ConfirmationCode
query := repository.Query().
Filter(repository.Field(fieldAccountRef), accountRef).
Filter(repository.Field(fieldDestination), destination).
Filter(repository.Field(fieldTarget), target).
Filter(repository.Field(fieldUsed), false).
Comparison(repository.Field(fieldExpiresAt), builder.Gt, time.Unix(now, 0))
if err := db.FindOne(ctx, query, &res); err != nil {
return nil, err
}
return &res, nil
}

View File

@@ -11,10 +11,8 @@ import (
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/account"
"github.com/tech/sendico/pkg/db/chainassets"
"github.com/tech/sendico/pkg/db/confirmation"
"github.com/tech/sendico/pkg/db/internal/mongo/accountdb"
"github.com/tech/sendico/pkg/db/internal/mongo/chainassetsdb"
"github.com/tech/sendico/pkg/db/internal/mongo/confirmationdb"
"github.com/tech/sendico/pkg/db/internal/mongo/invitationdb"
"github.com/tech/sendico/pkg/db/internal/mongo/organizationdb"
"github.com/tech/sendico/pkg/db/internal/mongo/paymethoddb"
@@ -188,10 +186,6 @@ func (db *DB) NewAccountDB() (account.DB, error) {
return accountdb.Create(db.logger, db.db())
}
func (db *DB) NewConfirmationsDB() (confirmation.DB, error) {
return confirmationdb.Create(db.logger, db.db())
}
func (db *DB) NewOrganizationDB() (organization.DB, error) {
pdb, err := db.NewPoliciesDB()
if err != nil {

View File

@@ -12,7 +12,7 @@ func (db *PaymentMethodsDB) SetArchived(ctx context.Context, accountRef, organiz
// Use the ArchivableDB for the main archiving logic
if err := db.ArchivableDB.SetArchived(ctx, accountRef, objectRef, isArchived); err != nil {
db.DBImp.Logger.Warn("Failed to chnage object archive status", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef),
mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade))
return err
}

View File

@@ -14,7 +14,7 @@ func (db *RecipientDB) SetArchived(ctx context.Context, accountRef, organization
// Use the ArchivableDB for the main archiving logic
if err := db.ArchivableDB.SetArchived(ctx, accountRef, objectRef, isArchived); err != nil {
db.DBImp.Logger.Warn("Failed to change recipient archive status", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef),
mzap.ObjRef("recipient_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade))
return err
}
@@ -22,7 +22,7 @@ func (db *RecipientDB) SetArchived(ctx context.Context, accountRef, organization
if cascade {
if err := db.setArchivedPaymentMethods(ctx, accountRef, organizationRef, objectRef, isArchived); err != nil {
db.DBImp.Logger.Warn("Failed to update payment methods archive status", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef),
mzap.ObjRef("recipient_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade))
return err

View File

@@ -78,7 +78,7 @@ func (db *RefreshTokenDB) Revoke(ctx context.Context, accountRef bson.ObjectID,
if err := db.Repository.FindOneByFilter(ctx, f, &rt); err != nil {
if errors.Is(err, merrors.ErrNoData) {
db.Logger.Warn("Failed to find refresh token", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), zap.String("client_id", session.ClientID), zap.String("device_id", session.DeviceID))
mzap.AccRef(accountRef), zap.String("client_id", session.ClientID), zap.String("device_id", session.DeviceID))
return nil
}
return err

View File

@@ -6,68 +6,161 @@ import (
"time"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/repository/builder"
"github.com/tech/sendico/pkg/db/verification"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"go.uber.org/zap"
mutil "github.com/tech/sendico/pkg/mutil/db"
"go.mongodb.org/mongo-driver/v2/bson"
)
func (db *verificationDB) Consume(
ct context.Context,
accountRef bson.ObjectID,
purpose model.VerificationPurpose,
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),
)
accountScoped := accountRef != bson.NilObjectID
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))
scopeFilter := repository.Query().And(
repository.Filter("purpose", purpose),
)
if accountScoped {
scopeFilter = scopeFilter.And(repository.Filter("accountRef", accountRef))
}
// 1) Fast path for magic-link tokens: hash is deterministic and globally unique.
var token *model.VerificationToken
magicFilter := scopeFilter.And(
repository.Filter("verifyTokenHash", tokenHash(rawToken)),
)
var direct model.VerificationToken
err := db.DBImp.FindOne(ctx, magicFilter, &direct)
switch {
case err == nil:
token = &direct
case errors.Is(err, merrors.ErrNoData):
default:
return nil, err
}
// If account is unknown, do not scan OTP candidates globally.
if token == nil && !accountScoped {
return nil, verification.ErorrTokenNotFound()
}
// 2) OTP path (and fallback): load purpose/account scoped tokens and compare hash with per-token salt.
if token == nil {
tokens, err := mutil.GetObjects[model.VerificationToken](
ctx, db.Logger, scopeFilter, nil, db.DBImp.Repository,
)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
return nil, verification.ErorrTokenNotFound()
}
return nil, err
}
for i := range tokens {
t := &tokens[i]
hash := hasherFor(t).Hash(rawToken, t)
if hash == t.VerifyTokenHash {
token = t
break
}
}
if token == nil {
// wrong code/token → increment attempts for active (not used, not expired) scoped tokens
activeFilter := scopeFilter.And(
repository.Filter("usedAt", nil),
repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now),
)
_, _ = db.DBImp.PatchMany(
ctx,
activeFilter,
repository.Patch().Inc(repository.Field("attempts"), 1),
)
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),
)
// 3) Static checks
if token.UsedAt != nil {
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),
)
if !token.ExpiresAt.After(now) {
return nil, verification.ErorrTokenExpired()
}
if token.MaxRetries != nil && token.Attempts >= *token.MaxRetries {
return nil, verification.ErrorTokenAttemptsExceeded()
}
// 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))
// 4) Atomic consume
consumeFilter := repository.Query().And(
repository.IDFilter(token.ID),
repository.Filter("purpose", purpose),
repository.Filter("usedAt", nil),
repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now),
)
if accountScoped {
consumeFilter = consumeFilter.And(repository.Filter("accountRef", accountRef))
}
if token.MaxRetries != nil {
consumeFilter = consumeFilter.And(
repository.Query().Comparison(repository.Field("attempts"), builder.Lt, *token.MaxRetries),
)
}
updated, err := db.DBImp.PatchMany(
ctx,
consumeFilter,
repository.Patch().Set(repository.Field("usedAt"), now),
)
if err != nil {
return nil, err
}
return &existing, nil
if updated == 1 {
token.UsedAt = &now
return token, nil
}
// 5) Consume failed → increment attempts
_, _ = db.DBImp.PatchMany(
ctx,
repository.IDFilter(token.ID),
repository.Patch().Inc(repository.Field("attempts"), 1),
)
// 6) Re-check state
var fresh model.VerificationToken
if err := db.DBImp.FindOne(ctx, repository.IDFilter(token.ID), &fresh); err != nil {
return nil, merrors.Internal("failed to re-check token state")
}
if fresh.UsedAt != nil {
return nil, verification.ErorrTokenAlreadyUsed()
}
if !fresh.ExpiresAt.After(now) {
return nil, verification.ErorrTokenExpired()
}
if fresh.MaxRetries != nil && fresh.Attempts >= *fresh.MaxRetries {
return nil, verification.ErrorTokenAttemptsExceeded()
}
return nil, verification.ErorrTokenNotFound()
},
)
if e != nil {
return nil, e
}
@@ -76,6 +169,5 @@ func (db *verificationDB) Consume(
if !ok {
return nil, merrors.Internal("unexpected token type")
}
return res, nil
}

View File

@@ -2,95 +2,224 @@ package verificationimp
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"strings"
"time"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/repository/builder"
"github.com/tech/sendico/pkg/db/verification"
"github.com/tech/sendico/pkg/merrors"
"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 normalizedIdempotencyKey(value *string) (string, bool) {
if value == nil {
return "", false
}
key := strings.TrimSpace(*value)
if key == "" {
return "", false
}
return key, true
}
func idempotencyFilter(
request *verification.Request,
idempotencyKey string,
) builder.Query {
return repository.Query().And(
repository.Filter("accountRef", request.AccountRef),
repository.Filter("purpose", request.Purpose),
repository.Filter("target", request.Target),
repository.Filter("idempotencyKey", idempotencyKey),
)
}
func hashFilter(hash string) builder.Query {
return repository.Filter("verifyTokenHash", hash)
}
func idempotencySeed(request *verification.Request, idempotencyKey string) string {
return strings.Join([]string{
request.AccountRef.Hex(),
string(request.Purpose),
request.Target,
request.Kind,
idempotencyKey,
}, "|")
}
func newVerificationToken(
accountRef bson.ObjectID,
purpose model.VerificationPurpose,
target string,
ttl time.Duration,
request *verification.Request,
idempotencyKey string,
hasIdempotency bool,
) (*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),
var (
raw string
hash string
salt *string
err error
)
switch request.Kind {
case verification.TokenKindOTP:
if hasIdempotency {
var saltValue string
raw, saltValue, hash = generateDeterministicOTP(idempotencySeed(request, idempotencyKey))
salt = &saltValue
} else {
var s string
raw, s, hash, err = generateOTP()
if err != nil {
return nil, "", err
}
salt = &s
}
default: // Magic token
if hasIdempotency {
raw, hash = generateDeterministicMagic(idempotencySeed(request, idempotencyKey))
} else {
raw, hash, err = generateMagic()
if err != nil {
return nil, "", err
}
}
}
return token, rawToken, nil
token := &model.VerificationToken{
AccountRef: request.AccountRef,
Purpose: request.Purpose,
Target: request.Target,
IdempotencyKey: nil,
VerifyTokenHash: hash,
Salt: salt,
UsedAt: nil,
ExpiresAt: now.Add(request.Ttl),
MaxRetries: request.MaxRetries,
}
if hasIdempotency {
token.IdempotencyKey = &idempotencyKey
}
return token, raw, nil
}
func (db *verificationDB) Create(
ctx context.Context,
accountRef bson.ObjectID,
purpose model.VerificationPurpose,
target string,
ttl time.Duration,
request *verification.Request,
) (string, error) {
logFields := []zap.Field{
zap.String("purpose", string(purpose)), zap.Duration("ttl", ttl),
mzap.AccRef(accountRef), zap.String("target", target),
if request == nil {
return "", merrors.Internal("nil request")
}
token, raw, err := newVerificationToken(accountRef, purpose, target, ttl)
idempotencyKey, hasIdempotency := normalizedIdempotencyKey(request.IdempotencyKey)
token, raw, err := newVerificationToken(request, idempotencyKey, hasIdempotency)
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),
_, err = db.tf.CreateTransaction().Execute(ctx, func(tx context.Context) (any, error) {
now := time.Now().UTC()
baseFilter := repository.Query().And(
repository.Filter("accountRef", request.AccountRef),
repository.Filter("purpose", request.Purpose),
repository.Filter("target", request.Target),
repository.Filter("usedAt", nil),
repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now),
),
repository.Patch().Set(repository.Field("usedAt"), now),
)
)
// Optional idempotency key support for safe retries.
if hasIdempotency {
var sameToken model.VerificationToken
err := db.DBImp.FindOne(tx, hashFilter(token.VerifyTokenHash), &sameToken)
switch {
case err == nil:
// Same hash means the same Create operation already succeeded.
return nil, nil
case errors.Is(err, merrors.ErrNoData):
default:
return nil, err
}
var existing model.VerificationToken
err = db.DBImp.FindOne(tx, idempotencyFilter(request, idempotencyKey), &existing)
switch {
case err == nil:
// Existing request with the same idempotency scope has already succeeded.
return nil, nil
case errors.Is(err, merrors.ErrNoData):
default:
return nil, err
}
}
// 1) Cooldown: if there exists ANY active token created after cutoff → block
if request.Cooldown != nil {
cutoff := now.Add(-*request.Cooldown)
cooldownFilter := baseFilter.And(
repository.Query().Comparison(repository.Field("createdAt"), builder.Gt, cutoff),
)
var recent model.VerificationToken
err := db.DBImp.FindOne(tx, cooldownFilter, &recent)
switch {
case err == nil:
return nil, verification.ErrorCooldownActive()
case errors.Is(err, merrors.ErrNoData):
default:
return nil, err
}
}
// 2) Invalidate active tokens for this context
if _, err := db.DBImp.PatchMany(
tx,
baseFilter,
repository.Patch().Set(repository.Field("usedAt"), now),
); err != nil {
return nil, err
}
// 3) Create new token only after cooldown/idempotency checks pass.
if err := db.DBImp.Create(tx, token); err != nil {
if hasIdempotency && errors.Is(err, merrors.ErrDataConflict) {
var sameToken model.VerificationToken
findErr := db.DBImp.FindOne(tx, hashFilter(token.VerifyTokenHash), &sameToken)
switch {
case findErr == nil:
return nil, nil
case errors.Is(findErr, merrors.ErrNoData):
default:
return nil, findErr
}
var existing model.VerificationToken
findErr = db.DBImp.FindOne(tx, idempotencyFilter(request, idempotencyKey), &existing)
switch {
case findErr == nil:
return nil, nil
case errors.Is(findErr, merrors.ErrNoData):
default:
return nil, findErr
}
}
return nil, err
}
return nil, nil
})
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

@@ -35,6 +35,21 @@ func Create(
return nil, err
}
if err := p.Repository.CreateIndex(&ri.Definition{
Keys: []ri.Key{
{Field: "accountRef", Sort: ri.Asc},
{Field: "purpose", Sort: ri.Asc},
{Field: "target", Sort: ri.Asc},
{Field: "idempotencyKey", Sort: ri.Asc},
},
Unique: true,
Sparse: true,
Name: "uniq_verification_context_idempotency",
}); err != nil {
p.Logger.Error("Failed to create unique idempotency index on verification context", 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}},

View File

@@ -1,10 +1,105 @@
package verificationimp
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"github.com/tech/sendico/pkg/model"
)
type TokenHasher interface {
Hash(raw string, token *model.VerificationToken) string
}
type magicHasher struct{}
func (magicHasher) Hash(raw string, _ *model.VerificationToken) string {
return tokenHash(raw)
}
type otpHasher struct{}
func (otpHasher) Hash(raw string, t *model.VerificationToken) string {
return otpHash(raw, *t.Salt)
}
func hasherFor(t *model.VerificationToken) TokenHasher {
if t.Salt != nil {
return otpHasher{}
}
return magicHasher{}
}
const verificationTokenBytes = 32
const otpDigits = 6
func generateMagic() (raw, hash string, err error) {
rawBytes := make([]byte, verificationTokenBytes)
if _, err = rand.Read(rawBytes); err != nil {
return
}
raw = base64.RawURLEncoding.EncodeToString(rawBytes)
hash = tokenHash(raw)
return
}
func generateDeterministicMagic(seed string) (raw, hash string) {
sum := sha256.Sum256([]byte("magic:" + seed))
raw = base64.RawURLEncoding.EncodeToString(sum[:])
hash = tokenHash(raw)
return
}
func generateOTP() (code, salt, hash string, err error) {
// otpDigits-digit code
n := make([]byte, 4)
if _, err = rand.Read(n); err != nil {
return
}
num := int(n[0])<<24 | int(n[1])<<16 | int(n[2])<<8 | int(n[3])
mod := 1
for i := 0; i < otpDigits; i++ {
mod *= 10
}
code = fmt.Sprintf("%0*d", otpDigits, num%mod)
// per-token salt
saltBytes := make([]byte, 16)
if _, err = rand.Read(saltBytes); err != nil {
return
}
salt = base64.RawURLEncoding.EncodeToString(saltBytes)
hash = otpHash(code, salt)
return
}
func generateDeterministicOTP(seed string) (code, salt, hash string) {
sum := sha256.Sum256([]byte("otp:" + seed))
num := int(sum[0])<<24 | int(sum[1])<<16 | int(sum[2])<<8 | int(sum[3])
mod := 1
for i := 0; i < otpDigits; i++ {
mod *= 10
}
code = fmt.Sprintf("%0*d", otpDigits, num%mod)
salt = base64.RawURLEncoding.EncodeToString(sum[4:20])
hash = otpHash(code, salt)
return
}
// We store only the resulting hash (+salt) in DB, never the OTP itself.
func otpHash(code, salt string) string {
sum := sha256.Sum256([]byte(salt + ":" + code))
return hex.EncodeToString(sum[:])
}
func tokenHash(rawToken string) string {
hash := sha256.Sum256([]byte(rawToken))
return base64.RawURLEncoding.EncodeToString(hash[:])

View File

@@ -20,6 +20,7 @@ import (
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.uber.org/zap"
)
@@ -28,6 +29,10 @@ import (
// ---------------------------------------------------------------------------
func newTestVerificationDB(t *testing.T) *verificationDB {
return newTestVerificationDBWithFactory(t, &passthroughTxFactory{})
}
func newTestVerificationDBWithFactory(t *testing.T, tf transaction.Factory) *verificationDB {
t.Helper()
repo := newMemoryTokenRepository()
logger := zap.NewNop()
@@ -36,7 +41,7 @@ func newTestVerificationDB(t *testing.T) *verificationDB {
Logger: logger,
Repository: repo,
},
tf: &passthroughTxFactory{},
tf: tf,
}
}
@@ -51,6 +56,20 @@ func (*passthroughTx) Execute(ctx context.Context, cb transaction.Callback) (any
return cb(ctx)
}
// retryingTxFactory simulates transaction callbacks being executed more than once.
type retryingTxFactory struct{}
func (*retryingTxFactory) CreateTransaction() transaction.Transaction { return &retryingTx{} }
type retryingTx struct{}
func (*retryingTx) Execute(ctx context.Context, cb transaction.Callback) (any, error) {
if _, err := cb(ctx); err != nil {
return nil, err
}
return cb(ctx)
}
// ---------------------------------------------------------------------------
// in-memory repository for VerificationToken
// ---------------------------------------------------------------------------
@@ -156,8 +175,34 @@ func (m *memoryTokenRepository) InsertMany(ctx context.Context, objs []storable.
}
return nil
}
func (m *memoryTokenRepository) FindManyByFilter(context.Context, builder.Query, rd.DecodingFunc) error {
return merrors.NotImplemented("not needed")
func (m *memoryTokenRepository) FindManyByFilter(_ context.Context, query builder.Query, decoder rd.DecodingFunc) error {
m.mu.Lock()
var matches []interface{}
for _, id := range m.order {
tok := m.data[id]
if tok != nil && matchToken(query, tok) {
raw, err := bson.Marshal(cloneToken(tok))
if err != nil {
m.mu.Unlock()
return err
}
matches = append(matches, bson.Raw(raw))
}
}
m.mu.Unlock()
cur, err := mongo.NewCursorFromDocuments(matches, nil, nil)
if err != nil {
return err
}
defer cur.Close(context.Background())
for cur.Next(context.Background()) {
if err := decoder(cur); err != nil {
return err
}
}
return nil
}
func (m *memoryTokenRepository) Patch(context.Context, bson.ObjectID, builder.Patch) error {
return merrors.NotImplemented("not needed")
@@ -190,8 +235,14 @@ func (m *memoryTokenRepository) Collection() string { return mservice.Verificati
// tokenFieldValue returns the stored value for a given BSON field name.
func tokenFieldValue(tok *model.VerificationToken, field string) any {
switch field {
case "_id":
return tok.ID
case "createdAt":
return tok.CreatedAt
case "verifyTokenHash":
return tok.VerifyTokenHash
case "salt":
return tok.Salt
case "usedAt":
return tok.UsedAt
case "expiresAt":
@@ -202,6 +253,15 @@ func tokenFieldValue(tok *model.VerificationToken, field string) any {
return tok.Purpose
case "target":
return tok.Target
case "idempotencyKey":
if tok.IdempotencyKey == nil {
return nil
}
return *tok.IdempotencyKey
case "maxRetries":
return tok.MaxRetries
case "attempts":
return tok.Attempts
default:
return nil
}
@@ -261,11 +321,11 @@ func matchOperator(stored any, ops bson.M) bool {
for op, cmpVal := range ops {
switch op {
case "$gt":
if !timeGt(stored, cmpVal) {
if !cmpGt(stored, cmpVal) {
return false
}
case "$lt":
if !timeLt(stored, cmpVal) {
if !cmpLt(stored, cmpVal) {
return false
}
}
@@ -273,6 +333,36 @@ func matchOperator(stored any, ops bson.M) bool {
return true
}
func cmpGt(stored, cmpVal any) bool {
if si, ok := toInt(stored); ok {
if ci, ok := toInt(cmpVal); ok {
return si > ci
}
}
return timeGt(stored, cmpVal)
}
func cmpLt(stored, cmpVal any) bool {
if si, ok := toInt(stored); ok {
if ci, ok := toInt(cmpVal); ok {
return si < ci
}
}
return timeLt(stored, cmpVal)
}
func toInt(v any) (int, bool) {
switch iv := v.(type) {
case int:
return iv, true
case int64:
return int(iv), true
case int32:
return int(iv), true
}
return 0, false
}
func valuesEqual(a, b any) bool {
// nil checks: usedAt == nil
if b == nil {
@@ -343,21 +433,34 @@ func toTime(v any) (time.Time, bool) {
return time.Time{}, false
}
// applyPatch applies $set operations from a patch bson.D to a token.
// applyPatch applies $set and $inc 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
switch op.Key {
case "$set":
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
}
}
}
case "$inc":
fields, ok := op.Value.(bson.D)
if !ok {
continue
}
for _, f := range fields {
switch f.Key {
case "attempts":
if v, ok := f.Value.(int); ok {
tok.Attempts += v
}
}
}
}
@@ -370,20 +473,27 @@ func cloneToken(src *model.VerificationToken) *model.VerificationToken {
t := *src.UsedAt
dst.UsedAt = &t
}
if src.MaxRetries != nil {
v := *src.MaxRetries
dst.MaxRetries = &v
}
if src.Salt != nil {
s := *src.Salt
dst.Salt = &s
}
if src.IdempotencyKey != nil {
k := *src.IdempotencyKey
dst.IdempotencyKey = &k
}
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
// ---------------------------------------------------------------------------
// helpers request builder
// ---------------------------------------------------------------------------
func req(accountRef bson.ObjectID, purpose model.VerificationPurpose, target string, ttl time.Duration) *verification.Request {
return verification.NewLinkRequest(accountRef, purpose, target).WithTTL(ttl)
}
// ---------------------------------------------------------------------------
@@ -395,7 +505,7 @@ func TestCreate_ReturnsRawToken(t *testing.T) {
ctx := context.Background()
accountRef := bson.NewObjectID()
raw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
raw, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
assert.NotEmpty(t, raw)
}
@@ -405,10 +515,10 @@ func TestCreate_TokenCanBeConsumed(t *testing.T) {
ctx := context.Background()
accountRef := bson.NewObjectID()
raw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
raw, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
tok, err := db.Consume(ctx, raw)
tok, err := db.Consume(ctx, accountRef, model.PurposePasswordReset, raw)
require.NoError(t, err)
assert.Equal(t, accountRef, tok.AccountRef)
assert.Equal(t, model.PurposePasswordReset, tok.Purpose)
@@ -420,10 +530,10 @@ func TestConsume_ReturnsCorrectFields(t *testing.T) {
ctx := context.Background()
accountRef := bson.NewObjectID()
raw, err := db.Create(ctx, accountRef, model.PurposeEmailChange, "new@example.com", time.Hour)
raw, err := db.Create(ctx, req(accountRef, model.PurposeEmailChange, "new@example.com", time.Hour))
require.NoError(t, err)
tok, err := db.Consume(ctx, raw)
tok, err := db.Consume(ctx, accountRef, model.PurposeEmailChange, raw)
require.NoError(t, err)
assert.Equal(t, accountRef, tok.AccountRef)
assert.Equal(t, model.PurposeEmailChange, tok.Purpose)
@@ -435,16 +545,16 @@ func TestConsume_SecondConsumeFailsAlreadyUsed(t *testing.T) {
ctx := context.Background()
accountRef := bson.NewObjectID()
raw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
raw, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
_, err = db.Consume(ctx, raw)
_, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, raw)
require.NoError(t, err)
_, err = db.Consume(ctx, raw)
_, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, raw)
require.Error(t, err)
assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed),
"second consume should fail because usedAt is set")
"second consume should fail with already-used after usedAt is set")
}
func TestConsume_ExpiredTokenFails(t *testing.T) {
@@ -453,44 +563,58 @@ func TestConsume_ExpiredTokenFails(t *testing.T) {
accountRef := bson.NewObjectID()
// Create with a TTL that is already in the past.
raw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", -time.Hour)
raw, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", -time.Hour))
require.NoError(t, err)
_, err = db.Consume(ctx, raw)
_, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, raw)
require.Error(t, err)
assert.True(t, errors.Is(err, verification.ErrTokenExpired),
"expired token should not be consumable")
"expired token should return explicit expiry error")
}
func TestConsume_UnknownTokenFails(t *testing.T) {
db := newTestVerificationDB(t)
ctx := context.Background()
_, err := db.Consume(ctx, "nonexistent-token-value")
_, err := db.Consume(ctx, bson.NilObjectID, "", "nonexistent-token-value")
require.Error(t, err)
assert.True(t, errors.Is(err, verification.ErrTokenNotFound))
}
func TestConsume_AccountActivationWithoutAccountRef(t *testing.T) {
db := newTestVerificationDB(t)
ctx := context.Background()
accountRef := bson.NewObjectID()
raw, err := db.Create(ctx, req(accountRef, model.PurposeAccountActivation, "", time.Hour))
require.NoError(t, err)
tok, err := db.Consume(ctx, bson.NilObjectID, model.PurposeAccountActivation, raw)
require.NoError(t, err)
assert.Equal(t, accountRef, tok.AccountRef)
assert.Equal(t, model.PurposeAccountActivation, tok.Purpose)
}
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)
oldRaw, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
newRaw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
newRaw, err := db.Create(ctx, req(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)
// Old token is no longer consumable — invalidated (usedAt set) by the second Create.
_, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, oldRaw)
require.Error(t, err)
assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed),
"old token should be invalidated (usedAt set) after new token creation")
"old token should return already-used after invalidation")
// New token works fine.
tok, err := db.Consume(ctx, newRaw)
tok, err := db.Consume(ctx, accountRef, model.PurposePasswordReset, newRaw)
require.NoError(t, err)
assert.Equal(t, accountRef, tok.AccountRef)
}
@@ -500,19 +624,19 @@ func TestCreate_InvalidatesMultiplePreviousTokens(t *testing.T) {
ctx := context.Background()
accountRef := bson.NewObjectID()
first, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
first, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
second, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
second, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
third, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
third, err := db.Create(ctx, req(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")
_, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, first)
assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed), "first should be invalidated/used")
_, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, second)
assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed), "second should be invalidated/used")
tok, err := db.Consume(ctx, third)
tok, err := db.Consume(ctx, accountRef, model.PurposePasswordReset, third)
require.NoError(t, err)
assert.Equal(t, accountRef, tok.AccountRef)
}
@@ -522,14 +646,14 @@ func TestCreate_DifferentPurposeNotInvalidated(t *testing.T) {
ctx := context.Background()
accountRef := bson.NewObjectID()
resetRaw, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
resetRaw, err := db.Create(ctx, req(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)
_, err = db.Create(ctx, req(accountRef, model.PurposeAccountActivation, "", time.Hour))
require.NoError(t, err)
tok, err := db.Consume(ctx, resetRaw)
tok, err := db.Consume(ctx, accountRef, model.PurposePasswordReset, resetRaw)
require.NoError(t, err)
assert.Equal(t, model.PurposePasswordReset, tok.Purpose)
}
@@ -539,14 +663,14 @@ func TestCreate_DifferentTargetNotInvalidated(t *testing.T) {
ctx := context.Background()
accountRef := bson.NewObjectID()
firstRaw, err := db.Create(ctx, accountRef, model.PurposeEmailChange, "a@example.com", time.Hour)
firstRaw, err := db.Create(ctx, req(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)
_, err = db.Create(ctx, req(accountRef, model.PurposeEmailChange, "b@example.com", time.Hour))
require.NoError(t, err)
tok, err := db.Consume(ctx, firstRaw)
tok, err := db.Consume(ctx, accountRef, model.PurposeEmailChange, firstRaw)
require.NoError(t, err)
assert.Equal(t, "a@example.com", tok.Target)
}
@@ -557,13 +681,13 @@ func TestCreate_DifferentAccountNotInvalidated(t *testing.T) {
account1 := bson.NewObjectID()
account2 := bson.NewObjectID()
raw1, err := db.Create(ctx, account1, model.PurposePasswordReset, "", time.Hour)
raw1, err := db.Create(ctx, req(account1, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
_, err = db.Create(ctx, account2, model.PurposePasswordReset, "", time.Hour)
_, err = db.Create(ctx, req(account2, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
tok, err := db.Consume(ctx, raw1)
tok, err := db.Consume(ctx, account1, model.PurposePasswordReset, raw1)
require.NoError(t, err)
assert.Equal(t, account1, tok.AccountRef)
}
@@ -574,18 +698,18 @@ func TestCreate_AlreadyUsedTokenNotInvalidatedAgain(t *testing.T) {
accountRef := bson.NewObjectID()
// Create and consume first token.
raw1, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", time.Hour)
raw1, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
_, err = db.Consume(ctx, raw1)
_, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, 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)
raw2, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
tok, err := db.Consume(ctx, raw2)
tok, err := db.Consume(ctx, accountRef, model.PurposePasswordReset, raw2)
require.NoError(t, err)
assert.Equal(t, accountRef, tok.AccountRef)
}
@@ -596,14 +720,14 @@ func TestCreate_ExpiredTokenNotInvalidated(t *testing.T) {
accountRef := bson.NewObjectID()
// Create a token that is already expired.
_, err := db.Create(ctx, accountRef, model.PurposePasswordReset, "", -time.Hour)
_, err := db.Create(ctx, req(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)
raw2, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
tok, err := db.Consume(ctx, raw2)
tok, err := db.Consume(ctx, accountRef, model.PurposePasswordReset, raw2)
require.NoError(t, err)
assert.Equal(t, accountRef, tok.AccountRef)
}
@@ -619,3 +743,228 @@ func TestTokenHash_DifferentInputs(t *testing.T) {
h2 := tokenHash("input-b")
assert.NotEqual(t, h1, h2)
}
// ---------------------------------------------------------------------------
// cooldown tests
// ---------------------------------------------------------------------------
func TestCreate_CooldownBlocksCreation(t *testing.T) {
db := newTestVerificationDB(t)
ctx := context.Background()
accountRef := bson.NewObjectID()
// First creation without cooldown.
_, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
// Immediate re-create with cooldown should be blocked — token is too recent to invalidate.
r2 := req(accountRef, model.PurposePasswordReset, "", time.Hour).WithCooldown(time.Minute)
_, err = db.Create(ctx, r2)
require.Error(t, err)
assert.True(t, errors.Is(err, verification.ErrCooldownActive))
}
func TestCreate_CooldownExpiresAllowsCreation(t *testing.T) {
db := newTestVerificationDB(t)
ctx := context.Background()
accountRef := bson.NewObjectID()
// First creation without cooldown.
_, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// Re-create with short cooldown — the prior token is old enough to be invalidated.
r2 := req(accountRef, model.PurposePasswordReset, "", time.Hour).WithCooldown(time.Millisecond)
_, err = db.Create(ctx, r2)
require.NoError(t, err)
}
func TestCreate_CooldownNilIgnored(t *testing.T) {
db := newTestVerificationDB(t)
ctx := context.Background()
accountRef := bson.NewObjectID()
_, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
// No cooldown set — immediate re-create should succeed.
_, err = db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
}
func TestCreate_IdempotencyKeyReplayReturnsSameToken(t *testing.T) {
db := newTestVerificationDB(t)
ctx := context.Background()
accountRef := bson.NewObjectID()
firstReq := req(accountRef, model.PurposePasswordReset, "", time.Hour).WithIdempotencyKey("same-key")
firstRaw, err := db.Create(ctx, firstReq)
require.NoError(t, err)
require.NotEmpty(t, firstRaw)
// Replay with the same idempotency key should return success and same token.
secondReq := req(accountRef, model.PurposePasswordReset, "", time.Hour).WithIdempotencyKey("same-key")
secondRaw, err := db.Create(ctx, secondReq)
require.NoError(t, err)
assert.Equal(t, firstRaw, secondRaw)
repo := db.Repository.(*memoryTokenRepository)
repo.mu.Lock()
assert.Len(t, repo.data, 1)
repo.mu.Unlock()
}
func TestCreate_IdempotencyScopeIncludesTarget(t *testing.T) {
db := newTestVerificationDB(t)
ctx := context.Background()
accountRef := bson.NewObjectID()
r1 := req(accountRef, model.PurposeEmailChange, "a@example.com", time.Hour).WithIdempotencyKey("same-key")
raw1, err := db.Create(ctx, r1)
require.NoError(t, err)
require.NotEmpty(t, raw1)
// Same account/purpose/key but different target should be treated as a different idempotency scope.
r2 := req(accountRef, model.PurposeEmailChange, "b@example.com", time.Hour).WithIdempotencyKey("same-key")
raw2, err := db.Create(ctx, r2)
require.NoError(t, err)
require.NotEmpty(t, raw2)
assert.NotEqual(t, raw1, raw2)
t1, err := db.Consume(ctx, accountRef, model.PurposeEmailChange, raw1)
require.NoError(t, err)
assert.Equal(t, "a@example.com", t1.Target)
t2, err := db.Consume(ctx, accountRef, model.PurposeEmailChange, raw2)
require.NoError(t, err)
assert.Equal(t, "b@example.com", t2.Target)
}
func TestCreate_IdempotencySurvivesCallbackRetry(t *testing.T) {
db := newTestVerificationDBWithFactory(t, &retryingTxFactory{})
ctx := context.Background()
accountRef := bson.NewObjectID()
// Cooldown would block the second callback execution if idempotency wasn't handled.
r := req(accountRef, model.PurposePasswordReset, "", time.Hour).
WithCooldown(time.Minute).
WithIdempotencyKey("retry-safe")
raw, err := db.Create(ctx, r)
require.NoError(t, err)
require.NotEmpty(t, raw)
repo := db.Repository.(*memoryTokenRepository)
repo.mu.Lock()
require.Len(t, repo.data, 1)
for _, tok := range repo.data {
require.NotNil(t, tok.IdempotencyKey)
assert.Equal(t, "retry-safe", *tok.IdempotencyKey)
assert.Nil(t, tok.UsedAt)
assert.Equal(t, tok.VerifyTokenHash, tokenHash(raw))
}
repo.mu.Unlock()
}
// ---------------------------------------------------------------------------
// max retries / attempts tests
// ---------------------------------------------------------------------------
func TestConsume_MaxRetriesExceeded(t *testing.T) {
db := newTestVerificationDB(t)
ctx := context.Background()
accountRef := bson.NewObjectID()
r := req(accountRef, model.PurposePasswordReset, "", time.Hour).WithMaxRetries(2)
raw, err := db.Create(ctx, r)
require.NoError(t, err)
// Simulate 2 prior failed attempts by setting Attempts directly.
repo := db.Repository.(*memoryTokenRepository)
repo.mu.Lock()
for _, tok := range repo.data {
tok.Attempts = 2
}
repo.mu.Unlock()
// Consume with correct token should fail — attempts already at max.
_, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, raw)
require.Error(t, err)
assert.True(t, errors.Is(err, verification.ErrTokenAttemptsExceeded))
}
func TestConsume_UnderMaxRetriesSucceeds(t *testing.T) {
db := newTestVerificationDB(t)
ctx := context.Background()
accountRef := bson.NewObjectID()
r := req(accountRef, model.PurposePasswordReset, "", time.Hour).WithMaxRetries(3)
raw, err := db.Create(ctx, r)
require.NoError(t, err)
// Simulate 2 prior failed attempts (under maxRetries=3).
repo := db.Repository.(*memoryTokenRepository)
repo.mu.Lock()
for _, tok := range repo.data {
tok.Attempts = 2
}
repo.mu.Unlock()
// Consume with correct token should succeed.
tok, err := db.Consume(ctx, accountRef, model.PurposePasswordReset, raw)
require.NoError(t, err)
assert.Equal(t, accountRef, tok.AccountRef)
}
func TestConsume_NoMaxRetriesIgnoresAttempts(t *testing.T) {
db := newTestVerificationDB(t)
ctx := context.Background()
accountRef := bson.NewObjectID()
// Create without MaxRetries.
raw, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
// Simulate high attempt count — should be ignored since MaxRetries is nil.
repo := db.Repository.(*memoryTokenRepository)
repo.mu.Lock()
for _, tok := range repo.data {
tok.Attempts = 100
}
repo.mu.Unlock()
tok, err := db.Consume(ctx, accountRef, model.PurposePasswordReset, raw)
require.NoError(t, err)
assert.Equal(t, accountRef, tok.AccountRef)
}
func TestConsume_WrongHashReturnsNotFound(t *testing.T) {
db := newTestVerificationDB(t)
ctx := context.Background()
accountRef := bson.NewObjectID()
_, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
// Wrong code — hash won't match any token.
_, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, "wrong-code")
require.Error(t, err)
assert.True(t, errors.Is(err, verification.ErrTokenNotFound))
}
func TestConsume_ContextMismatchReturnsNotFound(t *testing.T) {
db := newTestVerificationDB(t)
ctx := context.Background()
accountRef := bson.NewObjectID()
otherAccount := bson.NewObjectID()
raw, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour))
require.NoError(t, err)
// Correct token but wrong accountRef — context mismatch.
_, err = db.Consume(ctx, otherAccount, model.PurposePasswordReset, raw)
require.Error(t, err)
assert.True(t, errors.Is(err, verification.ErrTokenNotFound))
}

View File

@@ -6,9 +6,12 @@ import (
)
var (
ErrTokenNotFound = errors.New("vtNotFound")
ErrTokenAlreadyUsed = errors.New("vtAlreadyUsed")
ErrTokenExpired = errors.New("vtExpired")
ErrTokenNotFound = errors.New("vtNotFound")
ErrTokenAlreadyUsed = errors.New("vtAlreadyUsed")
ErrTokenExpired = errors.New("vtExpired")
ErrTokenAttemptsExceeded = errors.New("vtAttemptsExceeded")
ErrCooldownActive = errors.New("vtCooldownActive")
ErrIdempotencyConflict = errors.New("vtIdempotencyConflict")
)
func wrap(err error, msg string) error {
@@ -26,3 +29,15 @@ func ErorrTokenAlreadyUsed() error {
func ErorrTokenExpired() error {
return wrap(ErrTokenExpired, "verification token expired")
}
func ErrorCooldownActive() error {
return wrap(ErrCooldownActive, "token creation cooldown is active")
}
func ErrorTokenAttemptsExceeded() error {
return wrap(ErrTokenAttemptsExceeded, "verification token max attempts exceeded")
}
func ErrorIdempotencyConflict() error {
return wrap(ErrIdempotencyConflict, "verification token request idempotency key has already been used")
}

View File

@@ -0,0 +1,70 @@
package verification
import (
"strings"
"time"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
type TokenKind = string
const (
TokenKindOTP TokenKind = "otp"
TokenKindLink TokenKind = "link"
)
type Request struct {
AccountRef bson.ObjectID
Purpose model.VerificationPurpose
Target string
Ttl time.Duration
Kind TokenKind
MaxRetries *int
Cooldown *time.Duration
IdempotencyKey *string // Optional key to make Create idempotent for retries.
}
func newRequest(accountRef bson.ObjectID, purpose model.VerificationPurpose, target string, kind TokenKind) *Request {
return &Request{
AccountRef: accountRef,
Purpose: purpose,
Target: target,
Kind: kind,
Ttl: 15 * time.Minute, // default TTL for verification tokens
}
}
func NewLinkRequest(accountRef bson.ObjectID, purpose model.VerificationPurpose, target string) *Request {
return newRequest(accountRef, purpose, target, TokenKindLink)
}
func NewOTPRequest(accountRef bson.ObjectID, purpose model.VerificationPurpose, target string) *Request {
return newRequest(accountRef, purpose, target, TokenKindOTP)
}
func (r *Request) WithTTL(ttl time.Duration) *Request {
r.Ttl = ttl
return r
}
func (r *Request) WithMaxRetries(maxRetries int) *Request {
r.MaxRetries = &maxRetries
return r
}
func (r *Request) WithCooldown(cooldown time.Duration) *Request {
r.Cooldown = &cooldown
return r
}
func (r *Request) WithIdempotencyKey(key string) *Request {
normalized := strings.TrimSpace(key)
if normalized == "" {
r.IdempotencyKey = nil
return r
}
r.IdempotencyKey = &normalized
return r
}

View File

@@ -2,7 +2,6 @@ package verification
import (
"context"
"time"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
@@ -10,12 +9,6 @@ import (
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)
Create(ctx context.Context, request *Request) (verificationToken string, err error)
Consume(ctx context.Context, accountRef bson.ObjectID, purpose model.VerificationPurpose, verificationToken string) (*model.VerificationToken, error)
}

View File

@@ -16,7 +16,7 @@ require (
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0
go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.47.0
golang.org/x/crypto v0.48.0
google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.11
)
@@ -87,11 +87,11 @@ require (
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -204,8 +204,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -218,8 +218,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -246,14 +246,14 @@ 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=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -271,8 +271,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -31,10 +31,10 @@ func (ccn *ConfirmationCodeNotification) Serialize() ([]byte, error) {
}
func newConfirmationEvent(action nm.NotificationAction) model.NotificationEvent {
return model.NewNotification(mservice.Confirmations, action)
return model.NewNotification(mservice.Verification, action)
}
func NewConfirmationCodeEnvelope(sender string, accountRef bson.ObjectID, destination string, target model.ConfirmationTarget, code string) messaging.Envelope {
func NewConfirmationCodeEnvelope(sender string, accountRef bson.ObjectID, destination string, target model.VerificationPurpose, code string) messaging.Envelope {
return &ConfirmationCodeNotification{
Envelope: messaging.CreateEnvelope(sender, newConfirmationEvent(nm.NAPending)),
payload: confirmationCodePayload{

View File

@@ -42,10 +42,12 @@ func (ccp *ConfirmationCodeProcessor) Process(ctx context.Context, envelope me.E
return err
}
target := model.ConfirmationTarget(msg.Target)
if target != model.ConfirmationTargetLogin && target != model.ConfirmationTargetPayout {
return merrors.InvalidArgument("invalid confirmation target", "target")
target, err := model.VPFromString(msg.Target)
if err != nil {
ccp.logger.Warn("Failed to parse confirmation target from envelope", zap.Error(err), zap.String("topic", ccp.event.ToString()), zap.String("target", msg.Target))
return err
}
if msg.Code == "" {
return merrors.InvalidArgument("empty confirmation code", "code")
}

View File

@@ -51,7 +51,7 @@ func confirmationResultEvent(sourceService, rail string) model.NotificationEvent
rail = "default"
}
rail = strings.ToLower(rail)
return model.NewNotification(mservice.Confirmations, nm.NotificationAction(action+"."+rail))
return model.NewNotification(mservice.Verification, nm.NotificationAction(action+"."+rail))
}
func NewConfirmationRequestEnvelope(sender string, request *model.ConfirmationRequest) messaging.Envelope {

View File

@@ -11,7 +11,7 @@ import (
"go.mongodb.org/mongo-driver/v2/bson"
)
func Code(sender string, accountRef bson.ObjectID, destination string, target model.ConfirmationTarget, code string) messaging.Envelope {
func Code(sender string, accountRef bson.ObjectID, destination string, target model.VerificationPurpose, code string) messaging.Envelope {
return cinternal.NewConfirmationCodeEnvelope(sender, accountRef, destination, target, code)
}

View File

@@ -6,4 +6,4 @@ import (
"github.com/tech/sendico/pkg/model"
)
type ConfirmationCodeHandler = func(context.Context, *model.Account, string, model.ConfirmationTarget, string) error
type ConfirmationCodeHandler = func(context.Context, *model.Account, string, model.VerificationPurpose, string) error

View File

@@ -1,36 +0,0 @@
package model
import (
"time"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/mservice"
"go.mongodb.org/mongo-driver/v2/bson"
)
type ConfirmationTarget string
const (
ConfirmationTargetLogin ConfirmationTarget = "login"
ConfirmationTargetPayout ConfirmationTarget = "payout"
)
type ConfirmationCode struct {
storable.Base `bson:",inline" json:",inline"`
AccountRef bson.ObjectID `bson:"accountRef" json:"accountRef"`
Destination string `bson:"destination" json:"destination"`
Target ConfirmationTarget `bson:"target" json:"target"`
CodeHash []byte `bson:"codeHash" json:"codeHash,omitempty"`
Salt []byte `bson:"salt" json:"salt,omitempty"`
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
MaxAttempts int `bson:"maxAttempts" json:"maxAttempts"`
ResendLimit int `bson:"resendLimit" json:"resendLimit"`
CooldownUntil time.Time `bson:"cooldownUntil" json:"cooldownUntil"`
Used bool `bson:"used" json:"used"`
Attempts int `bson:"attempts" json:"attempts"`
ResendCount int `bson:"resendCount" json:"resendCount"`
}
func (*ConfirmationCode) Collection() string {
return mservice.Confirmations
}

View File

@@ -106,7 +106,7 @@ func StringToNotificationEvent(eventType, eventAction string) (NotificationEvent
if err == nil {
return NewNotification(et, ea), nil
}
if et == mservice.Confirmations {
if et == mservice.Verification {
action := strings.TrimSpace(eventAction)
if action == "" {
return nil, err

View File

@@ -1,9 +1,11 @@
package model
import (
"fmt"
"time"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/merrors"
"go.mongodb.org/mongo-driver/v2/bson"
)
@@ -13,18 +15,45 @@ const (
PurposeAccountActivation VerificationPurpose = "account_activation"
PurposeEmailChange VerificationPurpose = "email_change"
PurposePasswordReset VerificationPurpose = "password_reset"
PurposeSensitiveAction VerificationPurpose = "sensitive_action"
PurposeMagicLogin VerificationPurpose = "magic_login"
PurposeLogin VerificationPurpose = "login"
PurposePayout VerificationPurpose = "payout"
)
func VPToString(code VerificationPurpose) string {
return string(code)
}
func VPFromString(s string) (VerificationPurpose, error) {
switch s {
case string(PurposeAccountActivation):
return PurposeAccountActivation, nil
case string(PurposeEmailChange):
return PurposeEmailChange, nil
case string(PurposePasswordReset):
return PurposePasswordReset, nil
case string(PurposeMagicLogin):
return PurposeMagicLogin, nil
case string(PurposeLogin):
return PurposeLogin, nil
case string(PurposePayout):
return PurposePayout, nil
default:
return "", merrors.InvalidArgument(fmt.Sprintf("invalid verification purpose: %s", s))
}
}
type VerificationToken struct {
storable.Base `bson:",inline" json:",inline"`
ArchivableBase `bson:",inline" json:",inline"`
storable.Base `bson:",inline" json:",inline"`
Target string `bson:"target,omitempty" json:"target"`
AccountRef bson.ObjectID `bson:"accountRef" json:"accountRef"`
Purpose VerificationPurpose `bson:"purpose" json:"purpose"`
IdempotencyKey *string `bson:"idempotencyKey,omitempty" json:"-"`
VerifyTokenHash string `bson:"verifyTokenHash" json:"-"`
Salt *string `bson:"salt,omitempty" json:"-"`
UsedAt *time.Time `bson:"usedAt,omitempty" json:"-"`
ExpiresAt time.Time `bson:"expiresAt" json:"-"`
MaxRetries *int `bson:"maxRetries,omitempty" json:"-"`
Attempts int `bson:"attempts" json:"-"`
}

View File

@@ -6,7 +6,7 @@ type Type = string
const (
Accounts Type = "accounts" // Represents user accounts in the system
Confirmations Type = "confirmations" // Represents confirmation code flows
Verification Type = "verification" // Represents verification code flows
Amplitude Type = "amplitude" // Represents analytics integration with Amplitude
Discovery Type = "discovery" // Represents service discovery registry
Site Type = "site" // Represents public site endpoints
@@ -57,7 +57,7 @@ const (
func StringToSType(s string) (Type, error) {
switch Type(s) {
case Accounts, Confirmations, Amplitude, Site, Changes, Clients, ChainGateway, ChainWallets, ChainWalletBalances,
case Accounts, Verification, Amplitude, Site, Changes, Clients, ChainGateway, ChainWallets, ChainWalletBalances,
ChainTransfers, ChainDeposits, MntxGateway, PaymentGateway, FXOracle, FeePlans, BillingDocuments, FilterProjects, Invitations, Invoices, Logo, Ledger,
LedgerAccounts, LedgerBalances, LedgerEntries, LedgerOutbox, LedgerParties, LedgerPlines, Notifications,
Organizations, Payments, PaymentRoutes, PaymentPlanTemplates, PaymentOrchestrator, Permissions, Policies, PolicyAssignements,

View File

@@ -54,12 +54,12 @@ func GetAccountBoundObjects[T any](
if err != nil {
if !errors.Is(err, merrors.ErrNoData) {
logger.Warn("Failed to fetch account bound objects", zap.Error(err),
mzap.ObjRef("account_ref", accountRef),
mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef),
)
} else {
logger.Debug("No matching account bound objects found", zap.Error(err),
mzap.ObjRef("account_ref", accountRef),
mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef),
)
}
@@ -80,7 +80,7 @@ func GetAccountBoundObjects[T any](
logger.Debug("Successfully retrieved account bound objects",
zap.Int("total_count", len(allObjects)),
mzap.ObjRef("account_ref", accountRef),
mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef),
zap.Any("objs", allObjects),
)

View File

@@ -29,17 +29,17 @@ func GetProtectedObjects[T any](
refs, err := repo.ListPermissionBound(ctx, repository.ApplyCursor(filter, cursor))
if err != nil {
if !errors.Is(err, merrors.ErrNoData) {
logger.Warn("Failed to fetch object IDs", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
logger.Warn("Failed to fetch object IDs", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), zap.String("action", string(action)))
} else {
logger.Debug("No matching IDs found", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
logger.Debug("No matching IDs found", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), zap.String("action", string(action)))
}
return nil, err
}
res, err := enforcer.EnforceBatch(ctx, refs, accountRef, action)
if err != nil {
logger.Warn("Failed to enforce object IDs", zap.Error(err), mzap.ObjRef("account_ref", accountRef),
logger.Warn("Failed to enforce object IDs", zap.Error(err), mzap.AccRef(accountRef),
mzap.ObjRef("organization_ref", organizationRef), zap.String("action", string(action)))
return nil, err
}

View File

@@ -73,22 +73,22 @@ func (m *AccountManager) DeleteOrganization(ctx context.Context, orgRef bson.Obj
// DeleteAccount deletes an account and all its associated data
// The caller is responsible for wrapping this in a transaction
func (m *AccountManager) DeleteAccount(ctx context.Context, accountRef bson.ObjectID) error {
m.logger.Debug("Deleting account", mzap.ObjRef("account_ref", accountRef))
m.logger.Debug("Deleting account", mzap.AccRef(accountRef))
// Delete the account
if err := m.accountDB.Delete(ctx, accountRef); err != nil {
m.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
m.logger.Warn("Failed to delete account", zap.Error(err), mzap.AccRef(accountRef))
return err
}
m.logger.Info("Successfully deleted account", mzap.ObjRef("account_ref", accountRef))
m.logger.Info("Successfully deleted account", mzap.AccRef(accountRef))
return nil
}
// DeleteAll deletes all data for a given account and organization
// The caller is responsible for wrapping this in a transaction
func (m *AccountManager) DeleteAll(ctx context.Context, accountRef, organizationRef bson.ObjectID) error {
m.logger.Debug("Deleting all data", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef))
m.logger.Debug("Deleting all data", mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
// Delete organization first (which will cascade delete all related data)
if err := m.DeleteOrganization(ctx, organizationRef); err != nil {
@@ -98,11 +98,11 @@ func (m *AccountManager) DeleteAll(ctx context.Context, accountRef, organization
// Delete account
if err := m.DeleteAccount(ctx, accountRef); err != nil {
m.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
m.logger.Warn("Failed to delete account", zap.Error(err), mzap.AccRef(accountRef))
return err
}
m.logger.Info("Successfully deleted all data", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef))
m.logger.Info("Successfully deleted all data", mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef))
return nil
}

View File

@@ -0,0 +1,17 @@
package mask
import "strings"
func Email(email string) string {
parts := strings.Split(email, "@")
if len(parts) != 2 {
return email
}
local := parts[0]
if len(local) > 2 {
local = local[:1] + "***" + local[len(local)-1:]
} else {
local = local[:1] + "***"
}
return local + "@" + parts[1]
}

View File

@@ -0,0 +1,20 @@
package mzap
import (
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mask"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func AccRef(accountRef bson.ObjectID) zap.Field {
return ObjRef("account_ref", accountRef)
}
func Email(email string) zap.Field {
return zap.String("email", mask.Email(email))
}
func Login(account *model.Account) zap.Field {
return Email(account.Login)
}

View File

@@ -13,7 +13,3 @@ 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

@@ -1,6 +1,6 @@
module github.com/tech/sendico/server
go 1.25.6
go 1.25.7
replace github.com/tech/sendico/pkg => ../pkg
@@ -31,7 +31,7 @@ require (
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0
go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1
golang.org/x/net v0.49.0
golang.org/x/net v0.50.0
google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
@@ -134,9 +134,9 @@ require (
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sync v0.19.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
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
)

View File

@@ -292,8 +292,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -308,8 +308,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -336,14 +336,14 @@ 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=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -361,10 +361,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -155,14 +155,18 @@ func (s *service) CreateAccount(
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))
) (string, error) {
token, err := s.vdb.Create(
ctx,
verification.NewLinkRequest(*acct.GetID(), model.PurposeAccountActivation, "").
WithTTL(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
return token, nil
}
@@ -174,19 +178,19 @@ func (s *service) DeleteAccount(
// Check if this is the only member in the organization
if len(org.Members) <= 1 {
s.logger.Warn("Cannot delete account - it's the only member in the organization",
mzap.ObjRef("account_ref", accountRef), mzap.StorableRef(org))
mzap.AccRef(accountRef), mzap.StorableRef(org))
return merrors.InvalidArgument("Cannot delete the only member of an organization")
}
// 1) Remove from organization
if err := s.RemoveAccountFromOrganization(ctx, org, accountRef); err != nil {
s.logger.Warn("Failed to revoke account role", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
s.logger.Warn("Failed to revoke account role", zap.Error(err), mzap.AccRef(accountRef))
return err
}
// 2) Delete the account document
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.AccRef(accountRef))
return err
}
return nil
@@ -203,13 +207,13 @@ func (s *service) RemoveAccountFromOrganization(
roles, err := s.enforcer.GetRoles(ctx, accountRef, org.ID)
if err != nil {
s.logger.Warn("Failed to fetch account permissions", zap.Error(err), mzap.StorableRef(org),
mzap.ObjRef("account_ref", accountRef))
mzap.AccRef(accountRef))
return err
}
for _, role := range roles {
if err := s.roleManager.Revoke(ctx, role.DescriptionRef, accountRef, org.ID); err != nil {
s.logger.Warn("Failed to revoke account role", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("role_ref", role.DescriptionRef))
mzap.AccRef(accountRef), mzap.ObjRef("role_ref", role.DescriptionRef))
return err
}
}
@@ -218,7 +222,7 @@ func (s *service) RemoveAccountFromOrganization(
// Remove the member by slicing it out
org.Members = append(org.Members[:i], org.Members[i+1:]...)
if err := s.orgDB.Update(ctx, accountRef, org); err != nil {
s.logger.Warn("Failed to remove member from organization", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
s.logger.Warn("Failed to remove member from organization", zap.Error(err), mzap.AccRef(accountRef))
return err
}
break
@@ -231,7 +235,11 @@ func (s *service) ResetPassword(
ctx context.Context,
acct *model.Account,
) (string, error) {
return s.vdb.Create(ctx, *acct.GetID(), model.PurposePasswordReset, "", time.Duration(time.Hour*1))
return s.vdb.Create(
ctx,
verification.NewOTPRequest(*acct.GetID(), model.PurposePasswordReset, "").
WithTTL(time.Duration(time.Hour*1)),
)
}
func (s *service) UpdateLogin(
@@ -239,7 +247,11 @@ func (s *service) UpdateLogin(
acct *model.Account,
newLogin string,
) (string, error) {
return s.vdb.Create(ctx, *acct.GetID(), model.PurposeEmailChange, newLogin, time.Duration(time.Hour*1))
return s.vdb.Create(
ctx,
verification.NewOTPRequest(*acct.GetID(), model.PurposeEmailChange, newLogin).
WithTTL(time.Duration(time.Hour*1)),
)
}
func (s *service) JoinOrganization(
@@ -350,7 +362,7 @@ func (s *service) DeleteAll(
accountRef bson.ObjectID,
) error {
s.logger.Info("Starting complete deletion (organization + account)",
mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
mzap.StorableRef(org), mzap.AccRef(accountRef))
// 1. First delete the organization and all its data
if err := s.DeleteOrganization(ctx, org); err != nil {
@@ -359,11 +371,11 @@ func (s *service) DeleteAll(
// 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))
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.AccRef(accountRef))
return err
}
s.logger.Info("Complete deletion successful", mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
s.logger.Info("Complete deletion successful", mzap.StorableRef(org), mzap.AccRef(accountRef))
return nil
}

View File

@@ -12,10 +12,9 @@ type pendingLoginResponse struct {
Account accountResponse `json:"account"`
PendingToken TokenData `json:"pendingToken"`
Destination string `json:"destination"`
TTLSeconds int `json:"ttlSeconds"`
}
func LoginPending(logger mlogger.Logger, account *model.Account, pendingToken *TokenData, destination string, ttlSeconds int) http.HandlerFunc {
func LoginPending(logger mlogger.Logger, account *model.Account, pendingToken *TokenData, destination string) http.HandlerFunc {
return response.Accepted(
logger,
&pendingLoginResponse{
@@ -25,7 +24,6 @@ func LoginPending(logger mlogger.Logger, account *model.Account, pendingToken *T
},
PendingToken: *pendingToken,
Destination: destination,
TTLSeconds: ttlSeconds,
},
)
}

View File

@@ -1,11 +1,11 @@
package confirmation
package verification
import (
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/internal/server/confirmationimp"
"github.com/tech/sendico/server/internal/server/verificationimp"
)
func Create(a api.API) (mservice.MicroService, error) {
return confirmationimp.CreateAPI(a)
return verificationimp.CreateAPI(a)
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/interface/services/account"
"github.com/tech/sendico/server/interface/services/confirmation"
"github.com/tech/sendico/server/interface/services/invitation"
"github.com/tech/sendico/server/interface/services/ledger"
"github.com/tech/sendico/server/interface/services/logo"
@@ -22,6 +21,7 @@ import (
"github.com/tech/sendico/server/interface/services/permission"
"github.com/tech/sendico/server/interface/services/recipient"
"github.com/tech/sendico/server/interface/services/site"
"github.com/tech/sendico/server/interface/services/verification"
"github.com/tech/sendico/server/interface/services/wallet"
"go.uber.org/zap"
)
@@ -82,7 +82,7 @@ func (a *APIImp) installServices() error {
srvf := make([]api.MicroServiceFactoryT, 0)
srvf = append(srvf, account.Create)
srvf = append(srvf, confirmation.Create)
srvf = append(srvf, verification.Create)
srvf = append(srvf, organization.Create)
srvf = append(srvf, invitation.Create)
srvf = append(srvf, logo.Create)

View File

@@ -137,7 +137,7 @@ func CreateMiddleware(logger mlogger.Logger, db db.Factory, enforcer auth.Enforc
return nil, err
}
cdb, err := db.NewConfirmationsDB()
cdb, err := db.NewVerificationsDB()
if err != nil {
p.logger.Error("Failed to create confirmations database", zap.Error(err))
return nil, err

View File

@@ -38,12 +38,12 @@ func (ar *AuthorizedRouter) tokenHandler(service mservice.Type, endpoint string,
func (ar *AuthorizedRouter) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) {
hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc {
if t.Pending {
return response.Forbidden(ar.logger, ar.service, "confirmation_required", "pending token requires confirmation")
return response.Unauthorized(ar.logger, ar.service, "additional verification required")
}
var a model.Account
if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil {
if errors.Is(err, merrors.ErrNoData) {
ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.ObjRef("account_ref", t.AccountRef))
ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.AccRef(t.AccountRef))
return response.NotFound(ar.logger, ar.service, err.Error())
}
return response.Internal(ar.logger, ar.service, err)
@@ -63,7 +63,7 @@ func (ar *AuthorizedRouter) PendingAccountHandler(service mservice.Type, endpoin
var a model.Account
if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil {
if errors.Is(err, merrors.ErrNoData) {
ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.ObjRef("account_ref", t.AccountRef))
ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.AccRef(t.AccountRef))
return response.NotFound(ar.logger, ar.service, err.Error())
}
return response.Internal(ar.logger, ar.service, err)

View File

@@ -8,8 +8,8 @@ import (
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/account"
"github.com/tech/sendico/pkg/db/confirmation"
"github.com/tech/sendico/pkg/db/refreshtokens"
"github.com/tech/sendico/pkg/db/verification"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api/sresponse"
@@ -36,7 +36,7 @@ func (d *Dispatcher) PendingAccountHandler(service mservice.Type, endpoint strin
d.protected.PendingAccountHandler(service, endpoint, method, handler)
}
func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, cdb confirmation.DB, rtdb refreshtokens.DB, enforcer auth.Enforcer, config *middleware.Config) *Dispatcher {
func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, vdb verification.DB, rtdb refreshtokens.DB, enforcer auth.Enforcer, config *middleware.Config) *Dispatcher {
d := &Dispatcher{
logger: logger.Named("api_dispatcher"),
}
@@ -45,7 +45,7 @@ func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, cdb
endpoint := os.Getenv(config.EndPointEnv)
signature := middleware.SignatureConf(config)
router.Group(func(r chi.Router) {
d.public = rpublic.NewRouter(d.logger, endpoint, db, cdb, rtdb, r, &config.Token, &signature)
d.public = rpublic.NewRouter(d.logger, endpoint, db, vdb, rtdb, r, &config.Token, &signature)
})
router.Group(func(r chi.Router) {
d.protected = rauthorized.NewRouter(d.logger, endpoint, r, db, enforcer, &config.Token, &signature)

View File

@@ -6,15 +6,13 @@ import (
"errors"
"net/http"
"strings"
"time"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mask"
"github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse"
"github.com/tech/sendico/server/internal/server/confirmationimp"
"go.uber.org/zap"
)
@@ -47,15 +45,7 @@ func (pr *PublicRouter) logUserIn(ctx context.Context, _ *http.Request, req *sre
return response.Internal(pr.logger, pr.service, err)
}
cfg := confirmationimp.DefaultConfig()
_, rec, err := pr.cstore.Create(ctx, account.ID, account.Login, model.ConfirmationTargetLogin, cfg, pr.generateCode)
if err != nil {
pr.logger.Warn("Failed to create login confirmation code", zap.Error(err))
return response.Internal(pr.logger, pr.service, err)
}
pr.logger.Info("Login confirmation code issued", zap.String("destination", pr.maskEmail(account.Login)))
return sresponse.LoginPending(pr.logger, account, &pendingToken, pr.maskEmail(account.Login), int(time.Until(rec.ExpiresAt).Seconds()))
return sresponse.LoginPending(pr.logger, account, &pendingToken, mask.Email(account.Login))
}
func (a *PublicRouter) login(r *http.Request) http.HandlerFunc {

View File

@@ -1,27 +1,21 @@
package routers
import (
"crypto/rand"
"strings"
"github.com/go-chi/chi/v5"
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/db/account"
"github.com/tech/sendico/pkg/db/confirmation"
"github.com/tech/sendico/pkg/db/refreshtokens"
"github.com/tech/sendico/pkg/db/verification"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api/sresponse"
"github.com/tech/sendico/server/interface/middleware"
re "github.com/tech/sendico/server/internal/api/routers/endpoint"
"github.com/tech/sendico/server/internal/server/confirmationimp"
)
type PublicRouter struct {
logger mlogger.Logger
db account.DB
cdb confirmation.DB
cstore *confirmationimp.ConfirmationStore
imp *re.HttpEndpointRouter
rtdb refreshtokens.DB
config middleware.TokenConfig
@@ -33,39 +27,11 @@ func (pr *PublicRouter) InstallHandler(service mservice.Type, endpoint string, m
pr.imp.InstallHandler(service, endpoint, method, handler)
}
func (pr *PublicRouter) generateCode() (string, error) {
const digits = "0123456789"
b := make([]byte, confirmationimp.DefaultConfig().CodeLength)
if _, err := rand.Read(b); err != nil {
return "", err
}
for i := range b {
b[i] = digits[int(b[i])%len(digits)]
}
return string(b), nil
}
func (pr *PublicRouter) maskEmail(email string) string {
parts := strings.Split(email, "@")
if len(parts) != 2 {
return email
}
local := parts[0]
if len(local) > 2 {
local = local[:1] + "***" + local[len(local)-1:]
} else {
local = local[:1] + "***"
}
return local + "@" + parts[1]
}
func NewRouter(logger mlogger.Logger, apiEndpoint string, db account.DB, cdb confirmation.DB, rtdb refreshtokens.DB, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *PublicRouter {
func NewRouter(logger mlogger.Logger, apiEndpoint string, db account.DB, vdb verification.DB, rtdb refreshtokens.DB, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *PublicRouter {
l := logger.Named("public")
hr := PublicRouter{
logger: l,
db: db,
cdb: cdb,
cstore: confirmationimp.NewStore(cdb),
rtdb: rtdb,
config: *config,
signature: *signature,

View File

@@ -45,7 +45,7 @@ func (pr *PublicRouter) validateRefreshToken(ctx context.Context, _ *http.Reques
var account model.Account
if err := pr.db.Get(ctx, *rt.AccountRef, &account); errors.Is(err, merrors.ErrNoData) {
pr.logger.Info("User not found while rotating refresh token", zap.Error(err), mzap.ObjRef("account_ref", *rt.AccountRef))
pr.logger.Info("User not found while rotating refresh token", zap.Error(err), mzap.AccRef(*rt.AccountRef))
return nil, nil, merrors.Unauthorized("user not found")
}

View File

@@ -0,0 +1,45 @@
package mutil
import (
"errors"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/db/verification"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
"go.uber.org/zap"
)
func MapTokenErrorToResponse(logger mlogger.Logger, service mservice.Type, err error) http.HandlerFunc {
if errors.Is(err, verification.ErrTokenNotFound) {
logger.Debug("Verification token not found during consume", zap.Error(err))
return response.NotFound(logger, service, "No account found associated with given verifcation token")
}
if errors.Is(err, verification.ErrTokenExpired) {
logger.Debug("Verification token expired during consume", zap.Error(err))
return response.Gone(logger, service, "token_expired", "verification token has expired")
}
if errors.Is(err, verification.ErrTokenAlreadyUsed) {
logger.Debug("Verification token already used during consume", zap.Error(err))
return response.DataConflict(logger, service, "verification token has already been used")
}
if errors.Is(err, verification.ErrTokenAttemptsExceeded) {
logger.Debug("Verification token attempts exceeded", zap.Error(err))
return response.Forbidden(logger, service, "code_attempts_exceeded", "verification token has already been used")
}
if errors.Is(err, verification.ErrCooldownActive) {
logger.Debug("Cooldown is still active", zap.Error(err))
return response.TooManyRequests(logger, service, "verification token can't be generated yet, cooldown is still active")
}
if errors.Is(err, verification.ErrIdempotencyConflict) {
logger.Debug("Verification idempotency key conflict", zap.Error(err))
return response.DataConflict(logger, service, "verification request was already processed")
}
if err != nil {
logger.Warn("Unexpected error during token verification", zap.Error(err))
return response.Auto(logger, service, err)
}
logger.Debug("No token verification error found")
return response.Success(logger)
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
@@ -17,7 +18,7 @@ func (a *AccountAPI) verify(r *http.Request) http.HandlerFunc {
// Get user
ctx := r.Context()
// Delete verification token to confirm account
t, err := a.vdb.Consume(ctx, token)
t, err := a.vdb.Consume(ctx, bson.NilObjectID, model.PurposeAccountActivation, token)
if err != nil {
a.logger.Debug("Failed to consume verification token", zap.Error(err))
return a.mapTokenErrorToResponse(err)

View File

@@ -128,7 +128,7 @@ func (a *AccountAPI) resetPassword(r *http.Request) http.HandlerFunc {
return response.Auto(a.logger, a.Name(), err)
}
t, err := a.vdb.Consume(ctx, token)
t, err := a.vdb.Consume(ctx, accountRef, model.PurposePasswordReset, token)
if err != nil {
a.logger.Warn("Failed to consume password reset token", zap.Error(err), zap.String("token", token))
return a.mapTokenErrorToResponse(err)

View File

@@ -1,48 +0,0 @@
package confirmationimp
import (
"encoding/json"
"net/http"
"time"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
emodel "github.com/tech/sendico/server/interface/model"
"go.uber.org/zap"
)
func (a *ConfirmationAPI) requestCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc {
var req confirmationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
a.logger.Warn("Failed to decode confirmation request", zap.Error(err))
return response.BadPayload(a.logger, a.Name(), err)
}
target, err := a.parseTarget(req.Target)
if err != nil {
return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error())
}
if target == model.ConfirmationTargetLogin && (token == nil || !token.Pending) {
return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token")
}
destination := a.resolveDestination(req.Destination, account)
if destination == "" {
return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required")
}
code, rec, err := a.store.Create(r.Context(), account.ID, destination, target, a.config, a.generateCode)
if err != nil {
a.logger.Warn("Failed to create confirmation code", zap.Error(err), mzap.ObjRef("account_ref", account.ID))
return response.Internal(a.logger, a.Name(), err)
}
a.sendCode(account, target, destination, code)
return response.Accepted(a.logger, confirmationResponse{
TTLSeconds: int(time.Until(rec.ExpiresAt).Seconds()),
CooldownSeconds: int(a.config.Cooldown.Seconds()),
Destination: maskEmail(destination),
})
}

View File

@@ -1,56 +0,0 @@
package confirmationimp
import (
"encoding/json"
"errors"
"net/http"
"time"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
emodel "github.com/tech/sendico/server/interface/model"
"go.uber.org/zap"
)
func (a *ConfirmationAPI) resendCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc {
var req confirmationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
a.logger.Warn("Failed to decode confirmation resend request", zap.Error(err))
return response.BadPayload(a.logger, a.Name(), err)
}
target, err := a.parseTarget(req.Target)
if err != nil {
return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error())
}
if target == model.ConfirmationTargetLogin && (token == nil || !token.Pending) {
return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token")
}
destination := a.resolveDestination(req.Destination, account)
if destination == "" {
return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required")
}
code, rec, err := a.store.Resend(r.Context(), account.ID, destination, target, a.config, a.generateCode)
switch {
case errors.Is(err, errConfirmationNotFound):
return response.NotFound(a.logger, a.Name(), "no_active_code_for_resend")
case errors.Is(err, errConfirmationCooldown):
return response.Forbidden(a.logger, a.Name(), "cooldown_active", "please wait before requesting another code")
case errors.Is(err, errConfirmationResendLimit):
return response.Forbidden(a.logger, a.Name(), "resend_limit_reached", "too many resend attempts")
case err != nil:
a.logger.Warn("Failed to resend confirmation code", zap.Error(err), mzap.ObjRef("account_ref", account.ID))
return response.Internal(a.logger, a.Name(), err)
}
a.sendCode(account, target, destination, code)
return response.Accepted(a.logger, confirmationResponse{
TTLSeconds: int(time.Until(rec.ExpiresAt).Seconds()),
CooldownSeconds: int(a.config.Cooldown.Seconds()),
Destination: maskEmail(destination),
})
}

View File

@@ -1,158 +0,0 @@
package confirmationimp
import (
"context"
"crypto/rand"
"fmt"
"strings"
"time"
"github.com/go-chi/jwtauth/v5"
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/db/refreshtokens"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/messaging"
cnotifications "github.com/tech/sendico/pkg/messaging/notifications/confirmation"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
eapi "github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/interface/api/sresponse"
"github.com/tech/sendico/server/interface/middleware"
emodel "github.com/tech/sendico/server/interface/model"
"go.uber.org/zap"
)
type Config struct {
CodeLength int
TTL time.Duration
MaxAttempts int
Cooldown time.Duration
ResendLimit int
}
func defaultConfig() Config {
return Config{
CodeLength: 6,
TTL: 10 * time.Minute,
MaxAttempts: 5,
Cooldown: time.Minute,
ResendLimit: 5,
}
}
func DefaultConfig() Config {
return defaultConfig()
}
type ConfirmationAPI struct {
logger mlogger.Logger
config Config
store *ConfirmationStore
rtdb refreshtokens.DB
producer messaging.Producer
tokenConfig middleware.TokenConfig
signature middleware.Signature
}
func (a *ConfirmationAPI) Name() mservice.Type {
return mservice.Confirmations
}
func (a *ConfirmationAPI) Finish(_ context.Context) error {
return nil
}
func CreateAPI(a eapi.API) (*ConfirmationAPI, error) {
cdb, err := a.DBFactory().NewConfirmationsDB()
if err != nil {
return nil, err
}
rtdb, err := a.DBFactory().NewRefreshTokensDB()
if err != nil {
return nil, err
}
p := &ConfirmationAPI{
logger: a.Logger().Named(mservice.Confirmations),
config: defaultConfig(),
store: NewStore(cdb),
rtdb: rtdb,
producer: a.Register().Messaging().Producer(),
tokenConfig: a.Config().Mw.Token,
signature: middleware.SignatureConf(a.Config().Mw),
}
a.Register().PendingAccountHandler(p.Name(), "/", api.Post, p.requestCode)
a.Register().PendingAccountHandler(p.Name(), "/resend", api.Post, p.resendCode)
a.Register().PendingAccountHandler(p.Name(), "/verify", api.Post, p.verifyCode)
return p, nil
}
func (a *ConfirmationAPI) generateCode() (string, error) {
const digits = "0123456789"
b := make([]byte, a.config.CodeLength)
_, err := rand.Read(b)
if err != nil {
return "", err
}
for i := range b {
b[i] = digits[int(b[i])%len(digits)]
}
return string(b), nil
}
func (a *ConfirmationAPI) parseTarget(raw string) (model.ConfirmationTarget, error) {
switch strings.ToLower(strings.TrimSpace(raw)) {
case string(model.ConfirmationTargetLogin):
return model.ConfirmationTargetLogin, nil
case string(model.ConfirmationTargetPayout):
return model.ConfirmationTargetPayout, nil
default:
return "", merrors.InvalidArgument(fmt.Sprintf("unsupported target '%s'", raw), "target")
}
}
func (a *ConfirmationAPI) resolveDestination(reqDest string, account *model.Account) string {
destination := strings.ToLower(strings.TrimSpace(reqDest))
if destination == "" && account != nil {
destination = strings.ToLower(strings.TrimSpace(account.Login))
}
return destination
}
func (a *ConfirmationAPI) sendCode(account *model.Account, target model.ConfirmationTarget, destination, code string) {
a.logger.Info("Confirmation code generated",
zap.String("target", string(target)),
zap.String("destination", maskEmail(destination)),
mzap.ObjRef("account_ref", account.ID))
if err := a.producer.SendMessage(cnotifications.Code(a.Name(), account.ID, destination, target, code)); err != nil {
a.logger.Warn("Failed to send confirmation code notification", zap.Error(err), mzap.ObjRef("account_ref", account.ID))
}
a.logger.Debug("Confirmation code debug dump", zap.String("code", code))
}
func maskEmail(email string) string {
parts := strings.Split(email, "@")
if len(parts) != 2 {
return email
}
local := parts[0]
if len(local) > 2 {
local = local[:1] + "***" + local[len(local)-1:]
} else {
local = local[:1] + "***"
}
return local + "@" + parts[1]
}
func (a *ConfirmationAPI) createAccessToken(account *model.Account) (sresponse.TokenData, error) {
ja := jwtauth.New(a.signature.Algorithm, a.signature.PrivateKey, a.signature.PublicKey)
_, res, err := ja.Encode(emodel.Account2Claims(account, a.tokenConfig.Expiration.Account))
token := sresponse.TokenData{
Token: res,
Expiration: time.Now().Add(time.Duration(a.tokenConfig.Expiration.Account) * time.Hour),
}
return token, err
}

View File

@@ -1,187 +0,0 @@
package confirmationimp
import (
"context"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"errors"
"time"
"github.com/tech/sendico/pkg/db/confirmation"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
var (
errConfirmationNotFound confirmationError = "confirmation not found or expired"
errConfirmationUsed confirmationError = "confirmation already used"
errConfirmationMismatch confirmationError = "confirmation code mismatch"
errConfirmationAttemptsExceeded confirmationError = "confirmation attempts exceeded"
errConfirmationCooldown confirmationError = "confirmation cooldown active"
errConfirmationResendLimit confirmationError = "confirmation resend limit reached"
)
type confirmationError string
func (e confirmationError) Error() string {
return string(e)
}
type ConfirmationStore struct {
db confirmation.DB
}
func NewStore(db confirmation.DB) *ConfirmationStore {
return &ConfirmationStore{db: db}
}
func (s *ConfirmationStore) Create(
ctx context.Context,
accountRef bson.ObjectID,
destination string,
target model.ConfirmationTarget,
cfg Config,
generator func() (string, error),
) (string, *model.ConfirmationCode, error) {
if err := s.db.DeleteTuple(ctx, accountRef, destination, target); err != nil && !errors.Is(err, merrors.ErrNoData) {
return "", nil, err
}
code, _, rec, err := s.buildRecord(accountRef, destination, target, cfg, generator)
if err != nil {
return "", nil, err
}
if err := s.db.Create(ctx, rec); err != nil {
return "", nil, err
}
return code, rec, nil
}
func (s *ConfirmationStore) Resend(
ctx context.Context,
accountRef bson.ObjectID,
destination string,
target model.ConfirmationTarget,
cfg Config,
generator func() (string, error),
) (string, *model.ConfirmationCode, error) {
now := time.Now().UTC()
active, err := s.db.FindActive(ctx, accountRef, destination, target, now.Unix())
if errors.Is(err, merrors.ErrNoData) {
return s.Create(ctx, accountRef, destination, target, cfg, generator)
}
if err != nil {
return "", nil, err
}
if active.ResendCount >= active.ResendLimit {
return "", nil, errConfirmationResendLimit
}
if now.Before(active.CooldownUntil) {
return "", nil, errConfirmationCooldown
}
code, salt, updated, err := s.buildRecord(accountRef, destination, target, cfg, generator)
if err != nil {
return "", nil, err
}
// Preserve attempt counters but bump resend count.
updated.ID = active.ID
updated.CreatedAt = active.CreatedAt
updated.Attempts = active.Attempts
updated.ResendCount = active.ResendCount + 1
updated.Salt = salt
if err := s.db.Update(ctx, updated); err != nil {
return "", nil, err
}
return code, updated, nil
}
func (s *ConfirmationStore) Verify(
ctx context.Context,
accountRef bson.ObjectID,
destination string,
target model.ConfirmationTarget,
code string,
) error {
now := time.Now().UTC()
rec, err := s.db.FindActive(ctx, accountRef, destination, target, now.Unix())
if errors.Is(err, merrors.ErrNoData) {
return errConfirmationNotFound
}
if err != nil {
return err
}
if rec.Used {
return errConfirmationUsed
}
rec.Attempts++
if rec.Attempts > rec.MaxAttempts {
rec.Used = true
_ = s.db.Update(ctx, rec)
return errConfirmationAttemptsExceeded
}
if subtle.ConstantTimeCompare(rec.CodeHash, hashCode(rec.Salt, code)) != 1 {
_ = s.db.Update(ctx, rec)
return errConfirmationMismatch
}
rec.Used = true
return s.db.Update(ctx, rec)
}
func (s *ConfirmationStore) buildRecord(
accountRef bson.ObjectID,
destination string,
target model.ConfirmationTarget,
cfg Config,
generator func() (string, error),
) (string, []byte, *model.ConfirmationCode, error) {
code, err := generator()
if err != nil {
return "", nil, nil, err
}
salt, err := newSalt()
if err != nil {
return "", nil, nil, err
}
now := time.Now().UTC()
rec := &model.ConfirmationCode{
AccountRef: accountRef,
Destination: destination,
Target: target,
CodeHash: hashCode(salt, code),
Salt: salt,
ExpiresAt: now.Add(cfg.TTL),
MaxAttempts: cfg.MaxAttempts,
ResendLimit: cfg.ResendLimit,
CooldownUntil: now.Add(cfg.Cooldown),
Used: false,
Attempts: 0,
ResendCount: 0,
}
return code, salt, rec, nil
}
func hashCode(salt []byte, code string) []byte {
h := sha256.New()
h.Write(salt)
h.Write([]byte(code))
return h.Sum(nil)
}
func newSalt() ([]byte, error) {
buf := make([]byte, 16)
if _, err := rand.Read(buf); err != nil {
return nil, err
}
return buf, nil
}

View File

@@ -1,23 +0,0 @@
package confirmationimp
import (
"github.com/tech/sendico/pkg/model"
)
type confirmationRequest struct {
Target string `json:"target"`
Destination string `json:"destination,omitempty"`
}
type confirmationVerifyRequest struct {
Target string `json:"target"`
Code string `json:"code"`
Destination string `json:"destination,omitempty"`
SessionIdentifier model.SessionIdentifier `json:"sessionIdentifier"`
}
type confirmationResponse struct {
TTLSeconds int `json:"ttl_seconds"`
CooldownSeconds int `json:"cooldown_seconds"`
Destination string `json:"destination"`
}

View File

@@ -33,19 +33,19 @@ func (a *PermissionsAPI) changeRole(r *http.Request, account *model.Account, _ *
res, err := a.enforcer.Enforce(ctx, a.rolesPermissionRef, account.ID, orgRef, req.AccountRef, model.ActionUpdate)
if err != nil {
a.logger.Warn("Failed to check permissions while assigning new role", zap.Error(err),
mzap.ObjRef("requesting_account_ref", account.ID), mzap.ObjRef("account_ref", req.AccountRef),
mzap.ObjRef("requesting_account_ref", account.ID), mzap.AccRef(req.AccountRef),
mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef))
}
if !res {
a.logger.Debug("Permission denied to set new role", mzap.ObjRef("requesting_account_ref", account.ID),
mzap.ObjRef("account_ref", req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef))
mzap.AccRef(req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef))
return response.AccessDenied(a.logger, a.Name(), "no permission to change user roles")
}
var roleDescription model.RoleDescription
if err := a.rdb.Get(ctx, req.NewRoleDescriptionRef, &roleDescription); err != nil {
a.logger.Warn("Failed to fetch and validate role description", zap.Error(err), mzap.ObjRef("requesting_account_ref", account.ID),
mzap.ObjRef("account_ref", req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef))
mzap.AccRef(req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef))
return response.Auto(a.logger, a.Name(), err)
}
@@ -57,13 +57,13 @@ func (a *PermissionsAPI) changeRoleImp(ctx context.Context, req *srequest.Change
// TODO: add check that role revocation won't leave venue without the owner
if err != nil {
a.logger.Warn("Failed to fetch account roles", zap.Error(err), mzap.ObjRef("requesting_account_ref", account.ID),
mzap.ObjRef("account_ref", req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef))
mzap.AccRef(req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef))
return response.Auto(a.logger, a.Name(), err)
}
for _, role := range roles {
if err := a.manager.Role().Revoke(ctx, role.DescriptionRef, req.AccountRef, organizationRef); err != nil {
a.logger.Warn("Failed to revoke old role", zap.Error(err), mzap.ObjRef("requesting_account_ref", account.ID),
mzap.ObjRef("account_ref", req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef),
mzap.AccRef(req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef),
mzap.ObjRef("role_ref", role.DescriptionRef))
// continue...
}
@@ -76,7 +76,7 @@ func (a *PermissionsAPI) changeRoleImp(ctx context.Context, req *srequest.Change
}
if err := a.manager.Role().Assign(ctx, &role); err != nil {
a.logger.Warn("Failed to assign new role", zap.Error(err), mzap.ObjRef("requesting_account_ref", account.ID),
mzap.ObjRef("account_ref", req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef),
mzap.AccRef(req.AccountRef), mzap.ObjRef("role_description_ref", req.NewRoleDescriptionRef),
mzap.ObjRef("role_ref", req.NewRoleDescriptionRef))
return response.Auto(a.logger, a.Name(), err)
}

View File

@@ -0,0 +1,57 @@
package verificationimp
import (
"encoding/json"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/db/verification"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mask"
"github.com/tech/sendico/pkg/mutil/mzap"
emodel "github.com/tech/sendico/server/interface/model"
mutil "github.com/tech/sendico/server/internal/mutil/verification"
"go.uber.org/zap"
)
func (a *VerificationAPI) requestCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc {
var req verificationCodeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
a.logger.Warn("Failed to decode confirmation resend request", zap.Error(err))
return response.BadPayload(a.logger, a.Name(), err)
}
purpose, err := model.VPFromString(req.Purpose)
if err != nil {
return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error())
}
if purpose == model.PurposeLogin && (token == nil || !token.Pending) {
return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token")
}
target := a.resolveTarget(req.Destination, account)
if target == "" {
return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required")
}
vReq := verification.NewOTPRequest(account.ID, purpose, target).
WithTTL(a.config.TTL).
WithCooldown(a.config.Cooldown).
WithMaxRetries(a.config.ResendLimit).
WithIdempotencyKey(req.IdempotencyKey)
otp, err := a.store.Create(r.Context(), vReq)
if err != nil {
a.logger.Warn("Failed to create confirmation code for resend", zap.Error(err), mzap.AccRef(account.ID))
return mutil.MapTokenErrorToResponse(a.logger, a.Name(), err)
}
a.sendCode(account, purpose, target, otp)
return response.Accepted(a.logger, verificationResponse{
TTLSeconds: int(vReq.Ttl.Seconds()),
CooldownSeconds: int(a.config.Cooldown.Seconds()),
Destination: mask.Email(target),
IdempotencyKey: req.IdempotencyKey,
})
}

Some files were not shown because too many files have changed in this diff Show More