From 7f540671c1665f8848e803fb01fb460eeda17411 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 10 Feb 2026 01:55:33 +0100 Subject: [PATCH 1/2] unified code verification service --- api/billing/documents/go.mod | 10 +- api/billing/documents/go.sum | 16 +- api/billing/fees/go.mod | 10 +- api/billing/fees/go.sum | 16 +- api/discovery/go.mod | 10 +- api/discovery/go.sum | 16 +- api/fx/ingestor/go.mod | 10 +- api/fx/ingestor/go.sum | 16 +- api/fx/oracle/go.mod | 10 +- api/fx/oracle/go.sum | 16 +- api/fx/storage/go.mod | 6 +- api/fx/storage/go.sum | 12 +- api/gateway/chain/go.mod | 10 +- api/gateway/chain/go.sum | 16 +- api/gateway/mntx/go.mod | 10 +- api/gateway/mntx/go.sum | 16 +- api/gateway/tgsettle/go.mod | 10 +- api/gateway/tgsettle/go.sum | 16 +- api/gateway/tron/go.mod | 12 +- api/gateway/tron/go.sum | 20 +- api/ledger/go.mod | 8 +- api/ledger/go.sum | 16 +- .../internal/service/ledger/account_status.go | 12 +- .../service/ledger/posting_support.go | 6 +- api/ledger/storage/mongo/store/accounts.go | 10 +- api/ledger/storage/mongo/store/balances.go | 6 +- .../storage/mongo/store/posting_lines.go | 4 +- api/notification/go.mod | 10 +- api/notification/go.sum | 16 +- .../server/notificationimp/accountcreated.go | 7 +- .../server/notificationimp/confcode.go | 11 +- .../server/notificationimp/password_reset.go | 7 +- api/payments/orchestrator/go.mod | 10 +- api/payments/orchestrator/go.sum | 20 +- api/pkg/api/http/response/response.go | 4 + api/pkg/auth/archivableimp.go | 14 +- api/pkg/auth/dbimp.go | 60 +-- api/pkg/auth/dbimpab.go | 90 ++-- api/pkg/auth/helper.go | 12 +- api/pkg/auth/indexableimp.go | 34 +- api/pkg/auth/internal/casbin/enforcer.go | 4 +- api/pkg/auth/internal/casbin/role.go | 4 +- api/pkg/auth/internal/native/enforcer.go | 22 +- api/pkg/auth/internal/native/role.go | 4 +- api/pkg/db/confirmation/confirmation.go | 16 - api/pkg/db/factory.go | 2 - .../db/internal/mongo/confirmationdb/db.go | 67 --- .../internal/mongo/confirmationdb/delete.go | 17 - .../db/internal/mongo/confirmationdb/find.go | 26 - api/pkg/db/internal/mongo/db.go | 6 - .../db/internal/mongo/paymethoddb/archived.go | 2 +- .../db/internal/mongo/recipientdb/archived.go | 4 +- .../db/internal/mongo/refreshtokensdb/crud.go | 2 +- .../internal/mongo/verificationimp/consume.go | 139 +++-- .../internal/mongo/verificationimp/create.go | 243 +++++++-- .../db/internal/mongo/verificationimp/db.go | 15 + .../db/internal/mongo/verificationimp/hash.go | 95 ++++ .../verificationimp/verification_test.go | 485 +++++++++++++++--- api/pkg/db/verification/errors.go | 21 +- api/pkg/db/verification/request.go | 70 +++ api/pkg/db/verification/verification.go | 11 +- api/pkg/go.mod | 8 +- api/pkg/go.sum | 20 +- .../confirmation/notification.go | 4 +- .../notifications/confirmation/processor.go | 8 +- .../confirmations/notification.go | 2 +- .../confirmation/confirmation.go | 2 +- .../confirmation/handler/interface.go | 2 +- api/pkg/model/confirmation_code.go | 36 -- api/pkg/model/notificationevent.go | 2 +- api/pkg/model/verificaton.go | 35 +- api/pkg/mservice/services.go | 4 +- api/pkg/mutil/db/auth/accountbound.go | 6 +- api/pkg/mutil/db/auth/protected.go | 6 +- .../mutil/helpers/internal/accountmanager.go | 12 +- api/pkg/mutil/mask/mask.go | 17 + api/pkg/mutil/mzap/account.go | 20 + api/pkg/mutil/mzap/object.go | 4 - api/server/go.mod | 10 +- api/server/go.sum | 24 +- .../accountservice/internal/service.go | 40 +- .../interface/api/sresponse/login_pending.go | 4 +- .../verification.go} | 6 +- api/server/internal/api/api.go | 4 +- api/server/internal/api/middleware.go | 2 +- .../api/routers/authorized/handler.go | 6 +- api/server/internal/api/routers/dispatcher.go | 6 +- .../internal/api/routers/public/login.go | 14 +- .../internal/api/routers/public/router.go | 38 +- .../internal/api/routers/public/validate.go | 2 +- .../mutil/verification/verificatoin.go | 45 ++ .../internal/server/accountapiimp/email.go | 3 +- .../internal/server/accountapiimp/password.go | 2 +- .../server/confirmationimp/request.go | 48 -- .../internal/server/confirmationimp/resend.go | 56 -- .../server/confirmationimp/service.go | 158 ------ .../internal/server/confirmationimp/store.go | 187 ------- .../internal/server/confirmationimp/types.go | 23 - .../server/permissionsimp/changerole.go | 12 +- .../server/verificationimp/request.go | 57 ++ .../server/verificationimp/sendcode.go | 19 + .../server/verificationimp/service.go | 80 +++ .../internal/server/verificationimp/store.go | 45 ++ .../internal/server/verificationimp/target.go | 15 + .../internal/server/verificationimp/token.go | 20 + .../internal/server/verificationimp/types.go | 24 + .../verify.go | 43 +- .../confirmations/login_confirmation.dart | 39 -- .../requests/tokens/session_identifier.dart | 8 +- .../lib/api/requests/verification/login.dart | 41 ++ .../lib/api/requests/verification/resend.dart | 22 + .../lib/api/responses/login_pending.dart | 2 - .../response.dart} | 12 +- .../lib/data/mapper/session_identifier.dart | 4 +- .../lib/models/auth/pending_login.dart | 12 +- .../purpose.dart} | 8 +- frontend/pshared/lib/provider/account.dart | 18 +- frontend/pshared/lib/service/account.dart | 3 +- frontend/pshared/lib/service/services.dart | 2 +- .../pshared/lib/service/verification.dart | 35 +- 120 files changed, 1863 insertions(+), 1394 deletions(-) delete mode 100644 api/pkg/db/confirmation/confirmation.go delete mode 100644 api/pkg/db/internal/mongo/confirmationdb/db.go delete mode 100644 api/pkg/db/internal/mongo/confirmationdb/delete.go delete mode 100644 api/pkg/db/internal/mongo/confirmationdb/find.go create mode 100644 api/pkg/db/verification/request.go delete mode 100644 api/pkg/model/confirmation_code.go create mode 100644 api/pkg/mutil/mask/mask.go create mode 100644 api/pkg/mutil/mzap/account.go rename api/server/interface/services/{confirmation/confirmation.go => verification/verification.go} (56%) create mode 100644 api/server/internal/mutil/verification/verificatoin.go delete mode 100644 api/server/internal/server/confirmationimp/request.go delete mode 100644 api/server/internal/server/confirmationimp/resend.go delete mode 100644 api/server/internal/server/confirmationimp/service.go delete mode 100644 api/server/internal/server/confirmationimp/store.go delete mode 100644 api/server/internal/server/confirmationimp/types.go create mode 100644 api/server/internal/server/verificationimp/request.go create mode 100644 api/server/internal/server/verificationimp/sendcode.go create mode 100644 api/server/internal/server/verificationimp/service.go create mode 100644 api/server/internal/server/verificationimp/store.go create mode 100644 api/server/internal/server/verificationimp/target.go create mode 100644 api/server/internal/server/verificationimp/token.go create mode 100644 api/server/internal/server/verificationimp/types.go rename api/server/internal/server/{confirmationimp => verificationimp}/verify.go (57%) delete mode 100644 frontend/pshared/lib/api/requests/confirmations/login_confirmation.dart create mode 100644 frontend/pshared/lib/api/requests/verification/login.dart create mode 100644 frontend/pshared/lib/api/requests/verification/resend.dart rename frontend/pshared/lib/api/responses/{confirmation.dart => verification/response.dart} (62%) rename frontend/pshared/lib/models/{confirmation_target.dart => verification/purpose.dart} (51%) diff --git a/api/billing/documents/go.mod b/api/billing/documents/go.mod index f4d2d4d0..347d3098 100644 --- a/api/billing/documents/go.mod +++ b/api/billing/documents/go.mod @@ -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 ) diff --git a/api/billing/documents/go.sum b/api/billing/documents/go.sum index c82ace98..3bd6cadc 100644 --- a/api/billing/documents/go.sum +++ b/api/billing/documents/go.sum @@ -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= diff --git a/api/billing/fees/go.mod b/api/billing/fees/go.mod index 0c1e4d75..6096ab2e 100644 --- a/api/billing/fees/go.mod +++ b/api/billing/fees/go.mod @@ -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 ) diff --git a/api/billing/fees/go.sum b/api/billing/fees/go.sum index 58daa9d4..e9ad2722 100644 --- a/api/billing/fees/go.sum +++ b/api/billing/fees/go.sum @@ -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= diff --git a/api/discovery/go.mod b/api/discovery/go.mod index 57d0a032..bcda6b99 100644 --- a/api/discovery/go.mod +++ b/api/discovery/go.mod @@ -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 ) diff --git a/api/discovery/go.sum b/api/discovery/go.sum index 58daa9d4..e9ad2722 100644 --- a/api/discovery/go.sum +++ b/api/discovery/go.sum @@ -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= diff --git a/api/fx/ingestor/go.mod b/api/fx/ingestor/go.mod index 1d8b2b7c..e6db5199 100644 --- a/api/fx/ingestor/go.mod +++ b/api/fx/ingestor/go.mod @@ -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 ) diff --git a/api/fx/ingestor/go.sum b/api/fx/ingestor/go.sum index 58daa9d4..e9ad2722 100644 --- a/api/fx/ingestor/go.sum +++ b/api/fx/ingestor/go.sum @@ -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= diff --git a/api/fx/oracle/go.mod b/api/fx/oracle/go.mod index 5ed22a67..d8ab34bc 100644 --- a/api/fx/oracle/go.mod +++ b/api/fx/oracle/go.mod @@ -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 ) diff --git a/api/fx/oracle/go.sum b/api/fx/oracle/go.sum index 58daa9d4..e9ad2722 100644 --- a/api/fx/oracle/go.sum +++ b/api/fx/oracle/go.sum @@ -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= diff --git a/api/fx/storage/go.mod b/api/fx/storage/go.mod index f23c2ea9..c4365272 100644 --- a/api/fx/storage/go.mod +++ b/api/fx/storage/go.mod @@ -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 ) diff --git a/api/fx/storage/go.sum b/api/fx/storage/go.sum index 0fe20d5e..b007cadd 100644 --- a/api/fx/storage/go.sum +++ b/api/fx/storage/go.sum @@ -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= diff --git a/api/gateway/chain/go.mod b/api/gateway/chain/go.mod index 4891049e..0de5737e 100644 --- a/api/gateway/chain/go.mod +++ b/api/gateway/chain/go.mod @@ -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 ) diff --git a/api/gateway/chain/go.sum b/api/gateway/chain/go.sum index 4e5ec1e5..a976f0d5 100644 --- a/api/gateway/chain/go.sum +++ b/api/gateway/chain/go.sum @@ -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= diff --git a/api/gateway/mntx/go.mod b/api/gateway/mntx/go.mod index 5f88b860..e09985e9 100644 --- a/api/gateway/mntx/go.mod +++ b/api/gateway/mntx/go.mod @@ -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 ) diff --git a/api/gateway/mntx/go.sum b/api/gateway/mntx/go.sum index aea5d3cd..0d812dcc 100644 --- a/api/gateway/mntx/go.sum +++ b/api/gateway/mntx/go.sum @@ -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= diff --git a/api/gateway/tgsettle/go.mod b/api/gateway/tgsettle/go.mod index c47607d5..0f738c0d 100644 --- a/api/gateway/tgsettle/go.mod +++ b/api/gateway/tgsettle/go.mod @@ -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 ) diff --git a/api/gateway/tgsettle/go.sum b/api/gateway/tgsettle/go.sum index 58daa9d4..e9ad2722 100644 --- a/api/gateway/tgsettle/go.sum +++ b/api/gateway/tgsettle/go.sum @@ -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= diff --git a/api/gateway/tron/go.mod b/api/gateway/tron/go.mod index a59bd4bc..fe4ea9a2 100644 --- a/api/gateway/tron/go.mod +++ b/api/gateway/tron/go.mod @@ -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 ) diff --git a/api/gateway/tron/go.sum b/api/gateway/tron/go.sum index 47e7d856..40a6d5f7 100644 --- a/api/gateway/tron/go.sum +++ b/api/gateway/tron/go.sum @@ -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= diff --git a/api/ledger/go.mod b/api/ledger/go.mod index bdc01bed..41a81fc6 100644 --- a/api/ledger/go.mod +++ b/api/ledger/go.mod @@ -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 ) diff --git a/api/ledger/go.sum b/api/ledger/go.sum index b9863090..88b3b986 100644 --- a/api/ledger/go.sum +++ b/api/ledger/go.sum @@ -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= diff --git a/api/ledger/internal/service/ledger/account_status.go b/api/ledger/internal/service/ledger/account_status.go index bc65ed58..c07e6bfa 100644 --- a/api/ledger/internal/service/ledger/account_status.go +++ b/api/ledger/internal/service/ledger/account_status.go @@ -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 } } diff --git a/api/ledger/internal/service/ledger/posting_support.go b/api/ledger/internal/service/ledger/posting_support.go index b799e12b..d3cbd4b5 100644 --- a/api/ledger/internal/service/ledger/posting_support.go +++ b/api/ledger/internal/service/ledger/posting_support.go @@ -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") } } diff --git a/api/ledger/storage/mongo/store/accounts.go b/api/ledger/storage/mongo/store/accounts.go index ef09a545..d9484450 100644 --- a/api/ledger/storage/mongo/store/accounts.go +++ b/api/ledger/storage/mongo/store/accounts.go @@ -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 } diff --git a/api/ledger/storage/mongo/store/balances.go b/api/ledger/storage/mongo/store/balances.go index c47eb489..6fa7b8a5 100644 --- a/api/ledger/storage/mongo/store/balances.go +++ b/api/ledger/storage/mongo/store/balances.go @@ -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 } diff --git a/api/ledger/storage/mongo/store/posting_lines.go b/api/ledger/storage/mongo/store/posting_lines.go index 54d31263..03e215f9 100644 --- a/api/ledger/storage/mongo/store/posting_lines.go +++ b/api/ledger/storage/mongo/store/posting_lines.go @@ -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 } diff --git a/api/notification/go.mod b/api/notification/go.mod index c3fe6460..ea61d3ca 100644 --- a/api/notification/go.mod +++ b/api/notification/go.mod @@ -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 ) diff --git a/api/notification/go.sum b/api/notification/go.sum index b861aaea..fb432038 100644 --- a/api/notification/go.sum +++ b/api/notification/go.sum @@ -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= diff --git a/api/notification/internal/server/notificationimp/accountcreated.go b/api/notification/internal/server/notificationimp/accountcreated.go index 8a36bf07..5bb0bde8 100644 --- a/api/notification/internal/server/notificationimp/accountcreated.go +++ b/api/notification/internal/server/notificationimp/accountcreated.go @@ -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 } diff --git a/api/notification/internal/server/notificationimp/confcode.go b/api/notification/internal/server/notificationimp/confcode.go index bccf5ee6..e88913c0 100644 --- a/api/notification/internal/server/notificationimp/confcode.go +++ b/api/notification/internal/server/notificationimp/confcode.go @@ -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 } diff --git a/api/notification/internal/server/notificationimp/password_reset.go b/api/notification/internal/server/notificationimp/password_reset.go index 4683693b..506ac74c 100644 --- a/api/notification/internal/server/notificationimp/password_reset.go +++ b/api/notification/internal/server/notificationimp/password_reset.go @@ -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 } diff --git a/api/payments/orchestrator/go.mod b/api/payments/orchestrator/go.mod index b3fe0619..9ea1ab9f 100644 --- a/api/payments/orchestrator/go.mod +++ b/api/payments/orchestrator/go.mod @@ -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 ) diff --git a/api/payments/orchestrator/go.sum b/api/payments/orchestrator/go.sum index a283d207..60a7d790 100644 --- a/api/payments/orchestrator/go.sum +++ b/api/payments/orchestrator/go.sum @@ -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= diff --git a/api/pkg/api/http/response/response.go b/api/pkg/api/http/response/response.go index e76fba54..5253d4fb 100644 --- a/api/pkg/api/http/response/response.go +++ b/api/pkg/api/http/response/response.go @@ -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) diff --git a/api/pkg/auth/archivableimp.go b/api/pkg/auth/archivableimp.go index c2a2f3e2..256da6fe 100644 --- a/api/pkg/auth/archivableimp.go +++ b/api/pkg/auth/archivableimp.go @@ -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) diff --git a/api/pkg/auth/dbimp.go b/api/pkg/auth/dbimp.go index a9e4b132..de6e3b20 100644 --- a/api/pkg/auth/dbimp.go +++ b/api/pkg/auth/dbimp.go @@ -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 } diff --git a/api/pkg/auth/dbimpab.go b/api/pkg/auth/dbimpab.go index 418b61a2..54139373 100644 --- a/api/pkg/auth/dbimpab.go +++ b/api/pkg/auth/dbimpab.go @@ -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 } diff --git a/api/pkg/auth/helper.go b/api/pkg/auth/helper.go index 78f27ef1..1d133d57 100644 --- a/api/pkg/auth/helper.go +++ b/api/pkg/auth/helper.go @@ -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 diff --git a/api/pkg/auth/indexableimp.go b/api/pkg/auth/indexableimp.go index d7169cc8..eb47986d 100644 --- a/api/pkg/auth/indexableimp.go +++ b/api/pkg/auth/indexableimp.go @@ -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 } diff --git a/api/pkg/auth/internal/casbin/enforcer.go b/api/pkg/auth/internal/casbin/enforcer.go index e68e6e95..80efaaa3 100644 --- a/api/pkg/auth/internal/casbin/enforcer.go +++ b/api/pkg/auth/internal/casbin/enforcer.go @@ -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) diff --git a/api/pkg/auth/internal/casbin/role.go b/api/pkg/auth/internal/casbin/role.go index 21e01283..1c59faef 100644 --- a/api/pkg/auth/internal/casbin/role.go +++ b/api/pkg/auth/internal/casbin/role.go @@ -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 } diff --git a/api/pkg/auth/internal/native/enforcer.go b/api/pkg/auth/internal/native/enforcer.go index bf6b8153..d5a4f057 100644 --- a/api/pkg/auth/internal/native/enforcer.go +++ b/api/pkg/auth/internal/native/enforcer.go @@ -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 { diff --git a/api/pkg/auth/internal/native/role.go b/api/pkg/auth/internal/native/role.go index 7926dbc7..309e58ed 100644 --- a/api/pkg/auth/internal/native/role.go +++ b/api/pkg/auth/internal/native/role.go @@ -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 } diff --git a/api/pkg/db/confirmation/confirmation.go b/api/pkg/db/confirmation/confirmation.go deleted file mode 100644 index 61bc79fc..00000000 --- a/api/pkg/db/confirmation/confirmation.go +++ /dev/null @@ -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 -} diff --git a/api/pkg/db/factory.go b/api/pkg/db/factory.go index 27c89436..30fbd538 100644 --- a/api/pkg/db/factory.go +++ b/api/pkg/db/factory.go @@ -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) diff --git a/api/pkg/db/internal/mongo/confirmationdb/db.go b/api/pkg/db/internal/mongo/confirmationdb/db.go deleted file mode 100644 index 5e97b046..00000000 --- a/api/pkg/db/internal/mongo/confirmationdb/db.go +++ /dev/null @@ -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 -} diff --git a/api/pkg/db/internal/mongo/confirmationdb/delete.go b/api/pkg/db/internal/mongo/confirmationdb/delete.go deleted file mode 100644 index fa04153f..00000000 --- a/api/pkg/db/internal/mongo/confirmationdb/delete.go +++ /dev/null @@ -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) -} diff --git a/api/pkg/db/internal/mongo/confirmationdb/find.go b/api/pkg/db/internal/mongo/confirmationdb/find.go deleted file mode 100644 index 7737c0c6..00000000 --- a/api/pkg/db/internal/mongo/confirmationdb/find.go +++ /dev/null @@ -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 -} diff --git a/api/pkg/db/internal/mongo/db.go b/api/pkg/db/internal/mongo/db.go index dc65ee8f..3a9d1785 100755 --- a/api/pkg/db/internal/mongo/db.go +++ b/api/pkg/db/internal/mongo/db.go @@ -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 { diff --git a/api/pkg/db/internal/mongo/paymethoddb/archived.go b/api/pkg/db/internal/mongo/paymethoddb/archived.go index 9bcef783..d466dba6 100644 --- a/api/pkg/db/internal/mongo/paymethoddb/archived.go +++ b/api/pkg/db/internal/mongo/paymethoddb/archived.go @@ -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 } diff --git a/api/pkg/db/internal/mongo/recipientdb/archived.go b/api/pkg/db/internal/mongo/recipientdb/archived.go index 8a7c56b2..1fd0240d 100644 --- a/api/pkg/db/internal/mongo/recipientdb/archived.go +++ b/api/pkg/db/internal/mongo/recipientdb/archived.go @@ -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 diff --git a/api/pkg/db/internal/mongo/refreshtokensdb/crud.go b/api/pkg/db/internal/mongo/refreshtokensdb/crud.go index 4cfc039b..658c2bac 100644 --- a/api/pkg/db/internal/mongo/refreshtokensdb/crud.go +++ b/api/pkg/db/internal/mongo/refreshtokensdb/crud.go @@ -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 diff --git a/api/pkg/db/internal/mongo/verificationimp/consume.go b/api/pkg/db/internal/mongo/verificationimp/consume.go index 61357905..e37fd764 100644 --- a/api/pkg/db/internal/mongo/verificationimp/consume.go +++ b/api/pkg/db/internal/mongo/verificationimp/consume.go @@ -6,68 +6,146 @@ 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" + mutil "github.com/tech/sendico/pkg/mutil/db" + "github.com/tech/sendico/pkg/mutil/mzap" + "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" ) 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), - ) - 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 { + + // 1) Load active tokens for this context + activeFilter := repository.Query().And( + repository.Filter("accountRef", accountRef), + repository.Filter("purpose", purpose), + repository.Filter("usedAt", nil), + repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now), + ) + + tokens, err := mutil.GetObjects[model.VerificationToken]( + ctx, db.Logger, activeFilter, nil, db.DBImp.Repository, + ) + if err != nil { if errors.Is(err, merrors.ErrNoData) { - db.Logger.Debug("Token hash not found", zap.Error(err), zap.String("hash", hash)) + db.Logger.Debug("No tokens found", zap.Error(err), mzap.AccRef(accountRef), zap.String("purpose", string(purpose))) return nil, verification.ErorrTokenNotFound() } - db.Logger.Warn("Failed to check token", zap.Error(err), zap.String("hash", hash)) + db.Logger.Warn("Failed to load active tokens", zap.Error(err), mzap.AccRef(accountRef), zap.String("purpose", string(purpose))) 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), - ) + if len(tokens) == 0 { + db.Logger.Debug("No tokens found", zap.Error(err), mzap.AccRef(accountRef), zap.String("purpose", string(purpose))) + return nil, verification.ErorrTokenNotFound() + } + + // 2) Find matching token via hasher (OTP or Magic — doesn't matter) + var token *model.VerificationToken + + 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 _, t := range tokens { + _, _ = db.DBImp.PatchMany( + ctx, + repository.IDFilter(t.ID), + repository.Patch().Inc(repository.Field("attempts"), 1), + ) + } + return nil, verification.ErorrTokenNotFound() + } + + // 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("accountRef", accountRef), + repository.Filter("purpose", purpose), + repository.Filter("usedAt", nil), + repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now), + ) + + 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 +154,5 @@ func (db *verificationDB) Consume( if !ok { return nil, merrors.Internal("unexpected token type") } - return res, nil } diff --git a/api/pkg/db/internal/mongo/verificationimp/create.go b/api/pkg/db/internal/mongo/verificationimp/create.go index 64666af8..c4327cbc 100644 --- a/api/pkg/db/internal/mongo/verificationimp/create.go +++ b/api/pkg/db/internal/mongo/verificationimp/create.go @@ -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 } diff --git a/api/pkg/db/internal/mongo/verificationimp/db.go b/api/pkg/db/internal/mongo/verificationimp/db.go index c44e3bcf..e5d703a6 100644 --- a/api/pkg/db/internal/mongo/verificationimp/db.go +++ b/api/pkg/db/internal/mongo/verificationimp/db.go @@ -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}}, diff --git a/api/pkg/db/internal/mongo/verificationimp/hash.go b/api/pkg/db/internal/mongo/verificationimp/hash.go index 2e755c44..c4c1f35e 100644 --- a/api/pkg/db/internal/mongo/verificationimp/hash.go +++ b/api/pkg/db/internal/mongo/verificationimp/hash.go @@ -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[:]) diff --git a/api/pkg/db/internal/mongo/verificationimp/verification_test.go b/api/pkg/db/internal/mongo/verificationimp/verification_test.go index b519c7ca..71ab2283 100644 --- a/api/pkg/db/internal/mongo/verificationimp/verification_test.go +++ b/api/pkg/db/internal/mongo/verificationimp/verification_test.go @@ -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") + assert.True(t, errors.Is(err, verification.ErrTokenNotFound), + "second consume should fail — used tokens are excluded from active filter") } func TestConsume_ExpiredTokenFails(t *testing.T) { @@ -453,20 +563,20 @@ 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") + assert.True(t, errors.Is(err, verification.ErrTokenNotFound), + "expired token is excluded from active filter") } 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)) } @@ -476,21 +586,21 @@ func TestCreate_InvalidatesPreviousToken(t *testing.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") + assert.True(t, errors.Is(err, verification.ErrTokenNotFound), + "old token should be invalidated after new token creation") // 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 +610,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.ErrTokenNotFound), "first should be invalidated") + _, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, second) + assert.True(t, errors.Is(err, verification.ErrTokenNotFound), "second should be invalidated") - 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 +632,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 +649,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 +667,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 +684,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 +706,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 +729,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)) +} diff --git a/api/pkg/db/verification/errors.go b/api/pkg/db/verification/errors.go index 0d0b50d1..495941df 100644 --- a/api/pkg/db/verification/errors.go +++ b/api/pkg/db/verification/errors.go @@ -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") +} diff --git a/api/pkg/db/verification/request.go b/api/pkg/db/verification/request.go new file mode 100644 index 00000000..98c25450 --- /dev/null +++ b/api/pkg/db/verification/request.go @@ -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 +} diff --git a/api/pkg/db/verification/verification.go b/api/pkg/db/verification/verification.go index 39606720..da9c5c42 100644 --- a/api/pkg/db/verification/verification.go +++ b/api/pkg/db/verification/verification.go @@ -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) } diff --git a/api/pkg/go.mod b/api/pkg/go.mod index 29e80fd1..4b252cd4 100644 --- a/api/pkg/go.mod +++ b/api/pkg/go.mod @@ -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 ) diff --git a/api/pkg/go.sum b/api/pkg/go.sum index 92f86cc9..091b390c 100644 --- a/api/pkg/go.sum +++ b/api/pkg/go.sum @@ -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= diff --git a/api/pkg/messaging/internal/notifications/confirmation/notification.go b/api/pkg/messaging/internal/notifications/confirmation/notification.go index ccd25e4d..bd6b5abc 100644 --- a/api/pkg/messaging/internal/notifications/confirmation/notification.go +++ b/api/pkg/messaging/internal/notifications/confirmation/notification.go @@ -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{ diff --git a/api/pkg/messaging/internal/notifications/confirmation/processor.go b/api/pkg/messaging/internal/notifications/confirmation/processor.go index e93d999e..0c89ac85 100644 --- a/api/pkg/messaging/internal/notifications/confirmation/processor.go +++ b/api/pkg/messaging/internal/notifications/confirmation/processor.go @@ -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") } diff --git a/api/pkg/messaging/internal/notifications/confirmations/notification.go b/api/pkg/messaging/internal/notifications/confirmations/notification.go index 1b381e81..24b45a5f 100644 --- a/api/pkg/messaging/internal/notifications/confirmations/notification.go +++ b/api/pkg/messaging/internal/notifications/confirmations/notification.go @@ -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 { diff --git a/api/pkg/messaging/notifications/confirmation/confirmation.go b/api/pkg/messaging/notifications/confirmation/confirmation.go index c43d6f70..425e033c 100644 --- a/api/pkg/messaging/notifications/confirmation/confirmation.go +++ b/api/pkg/messaging/notifications/confirmation/confirmation.go @@ -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) } diff --git a/api/pkg/messaging/notifications/confirmation/handler/interface.go b/api/pkg/messaging/notifications/confirmation/handler/interface.go index a7c24771..f03dd7c1 100644 --- a/api/pkg/messaging/notifications/confirmation/handler/interface.go +++ b/api/pkg/messaging/notifications/confirmation/handler/interface.go @@ -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 diff --git a/api/pkg/model/confirmation_code.go b/api/pkg/model/confirmation_code.go deleted file mode 100644 index ce794b26..00000000 --- a/api/pkg/model/confirmation_code.go +++ /dev/null @@ -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 -} diff --git a/api/pkg/model/notificationevent.go b/api/pkg/model/notificationevent.go index 612a0ede..2c81f4ea 100644 --- a/api/pkg/model/notificationevent.go +++ b/api/pkg/model/notificationevent.go @@ -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 diff --git a/api/pkg/model/verificaton.go b/api/pkg/model/verificaton.go index c38440a3..0ac82ffa 100644 --- a/api/pkg/model/verificaton.go +++ b/api/pkg/model/verificaton.go @@ -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:"-"` } diff --git a/api/pkg/mservice/services.go b/api/pkg/mservice/services.go index 53309ea9..0605cf09 100644 --- a/api/pkg/mservice/services.go +++ b/api/pkg/mservice/services.go @@ -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, diff --git a/api/pkg/mutil/db/auth/accountbound.go b/api/pkg/mutil/db/auth/accountbound.go index 25ada631..e4b8adca 100644 --- a/api/pkg/mutil/db/auth/accountbound.go +++ b/api/pkg/mutil/db/auth/accountbound.go @@ -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), ) diff --git a/api/pkg/mutil/db/auth/protected.go b/api/pkg/mutil/db/auth/protected.go index 248b1dd7..81746b93 100644 --- a/api/pkg/mutil/db/auth/protected.go +++ b/api/pkg/mutil/db/auth/protected.go @@ -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 } diff --git a/api/pkg/mutil/helpers/internal/accountmanager.go b/api/pkg/mutil/helpers/internal/accountmanager.go index e09e5c55..2e924771 100644 --- a/api/pkg/mutil/helpers/internal/accountmanager.go +++ b/api/pkg/mutil/helpers/internal/accountmanager.go @@ -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 } diff --git a/api/pkg/mutil/mask/mask.go b/api/pkg/mutil/mask/mask.go new file mode 100644 index 00000000..9ee90c84 --- /dev/null +++ b/api/pkg/mutil/mask/mask.go @@ -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] +} diff --git a/api/pkg/mutil/mzap/account.go b/api/pkg/mutil/mzap/account.go new file mode 100644 index 00000000..7a10d280 --- /dev/null +++ b/api/pkg/mutil/mzap/account.go @@ -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) +} diff --git a/api/pkg/mutil/mzap/object.go b/api/pkg/mutil/mzap/object.go index e36a195a..fac4b695 100644 --- a/api/pkg/mutil/mzap/object.go +++ b/api/pkg/mutil/mzap/object.go @@ -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) -} diff --git a/api/server/go.mod b/api/server/go.mod index 080cdacb..cccd8839 100644 --- a/api/server/go.mod +++ b/api/server/go.mod @@ -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 ) diff --git a/api/server/go.sum b/api/server/go.sum index d64b3d30..bbed8aa0 100644 --- a/api/server/go.sum +++ b/api/server/go.sum @@ -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= diff --git a/api/server/interface/accountservice/internal/service.go b/api/server/interface/accountservice/internal/service.go index 5364e57f..e00cfcc0 100644 --- a/api/server/interface/accountservice/internal/service.go +++ b/api/server/interface/accountservice/internal/service.go @@ -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 } diff --git a/api/server/interface/api/sresponse/login_pending.go b/api/server/interface/api/sresponse/login_pending.go index 5ef2d4a8..d77cebc1 100644 --- a/api/server/interface/api/sresponse/login_pending.go +++ b/api/server/interface/api/sresponse/login_pending.go @@ -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, }, ) } diff --git a/api/server/interface/services/confirmation/confirmation.go b/api/server/interface/services/verification/verification.go similarity index 56% rename from api/server/interface/services/confirmation/confirmation.go rename to api/server/interface/services/verification/verification.go index 39cc9752..9f9d400e 100644 --- a/api/server/interface/services/confirmation/confirmation.go +++ b/api/server/interface/services/verification/verification.go @@ -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) } diff --git a/api/server/internal/api/api.go b/api/server/internal/api/api.go index c8ee5017..6721856d 100644 --- a/api/server/internal/api/api.go +++ b/api/server/internal/api/api.go @@ -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) diff --git a/api/server/internal/api/middleware.go b/api/server/internal/api/middleware.go index c0245828..f42ad68c 100644 --- a/api/server/internal/api/middleware.go +++ b/api/server/internal/api/middleware.go @@ -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 diff --git a/api/server/internal/api/routers/authorized/handler.go b/api/server/internal/api/routers/authorized/handler.go index 29118ce0..755859d7 100644 --- a/api/server/internal/api/routers/authorized/handler.go +++ b/api/server/internal/api/routers/authorized/handler.go @@ -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) diff --git a/api/server/internal/api/routers/dispatcher.go b/api/server/internal/api/routers/dispatcher.go index 3ec29298..ca7c6dfc 100644 --- a/api/server/internal/api/routers/dispatcher.go +++ b/api/server/internal/api/routers/dispatcher.go @@ -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) diff --git a/api/server/internal/api/routers/public/login.go b/api/server/internal/api/routers/public/login.go index e6da0423..3e94ef37 100644 --- a/api/server/internal/api/routers/public/login.go +++ b/api/server/internal/api/routers/public/login.go @@ -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 { diff --git a/api/server/internal/api/routers/public/router.go b/api/server/internal/api/routers/public/router.go index 2d5ab0fe..ddef7122 100644 --- a/api/server/internal/api/routers/public/router.go +++ b/api/server/internal/api/routers/public/router.go @@ -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, diff --git a/api/server/internal/api/routers/public/validate.go b/api/server/internal/api/routers/public/validate.go index 222e3e6d..41f7d431 100644 --- a/api/server/internal/api/routers/public/validate.go +++ b/api/server/internal/api/routers/public/validate.go @@ -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") } diff --git a/api/server/internal/mutil/verification/verificatoin.go b/api/server/internal/mutil/verification/verificatoin.go new file mode 100644 index 00000000..511efe60 --- /dev/null +++ b/api/server/internal/mutil/verification/verificatoin.go @@ -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) +} diff --git a/api/server/internal/server/accountapiimp/email.go b/api/server/internal/server/accountapiimp/email.go index e42be28f..2eb128cf 100644 --- a/api/server/internal/server/accountapiimp/email.go +++ b/api/server/internal/server/accountapiimp/email.go @@ -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) diff --git a/api/server/internal/server/accountapiimp/password.go b/api/server/internal/server/accountapiimp/password.go index f635ce25..eb96794c 100644 --- a/api/server/internal/server/accountapiimp/password.go +++ b/api/server/internal/server/accountapiimp/password.go @@ -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) diff --git a/api/server/internal/server/confirmationimp/request.go b/api/server/internal/server/confirmationimp/request.go deleted file mode 100644 index 68e61f39..00000000 --- a/api/server/internal/server/confirmationimp/request.go +++ /dev/null @@ -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), - }) -} diff --git a/api/server/internal/server/confirmationimp/resend.go b/api/server/internal/server/confirmationimp/resend.go deleted file mode 100644 index 4e7e1d2e..00000000 --- a/api/server/internal/server/confirmationimp/resend.go +++ /dev/null @@ -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), - }) -} diff --git a/api/server/internal/server/confirmationimp/service.go b/api/server/internal/server/confirmationimp/service.go deleted file mode 100644 index 52e97d7b..00000000 --- a/api/server/internal/server/confirmationimp/service.go +++ /dev/null @@ -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 -} diff --git a/api/server/internal/server/confirmationimp/store.go b/api/server/internal/server/confirmationimp/store.go deleted file mode 100644 index 9e513445..00000000 --- a/api/server/internal/server/confirmationimp/store.go +++ /dev/null @@ -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 -} diff --git a/api/server/internal/server/confirmationimp/types.go b/api/server/internal/server/confirmationimp/types.go deleted file mode 100644 index 743c0b61..00000000 --- a/api/server/internal/server/confirmationimp/types.go +++ /dev/null @@ -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"` -} diff --git a/api/server/internal/server/permissionsimp/changerole.go b/api/server/internal/server/permissionsimp/changerole.go index 9eac4d6c..08c92d98 100644 --- a/api/server/internal/server/permissionsimp/changerole.go +++ b/api/server/internal/server/permissionsimp/changerole.go @@ -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) } diff --git a/api/server/internal/server/verificationimp/request.go b/api/server/internal/server/verificationimp/request.go new file mode 100644 index 00000000..f47b0c4b --- /dev/null +++ b/api/server/internal/server/verificationimp/request.go @@ -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, + }) +} diff --git a/api/server/internal/server/verificationimp/sendcode.go b/api/server/internal/server/verificationimp/sendcode.go new file mode 100644 index 00000000..77069510 --- /dev/null +++ b/api/server/internal/server/verificationimp/sendcode.go @@ -0,0 +1,19 @@ +package verificationimp + +import ( + cnotifications "github.com/tech/sendico/pkg/messaging/notifications/confirmation" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mask" + "github.com/tech/sendico/pkg/mutil/mzap" + "go.uber.org/zap" +) + +func (a *VerificationAPI) sendCode(account *model.Account, target model.VerificationPurpose, destination, code string) { + a.logger.Info("Confirmation code generated", + zap.String("target", string(target)), + zap.String("destination", mask.Email(destination)), + mzap.AccRef(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.AccRef(account.ID)) + } +} diff --git a/api/server/internal/server/verificationimp/service.go b/api/server/internal/server/verificationimp/service.go new file mode 100644 index 00000000..6c4ee87f --- /dev/null +++ b/api/server/internal/server/verificationimp/service.go @@ -0,0 +1,80 @@ +package verificationimp + +import ( + "context" + "time" + + api "github.com/tech/sendico/pkg/api/http" + "github.com/tech/sendico/pkg/db/refreshtokens" + "github.com/tech/sendico/pkg/messaging" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/mservice" + eapi "github.com/tech/sendico/server/interface/api" + "github.com/tech/sendico/server/interface/middleware" +) + +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 VerificationAPI struct { + logger mlogger.Logger + config Config + store *ConfirmationStore + rtdb refreshtokens.DB + producer messaging.Producer + tokenConfig middleware.TokenConfig + signature middleware.Signature +} + +func (a *VerificationAPI) Name() mservice.Type { + return mservice.Verification +} + +func (a *VerificationAPI) Finish(_ context.Context) error { + return nil +} + +func CreateAPI(a eapi.API) (*VerificationAPI, error) { + cdb, err := a.DBFactory().NewVerificationsDB() + if err != nil { + return nil, err + } + rtdb, err := a.DBFactory().NewRefreshTokensDB() + if err != nil { + return nil, err + } + + p := &VerificationAPI{ + logger: a.Logger().Named(mservice.Verification), + 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.requestCode) + a.Register().PendingAccountHandler(p.Name(), "/verify", api.Post, p.verifyCode) + return p, nil +} diff --git a/api/server/internal/server/verificationimp/store.go b/api/server/internal/server/verificationimp/store.go new file mode 100644 index 00000000..211480e1 --- /dev/null +++ b/api/server/internal/server/verificationimp/store.go @@ -0,0 +1,45 @@ +package verificationimp + +import ( + "context" + + "github.com/tech/sendico/pkg/db/verification" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type ConfirmationStore struct { + db verification.DB +} + +func NewStore(db verification.DB) *ConfirmationStore { + return &ConfirmationStore{db: db} +} + +func (s *ConfirmationStore) Create( + ctx context.Context, + request *verification.Request, +) (verificationCode string, err error) { + code, err := s.db.Create(ctx, request) + if err != nil { + return "", err + } + return code, nil +} + +func (s *ConfirmationStore) Verify( + ctx context.Context, + accountRef bson.ObjectID, + purpose model.VerificationPurpose, + code string, +) (target string, err error) { + t, err := s.db.Consume(ctx, accountRef, purpose, code) + if err != nil { + return "", err + } + if t.Purpose != purpose { + return "", merrors.DataConflict("token has different verificaton purpose") + } + return t.Target, nil +} diff --git a/api/server/internal/server/verificationimp/target.go b/api/server/internal/server/verificationimp/target.go new file mode 100644 index 00000000..ea886cdc --- /dev/null +++ b/api/server/internal/server/verificationimp/target.go @@ -0,0 +1,15 @@ +package verificationimp + +import ( + "strings" + + "github.com/tech/sendico/pkg/model" +) + +func (a *VerificationAPI) resolveTarget(reqDest string, account *model.Account) string { + target := strings.ToLower(strings.TrimSpace(reqDest)) + if target == "" && account != nil { + target = strings.ToLower(strings.TrimSpace(account.Login)) + } + return target +} diff --git a/api/server/internal/server/verificationimp/token.go b/api/server/internal/server/verificationimp/token.go new file mode 100644 index 00000000..6134c9a0 --- /dev/null +++ b/api/server/internal/server/verificationimp/token.go @@ -0,0 +1,20 @@ +package verificationimp + +import ( + "time" + + "github.com/go-chi/jwtauth/v5" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/server/interface/api/sresponse" + emodel "github.com/tech/sendico/server/interface/model" +) + +func (a *VerificationAPI) createAccessToken(account *model.Account) (sresponse.TokenData, error) { + ja := jwtauth.New(a.signature.Algorithm, a.signature.PrivateKey, a.signature.PublicKey) + _, res, err := ja.Encode(emodel.Account2Claims(account, a.tokenConfig.Expiration.Account)) + token := sresponse.TokenData{ + Token: res, + Expiration: time.Now().Add(time.Duration(a.tokenConfig.Expiration.Account) * time.Hour), + } + return token, err +} diff --git a/api/server/internal/server/verificationimp/types.go b/api/server/internal/server/verificationimp/types.go new file mode 100644 index 00000000..80306a7d --- /dev/null +++ b/api/server/internal/server/verificationimp/types.go @@ -0,0 +1,24 @@ +package verificationimp + +import ( + "github.com/tech/sendico/pkg/model" +) + +type verificationCodeRequest struct { + Purpose string `json:"purpose"` + Destination string `json:"destination,omitempty"` + IdempotencyKey string `json:"idempotencyKey"` +} + +type codeVerificationRequest struct { + verificationCodeRequest `json:",inline"` + Code string `json:"code"` + SessionIdentifier model.SessionIdentifier `json:"sessionIdentifier"` +} + +type verificationResponse struct { + IdempotencyKey string `json:"idempotencyKey"` + TTLSeconds int `json:"ttl_seconds"` + CooldownSeconds int `json:"cooldown_seconds"` + Destination string `json:"destination"` +} diff --git a/api/server/internal/server/confirmationimp/verify.go b/api/server/internal/server/verificationimp/verify.go similarity index 57% rename from api/server/internal/server/confirmationimp/verify.go rename to api/server/internal/server/verificationimp/verify.go index 4ff44344..196bf060 100644 --- a/api/server/internal/server/confirmationimp/verify.go +++ b/api/server/internal/server/verificationimp/verify.go @@ -1,8 +1,7 @@ -package confirmationimp +package verificationimp import ( "encoding/json" - "errors" "net/http" "strings" @@ -12,50 +11,42 @@ import ( "github.com/tech/sendico/server/interface/api/sresponse" emodel "github.com/tech/sendico/server/interface/model" rtokens "github.com/tech/sendico/server/internal/api/routers/tokens" + mutil "github.com/tech/sendico/server/internal/mutil/verification" "go.uber.org/zap" ) -func (a *ConfirmationAPI) verifyCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc { - var req confirmationVerifyRequest +func (a *VerificationAPI) verifyCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc { + var req codeVerificationRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { a.logger.Warn("Failed to decode confirmation verification request", zap.Error(err)) return response.BadPayload(a.logger, a.Name(), err) } - target, err := a.parseTarget(req.Target) + purpose, err := model.VPFromString(req.Purpose) if err != nil { return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error()) } - if target == model.ConfirmationTargetLogin && (token == nil || !token.Pending) { - return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token") - } - if strings.TrimSpace(req.Code) == "" { return response.BadRequest(a.logger, a.Name(), "missing_code", "confirmation code is required") } - destination := a.resolveDestination(req.Destination, account) - if destination == "" { + target := a.resolveTarget(req.Destination, account) + if target == "" { return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required") } - err = a.store.Verify(r.Context(), account.ID, destination, target, strings.TrimSpace(req.Code)) - switch { - case errors.Is(err, errConfirmationNotFound): - return response.NotFound(a.logger, a.Name(), "code_not_found_or_expired") - case errors.Is(err, errConfirmationUsed): - return response.Forbidden(a.logger, a.Name(), "code_used", "code has already been used") - case errors.Is(err, errConfirmationAttemptsExceeded): - return response.Forbidden(a.logger, a.Name(), "attempt_limit_reached", "too many failed attempts") - case errors.Is(err, errConfirmationMismatch): - return response.Forbidden(a.logger, a.Name(), "invalid_code", "code does not match") - case err != nil: - a.logger.Warn("Failed to verify confirmation code", zap.Error(err), mzap.ObjRef("account_ref", account.ID)) - return response.Internal(a.logger, a.Name(), err) + dst, err := a.store.Verify(r.Context(), account.ID, purpose, req.Code) + if err != nil { + a.logger.Debug("Code verification failed", zap.Error(err)) + return mutil.MapTokenErrorToResponse(a.logger, a.Name(), err) + } + if dst != target { + a.logger.Warn("Verification code destination mismatch", zap.String("expected", target), zap.String("actual", dst), mzap.AccRef(account.ID)) + return response.DataConflict(a.logger, a.Name(), "the provided code does not match the expected destination") } - a.logger.Info("Confirmation code verified", zap.String("target", string(target)), mzap.ObjRef("account_ref", account.ID)) - if target == model.ConfirmationTargetLogin { + a.logger.Info("Confirmation code verified", zap.String("purpose", req.Purpose), mzap.AccRef(account.ID)) + if purpose == model.PurposeLogin { if req.SessionIdentifier.ClientID == "" || req.SessionIdentifier.DeviceID == "" { return response.BadRequest(a.logger, a.Name(), "missing_session", "session identifier is required") } diff --git a/frontend/pshared/lib/api/requests/confirmations/login_confirmation.dart b/frontend/pshared/lib/api/requests/confirmations/login_confirmation.dart deleted file mode 100644 index df9f2c78..00000000 --- a/frontend/pshared/lib/api/requests/confirmations/login_confirmation.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -import 'package:pshared/models/confirmation_target.dart'; -import 'package:pshared/api/requests/tokens/session_identifier.dart'; - -part 'login_confirmation.g.dart'; - - -@JsonSerializable(explicitToJson: true) -class LoginConfirmationRequest { - final ConfirmationTarget target; - final String? destination; - - const LoginConfirmationRequest({ - this.target = ConfirmationTarget.login, - this.destination, - }); - - factory LoginConfirmationRequest.fromJson(Map json) => _$LoginConfirmationRequestFromJson(json); - Map toJson() => _$LoginConfirmationRequestToJson(this); -} - -@JsonSerializable(explicitToJson: true) -class LoginConfirmationVerifyRequest { - final ConfirmationTarget target; - final String code; - final String? destination; - final SessionIdentifierDto sessionIdentifier; - - const LoginConfirmationVerifyRequest({ - this.target = ConfirmationTarget.login, - required this.code, - this.destination, - required this.sessionIdentifier, - }); - - factory LoginConfirmationVerifyRequest.fromJson(Map json) => _$LoginConfirmationVerifyRequestFromJson(json); - Map toJson() => _$LoginConfirmationVerifyRequestToJson(this); -} diff --git a/frontend/pshared/lib/api/requests/tokens/session_identifier.dart b/frontend/pshared/lib/api/requests/tokens/session_identifier.dart index f6d27aee..8594a903 100644 --- a/frontend/pshared/lib/api/requests/tokens/session_identifier.dart +++ b/frontend/pshared/lib/api/requests/tokens/session_identifier.dart @@ -4,15 +4,15 @@ part 'session_identifier.g.dart'; @JsonSerializable() -class SessionIdentifierDto { +class SessionIdentifierDTO { final String clientId; final String deviceId; - const SessionIdentifierDto({ + const SessionIdentifierDTO({ required this.clientId, required this.deviceId, }); - factory SessionIdentifierDto.fromJson(Map json) => _$SessionIdentifierDtoFromJson(json); - Map toJson() => _$SessionIdentifierDtoToJson(this); + factory SessionIdentifierDTO.fromJson(Map json) => _$SessionIdentifierDTOFromJson(json); + Map toJson() => _$SessionIdentifierDTOToJson(this); } diff --git a/frontend/pshared/lib/api/requests/verification/login.dart b/frontend/pshared/lib/api/requests/verification/login.dart new file mode 100644 index 00000000..7212ef19 --- /dev/null +++ b/frontend/pshared/lib/api/requests/verification/login.dart @@ -0,0 +1,41 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/models/verification/purpose.dart'; +import 'package:pshared/api/requests/tokens/session_identifier.dart'; + +part 'login.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class LoginVerificationRequest { + final VerificationPurpose purpose; + final String? target; + final String idempotencyKey; + + const LoginVerificationRequest({ + this.purpose = VerificationPurpose.login, + this.target, + required this.idempotencyKey, + }); + + factory LoginVerificationRequest.fromJson(Map json) => _$LoginVerificationRequestFromJson(json); + Map toJson() => _$LoginVerificationRequestToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class LoginCodeVerifyicationRequest extends LoginVerificationRequest { + final String code; + final SessionIdentifierDTO sessionIdentifier; + + const LoginCodeVerifyicationRequest({ + super.purpose = VerificationPurpose.login, + super.target, + required super.idempotencyKey, + required this.code, + required this.sessionIdentifier, + }); + + factory LoginCodeVerifyicationRequest.fromJson(Map json) => _$LoginCodeVerifyicationRequestFromJson(json); + @override + Map toJson() => _$LoginCodeVerifyicationRequestToJson(this); +} diff --git a/frontend/pshared/lib/api/requests/verification/resend.dart b/frontend/pshared/lib/api/requests/verification/resend.dart new file mode 100644 index 00000000..3ab1cd48 --- /dev/null +++ b/frontend/pshared/lib/api/requests/verification/resend.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'resend.g.dart'; + + +@JsonSerializable() +class ResendVerificationEmailRequest { + final String login; + + const ResendVerificationEmailRequest({ + required this.login, + }); + + factory ResendVerificationEmailRequest.fromJson(Map json) => _$ResendVerificationEmailRequestFromJson(json); + Map toJson() => _$ResendVerificationEmailRequestToJson(this); + + static ResendVerificationEmailRequest build({ + required String login, + }) => ResendVerificationEmailRequest(login: login); +} + + diff --git a/frontend/pshared/lib/api/responses/login_pending.dart b/frontend/pshared/lib/api/responses/login_pending.dart index 91e52418..a4660220 100644 --- a/frontend/pshared/lib/api/responses/login_pending.dart +++ b/frontend/pshared/lib/api/responses/login_pending.dart @@ -11,13 +11,11 @@ class PendingLoginResponse { final AccountResponse account; final TokenData pendingToken; final String destination; - final int ttlSeconds; const PendingLoginResponse({ required this.account, required this.pendingToken, required this.destination, - required this.ttlSeconds, }); factory PendingLoginResponse.fromJson(Map json) => _$PendingLoginResponseFromJson(json); diff --git a/frontend/pshared/lib/api/responses/confirmation.dart b/frontend/pshared/lib/api/responses/verification/response.dart similarity index 62% rename from frontend/pshared/lib/api/responses/confirmation.dart rename to frontend/pshared/lib/api/responses/verification/response.dart index 01b3b96a..55a37b64 100644 --- a/frontend/pshared/lib/api/responses/confirmation.dart +++ b/frontend/pshared/lib/api/responses/verification/response.dart @@ -1,27 +1,29 @@ import 'package:json_annotation/json_annotation.dart'; -part 'confirmation.g.dart'; +part 'response.g.dart'; @JsonSerializable() -class ConfirmationResponse { +class VerificationResponse { @JsonKey(name: 'ttl_seconds', defaultValue: 0) final int ttlSeconds; @JsonKey(name: 'cooldown_seconds', defaultValue: 0) final int cooldownSeconds; @JsonKey(defaultValue: '') final String destination; + final String idempotencyKey; - const ConfirmationResponse({ + const VerificationResponse({ required this.ttlSeconds, required this.cooldownSeconds, required this.destination, + required this.idempotencyKey, }); Duration get cooldownDuration => Duration(seconds: cooldownSeconds); Duration get ttlDuration => Duration(seconds: ttlSeconds); - factory ConfirmationResponse.fromJson(Map json) => _$ConfirmationResponseFromJson(json); + factory VerificationResponse.fromJson(Map json) => _$VerificationResponseFromJson(json); - Map toJson() => _$ConfirmationResponseToJson(this); + Map toJson() => _$VerificationResponseToJson(this); } diff --git a/frontend/pshared/lib/data/mapper/session_identifier.dart b/frontend/pshared/lib/data/mapper/session_identifier.dart index c04cc524..0ac3cd1e 100644 --- a/frontend/pshared/lib/data/mapper/session_identifier.dart +++ b/frontend/pshared/lib/data/mapper/session_identifier.dart @@ -2,13 +2,13 @@ import 'package:pshared/api/requests/tokens/session_identifier.dart'; import 'package:pshared/models/session_identifier.dart'; extension SessionIdentifierMapper on SessionIdentifier { - SessionIdentifierDto toDTO() => SessionIdentifierDto( + SessionIdentifierDTO toDTO() => SessionIdentifierDTO( clientId: clientId, deviceId: deviceId, ); } -extension SessionIdentifierDtoMapper on SessionIdentifierDto { +extension SessionIdentifierDtoMapper on SessionIdentifierDTO { SessionIdentifier toDomain() => SessionIdentifier( clientId: clientId, deviceId: deviceId, diff --git a/frontend/pshared/lib/models/auth/pending_login.dart b/frontend/pshared/lib/models/auth/pending_login.dart index 0526cda6..22f09ab1 100644 --- a/frontend/pshared/lib/models/auth/pending_login.dart +++ b/frontend/pshared/lib/models/auth/pending_login.dart @@ -9,19 +9,22 @@ class PendingLogin { final Account account; final TokenData pendingToken; final String destination; - final int ttlSeconds; final SessionIdentifier session; + + final int? ttlSeconds; final int? cooldownSeconds; final DateTime? cooldownUntil; + final String? idempotencyKey; const PendingLogin({ required this.account, required this.pendingToken, required this.destination, - required this.ttlSeconds, + this.ttlSeconds, required this.session, this.cooldownSeconds, this.cooldownUntil, + this.idempotencyKey, }); factory PendingLogin.fromResponse( @@ -31,7 +34,6 @@ class PendingLogin { account: response.account.account.toDomain(), pendingToken: response.pendingToken, destination: response.destination, - ttlSeconds: response.ttlSeconds, session: session, ); @@ -44,15 +46,17 @@ class PendingLogin { int? cooldownSeconds, DateTime? cooldownUntil, bool clearCooldown = false, + String? idempotencyKey, }) { return PendingLogin( account: account ?? this.account, pendingToken: pendingToken ?? this.pendingToken, destination: destination ?? this.destination, - ttlSeconds: ttlSeconds ?? this.ttlSeconds, + ttlSeconds: ttlSeconds ?? this.cooldownSeconds, session: session ?? this.session, cooldownSeconds: clearCooldown ? null : cooldownSeconds ?? this.cooldownSeconds, cooldownUntil: clearCooldown ? null : cooldownUntil ?? this.cooldownUntil, + idempotencyKey: idempotencyKey ?? this.idempotencyKey, ); } diff --git a/frontend/pshared/lib/models/confirmation_target.dart b/frontend/pshared/lib/models/verification/purpose.dart similarity index 51% rename from frontend/pshared/lib/models/confirmation_target.dart rename to frontend/pshared/lib/models/verification/purpose.dart index 25f196f4..4d08602f 100644 --- a/frontend/pshared/lib/models/confirmation_target.dart +++ b/frontend/pshared/lib/models/verification/purpose.dart @@ -3,9 +3,15 @@ import 'package:json_annotation/json_annotation.dart'; /// Targets for confirmation codes. @JsonEnum(alwaysCreate: true) -enum ConfirmationTarget { +enum VerificationPurpose { @JsonValue('login') login, @JsonValue('payout') payout, + @JsonValue('account_activation') + accountActivation, + @JsonValue('email_change') + emailChange, + @JsonValue('password_reset') + passwordReset, } diff --git a/frontend/pshared/lib/provider/account.dart b/frontend/pshared/lib/provider/account.dart index 07f9dabc..eb147008 100644 --- a/frontend/pshared/lib/provider/account.dart +++ b/frontend/pshared/lib/provider/account.dart @@ -7,7 +7,7 @@ import 'package:share_plus/share_plus.dart'; import 'package:pshared/api/errors/unauthorized.dart'; import 'package:pshared/api/requests/signup.dart'; import 'package:pshared/api/requests/login_data.dart'; -import 'package:pshared/api/responses/confirmation.dart'; +import 'package:pshared/api/responses/verification/response.dart'; import 'package:pshared/config/constants.dart'; import 'package:pshared/models/account/account.dart'; import 'package:pshared/models/auth/login_outcome.dart'; @@ -93,16 +93,16 @@ class AccountProvider extends ChangeNotifier { password: password, locale: locale, )); - if (outcome.account != null) { + if (outcome.isCompleted) { _authState = AuthState.ready; _setResource(Resource(data: outcome.account, isLoading: false)); _pickupLocale(outcome.account!.locale); } else { final pending = outcome.pending; - if (pending == null) { - throw Exception('Pending login data is missing'); + if (!outcome.isPending || pending == null) { + throw StateError('Pending login data is missing'); } - final confirmation = await VerificationService.requestLoginCode(pending); + final confirmation = await VerificationService.requestLoginCode(pending, target: email); _pendingLogin = _applyConfirmationMeta(pending, confirmation); _authState = AuthState.idle; _setResource(_resource.copyWith(isLoading: false)); @@ -115,7 +115,10 @@ class AccountProvider extends ChangeNotifier { } } - PendingLogin _applyConfirmationMeta(PendingLogin pending, ConfirmationResponse confirmation) { + PendingLogin _applyConfirmationMeta( + PendingLogin pending, + VerificationResponse confirmation, + ) { final ttlSeconds = confirmation.ttlSeconds != 0 ? confirmation.ttlSeconds : pending.ttlSeconds; final destination = confirmation.destination.isNotEmpty ? confirmation.destination : pending.destination; final cooldownSeconds = confirmation.cooldownSeconds; @@ -126,10 +129,11 @@ class AccountProvider extends ChangeNotifier { cooldownSeconds: cooldownSeconds > 0 ? cooldownSeconds : null, cooldownUntil: cooldownSeconds > 0 ? DateTime.now().add(confirmation.cooldownDuration) : null, clearCooldown: cooldownSeconds <= 0, + idempotencyKey: confirmation.idempotencyKey, ); } - void updatePendingLogin(ConfirmationResponse confirmation) { + void updatePendingLogin(VerificationResponse confirmation) { final pending = _pendingLogin; if (pending == null) return; _pendingLogin = _applyConfirmationMeta(pending, confirmation); diff --git a/frontend/pshared/lib/service/account.dart b/frontend/pshared/lib/service/account.dart index f3a2f203..1129c8d7 100644 --- a/frontend/pshared/lib/service/account.dart +++ b/frontend/pshared/lib/service/account.dart @@ -1,4 +1,5 @@ import 'package:logging/logging.dart'; +import 'package:pshared/api/requests/verification/resend.dart'; import 'package:pshared/service/device_id.dart'; import 'package:share_plus/share_plus.dart'; @@ -59,7 +60,7 @@ class AccountService { static Future resendVerificationEmail(String email) async { _logger.fine('Resending verification email'); - await getPUTResponse(_objectType, 'email', {'login': email}); + await getPUTResponse(_objectType, 'email', ResendVerificationEmailRequest.build(login: email).toJson()); } static Future verifyEmail(String token) async { diff --git a/frontend/pshared/lib/service/services.dart b/frontend/pshared/lib/service/services.dart index c2b2be3e..9b7ffc2d 100644 --- a/frontend/pshared/lib/service/services.dart +++ b/frontend/pshared/lib/service/services.dart @@ -1,7 +1,6 @@ class Services { static const String account = 'accounts'; static const String authorization = 'authorization'; - static const String confirmations = 'confirmations'; static const String device = 'device'; static const String invitations = 'invitations'; static const String organization = 'organizations'; @@ -9,6 +8,7 @@ class Services { static const String storage = 'storage'; static const String chainWallets = 'chain_wallets'; static const String ledger = 'ledger_accounts'; + static const String verification = 'verification'; static const String recipients = 'recipients'; static const String paymentMethods = 'payment_methods'; diff --git a/frontend/pshared/lib/service/verification.dart b/frontend/pshared/lib/service/verification.dart index ca318c8e..ec01ae3b 100644 --- a/frontend/pshared/lib/service/verification.dart +++ b/frontend/pshared/lib/service/verification.dart @@ -1,11 +1,13 @@ import 'package:logging/logging.dart'; -import 'package:pshared/api/requests/confirmations/login_confirmation.dart'; +import 'package:uuid/uuid.dart'; + +import 'package:pshared/api/requests/verification/login.dart'; import 'package:pshared/api/responses/login.dart'; +import 'package:pshared/api/responses/verification/response.dart'; +import 'package:pshared/data/mapper/account/account.dart'; import 'package:pshared/data/mapper/session_identifier.dart'; import 'package:pshared/models/account/account.dart'; -import 'package:pshared/data/mapper/account/account.dart'; -import 'package:pshared/api/responses/confirmation.dart'; import 'package:pshared/models/auth/pending_login.dart'; import 'package:pshared/service/authorization/storage.dart'; import 'package:pshared/service/services.dart'; @@ -14,43 +16,50 @@ import 'package:pshared/utils/http/requests.dart'; class VerificationService { static final _logger = Logger('service.verification'); - static const String _objectType = Services.confirmations; + static const String _objectType = Services.verification; - static Future requestLoginCode(PendingLogin pending, {String? destination}) async { + static Future requestLoginCode(PendingLogin pending, {String? target}) async { _logger.fine('Requesting login confirmation code'); final response = await getPOSTResponse( _objectType, '', - LoginConfirmationRequest(destination: destination).toJson(), + LoginVerificationRequest( + target: target, + idempotencyKey: Uuid().v4(), + ).toJson(), authToken: pending.pendingToken.token, ); - return ConfirmationResponse.fromJson(response); + return VerificationResponse.fromJson(response); } - static Future resendLoginCode(PendingLogin pending, {String? destination}) async { + static Future resendLoginCode(PendingLogin pending, {String? destination}) async { _logger.fine('Resending login confirmation code'); final response = await getPOSTResponse( _objectType, '/resend', - LoginConfirmationRequest(destination: destination).toJson(), + LoginVerificationRequest( + target: destination, + idempotencyKey: pending.idempotencyKey ?? Uuid().v4(), + ).toJson(), authToken: pending.pendingToken.token, ); - return ConfirmationResponse.fromJson(response); + return VerificationResponse.fromJson(response); } static Future confirmLoginCode({ required PendingLogin pending, required String code, - String? destination, + String? target, }) async { _logger.fine('Confirming login code'); final response = await getPOSTResponse( _objectType, '/verify', - LoginConfirmationVerifyRequest( + LoginCodeVerifyicationRequest( code: code, - destination: destination, + target: target, sessionIdentifier: pending.session.toDTO(), + idempotencyKey: pending.idempotencyKey ?? Uuid().v4(), ).toJson(), authToken: pending.pendingToken.token, ); From 7c182afd2364584a89b8100add89495e87569c1d Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 10 Feb 2026 02:11:22 +0100 Subject: [PATCH 2/2] fixed token errors --- .../internal/mongo/verificationimp/consume.go | 85 +++++++++++-------- .../verificationimp/verification_test.go | 30 +++++-- 2 files changed, 72 insertions(+), 43 deletions(-) diff --git a/api/pkg/db/internal/mongo/verificationimp/consume.go b/api/pkg/db/internal/mongo/verificationimp/consume.go index e37fd764..d9547130 100644 --- a/api/pkg/db/internal/mongo/verificationimp/consume.go +++ b/api/pkg/db/internal/mongo/verificationimp/consume.go @@ -11,9 +11,7 @@ import ( "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/model" mutil "github.com/tech/sendico/pkg/mutil/db" - "github.com/tech/sendico/pkg/mutil/mzap" "go.mongodb.org/mongo-driver/v2/bson" - "go.uber.org/zap" ) func (db *verificationDB) Consume( @@ -24,59 +22,74 @@ func (db *verificationDB) Consume( ) (*model.VerificationToken, error) { now := time.Now().UTC() + accountScoped := accountRef != bson.NilObjectID t, e := db.tf.CreateTransaction().Execute( ct, func(ctx context.Context) (any, error) { - // 1) Load active tokens for this context - activeFilter := repository.Query().And( - repository.Filter("accountRef", accountRef), + scopeFilter := repository.Query().And( repository.Filter("purpose", purpose), - repository.Filter("usedAt", nil), - repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now), ) + if accountScoped { + scopeFilter = scopeFilter.And(repository.Filter("accountRef", accountRef)) + } - tokens, err := mutil.GetObjects[model.VerificationToken]( - ctx, db.Logger, activeFilter, nil, db.DBImp.Repository, + // 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)), ) - if err != nil { - if errors.Is(err, merrors.ErrNoData) { - db.Logger.Debug("No tokens found", zap.Error(err), mzap.AccRef(accountRef), zap.String("purpose", string(purpose))) - return nil, verification.ErorrTokenNotFound() - } - db.Logger.Warn("Failed to load active tokens", zap.Error(err), mzap.AccRef(accountRef), zap.String("purpose", string(purpose))) + 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 len(tokens) == 0 { - db.Logger.Debug("No tokens found", zap.Error(err), mzap.AccRef(accountRef), zap.String("purpose", string(purpose))) + // If account is unknown, do not scan OTP candidates globally. + if token == nil && !accountScoped { return nil, verification.ErorrTokenNotFound() } - // 2) Find matching token via hasher (OTP or Magic — doesn't matter) - var token *model.VerificationToken - - for i := range tokens { - t := &tokens[i] - hash := hasherFor(t).Hash(rawToken, t) - - if hash == t.VerifyTokenHash { - token = t - break - } - } - + // 2) OTP path (and fallback): load purpose/account scoped tokens and compare hash with per-token salt. if token == nil { - // wrong code/token → increment attempts - for _, t := range tokens { + 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, - repository.IDFilter(t.ID), + activeFilter, repository.Patch().Inc(repository.Field("attempts"), 1), ) + return nil, verification.ErorrTokenNotFound() } - return nil, verification.ErorrTokenNotFound() } // 3) Static checks @@ -93,11 +106,13 @@ func (db *verificationDB) Consume( // 4) Atomic consume consumeFilter := repository.Query().And( repository.IDFilter(token.ID), - repository.Filter("accountRef", accountRef), 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( diff --git a/api/pkg/db/internal/mongo/verificationimp/verification_test.go b/api/pkg/db/internal/mongo/verificationimp/verification_test.go index 71ab2283..e63fb239 100644 --- a/api/pkg/db/internal/mongo/verificationimp/verification_test.go +++ b/api/pkg/db/internal/mongo/verificationimp/verification_test.go @@ -553,8 +553,8 @@ func TestConsume_SecondConsumeFailsAlreadyUsed(t *testing.T) { _, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, raw) require.Error(t, err) - assert.True(t, errors.Is(err, verification.ErrTokenNotFound), - "second consume should fail — used tokens are excluded from active filter") + assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed), + "second consume should fail with already-used after usedAt is set") } func TestConsume_ExpiredTokenFails(t *testing.T) { @@ -568,8 +568,8 @@ func TestConsume_ExpiredTokenFails(t *testing.T) { _, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, raw) require.Error(t, err) - assert.True(t, errors.Is(err, verification.ErrTokenNotFound), - "expired token is excluded from active filter") + assert.True(t, errors.Is(err, verification.ErrTokenExpired), + "expired token should return explicit expiry error") } func TestConsume_UnknownTokenFails(t *testing.T) { @@ -581,6 +581,20 @@ func TestConsume_UnknownTokenFails(t *testing.T) { 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() @@ -596,8 +610,8 @@ func TestCreate_InvalidatesPreviousToken(t *testing.T) { // 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.ErrTokenNotFound), - "old token should be invalidated after new token creation") + assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed), + "old token should return already-used after invalidation") // New token works fine. tok, err := db.Consume(ctx, accountRef, model.PurposePasswordReset, newRaw) @@ -618,9 +632,9 @@ func TestCreate_InvalidatesMultiplePreviousTokens(t *testing.T) { require.NoError(t, err) _, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, first) - assert.True(t, errors.Is(err, verification.ErrTokenNotFound), "first should be invalidated") + 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.ErrTokenNotFound), "second should be invalidated") + assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed), "second should be invalidated/used") tok, err := db.Consume(ctx, accountRef, model.PurposePasswordReset, third) require.NoError(t, err)