From 1ab7f2e7d3378a5e866b4546074e5b7f8f05de56 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Mon, 17 Nov 2025 18:00:38 +0100 Subject: [PATCH 01/32] +signup +email availability check --- api/pkg/model/userdata.go | 8 +- api/server/config.yml | 12 +- api/server/go.mod | 11 +- api/server/go.sum | 16 +-- api/server/interface/api/config.go | 19 ++- api/server/interface/api/srequest/signup.go | 2 +- .../interface/api/srequest/signup_test.go | 32 +++-- .../api/sresponse/signup_availability.go | 23 +++ .../internal/server/accountapiimp/service.go | 93 +++++++++++- .../internal/server/accountapiimp/signup.go | 109 +++++++++++++- .../accountapiimp/signup_integration_test.go | 28 ++-- .../server/accountapiimp/signup_test.go | 133 +++++++++++++++++- frontend/pshared/lib/api/requests/signup.dart | 78 +++++++--- 13 files changed, 496 insertions(+), 68 deletions(-) create mode 100644 api/server/interface/api/sresponse/signup_availability.go diff --git a/api/pkg/model/userdata.go b/api/pkg/model/userdata.go index 892b7ad..d06095e 100644 --- a/api/pkg/model/userdata.go +++ b/api/pkg/model/userdata.go @@ -11,17 +11,15 @@ type LoginData struct { } type AccountData struct { - LoginData `bson:",inline" json:",inline"` - Name string `bson:"name" json:"name"` + LoginData `bson:",inline" json:",inline"` + Describable `bson:",inline" json:",inline"` } func (ad *AccountData) ToAccount() *Account { return &Account{ AccountPublic: AccountPublic{ AccountBase: AccountBase{ - Describable: Describable{ - Name: ad.Name, - }, + Describable: ad.Describable, }, UserDataBase: ad.UserDataBase, }, diff --git a/api/server/config.yml b/api/server/config.yml index 6b401da..80950c8 100755 --- a/api/server/config.yml +++ b/api/server/config.yml @@ -74,6 +74,16 @@ api: settings: root_path: ./storage + chain_gateway: + address_env: CHAIN_GATEWAY_ADDRESS + dial_timeout_seconds: 5 + call_timeout_seconds: 5 + insecure: true + default_asset: + chain: ARBITRUM_ONE + token_symbol: USDT + contract_address: "" + app: database: @@ -94,4 +104,4 @@ database: collection_name_env: PERMISSION_COLLECTION database_name_env: MONGO_DATABASE timeout_seconds_env: PERMISSION_TIMEOUT - is_filtered_env: PERMISSION_IS_FILTERED \ No newline at end of file + is_filtered_env: PERMISSION_IS_FILTERED diff --git a/api/server/go.mod b/api/server/go.mod index 940de09..fd249c5 100644 --- a/api/server/go.mod +++ b/api/server/go.mod @@ -4,6 +4,8 @@ go 1.25.3 replace github.com/tech/sendico/pkg => ../pkg +replace github.com/tech/sendico/chain/gateway => ../chain/gateway + require ( github.com/aws/aws-sdk-go-v2 v1.39.6 github.com/aws/aws-sdk-go-v2/config v1.31.18 @@ -13,8 +15,10 @@ require ( github.com/go-chi/cors v1.2.2 github.com/go-chi/jwtauth/v5 v5.3.3 github.com/go-chi/metrics v0.1.1 + github.com/google/uuid v1.6.0 github.com/mitchellh/mapstructure v1.5.0 github.com/stretchr/testify v1.11.1 + github.com/tech/sendico/chain/gateway v0.1.0 github.com/tech/sendico/pkg v0.1.0 github.com/testcontainers/testcontainers-go v0.33.0 github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 @@ -27,7 +31,7 @@ require ( require ( github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect - github.com/casbin/casbin/v2 v2.132.0 // indirect + github.com/casbin/casbin/v2 v2.134.0 // indirect github.com/casbin/govaluate v1.10.0 // indirect ) @@ -70,7 +74,6 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v1.0.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.1 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect @@ -107,8 +110,8 @@ require ( github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/api/server/go.sum b/api/server/go.sum index b1939fb..8752498 100644 --- a/api/server/go.sum +++ b/api/server/go.sum @@ -48,8 +48,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= -github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= +github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM= +github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= @@ -228,10 +228,10 @@ github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5 github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY= github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -338,8 +338,8 @@ 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -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/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= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= diff --git a/api/server/interface/api/config.go b/api/server/interface/api/config.go index bee2ca9..c1f8850 100644 --- a/api/server/interface/api/config.go +++ b/api/server/interface/api/config.go @@ -6,6 +6,21 @@ import ( ) type Config struct { - Mw *mwa.Config `yaml:"middleware"` - Storage *fsc.Config `yaml:"storage"` + Mw *mwa.Config `yaml:"middleware"` + Storage *fsc.Config `yaml:"storage"` + ChainGateway *ChainGatewayConfig `yaml:"chain_gateway"` +} + +type ChainGatewayConfig struct { + AddressEnv string `yaml:"address_env"` + DialTimeoutSeconds int `yaml:"dial_timeout_seconds"` + CallTimeoutSeconds int `yaml:"call_timeout_seconds"` + Insecure bool `yaml:"insecure"` + DefaultAsset ChainGatewayAssetConfig `yaml:"default_asset"` +} + +type ChainGatewayAssetConfig struct { + Chain string `yaml:"chain"` + TokenSymbol string `yaml:"token_symbol"` + ContractAddress string `yaml:"contract_address"` } diff --git a/api/server/interface/api/srequest/signup.go b/api/server/interface/api/srequest/signup.go index 2f96c04..baac565 100644 --- a/api/server/interface/api/srequest/signup.go +++ b/api/server/interface/api/srequest/signup.go @@ -4,7 +4,7 @@ import "github.com/tech/sendico/pkg/model" type Signup struct { Account model.AccountData `json:"account"` - OrganizationName string `json:"organizationName"` + Organization model.Describable `json:"organization"` OrganizationTimeZone string `json:"organizationTimeZone"` AnonymousUser model.Describable `json:"anonymousUser"` OwnerRole model.Describable `json:"ownerRole"` diff --git a/api/server/interface/api/srequest/signup_test.go b/api/server/interface/api/srequest/signup_test.go index 0b284ef..71107b7 100644 --- a/api/server/interface/api/srequest/signup_test.go +++ b/api/server/interface/api/srequest/signup_test.go @@ -20,9 +20,13 @@ func TestSignupRequest_JSONSerialization(t *testing.T) { }, Password: "TestPassword123!", }, - Name: "Test User", + Describable: model.Describable{ + Name: "Test User", + }, + }, + Organization: model.Describable{ + Name: "Test Organization", }, - OrganizationName: "Test Organization", OrganizationTimeZone: "UTC", AnonymousUser: model.Describable{ Name: "Anonymous User", @@ -49,7 +53,7 @@ func TestSignupRequest_JSONSerialization(t *testing.T) { assert.Equal(t, signup.Account.Name, unmarshaled.Account.Name) assert.Equal(t, signup.Account.Login, unmarshaled.Account.Login) assert.Equal(t, signup.Account.Password, unmarshaled.Account.Password) - assert.Equal(t, signup.OrganizationName, unmarshaled.OrganizationName) + assert.Equal(t, signup.Organization.Name, unmarshaled.Organization.Name) assert.Equal(t, signup.OrganizationTimeZone, unmarshaled.OrganizationTimeZone) assert.Equal(t, signup.AnonymousUser.Name, unmarshaled.AnonymousUser.Name) assert.Equal(t, signup.OwnerRole.Name, unmarshaled.OwnerRole.Name) @@ -65,9 +69,13 @@ func TestSignupRequest_MinimalValidRequest(t *testing.T) { }, Password: "TestPassword123!", }, - Name: "Test User", + Describable: model.Describable{ + Name: "Test User", + }, + }, + Organization: model.Describable{ + Name: "Test Organization", }, - OrganizationName: "Test Organization", OrganizationTimeZone: "UTC", AnonymousUser: model.Describable{ Name: "Anonymous", @@ -93,13 +101,13 @@ func TestSignupRequest_MinimalValidRequest(t *testing.T) { // Verify minimal request is valid assert.Equal(t, signup.Account.Name, unmarshaled.Account.Name) assert.Equal(t, signup.Account.Login, unmarshaled.Account.Login) - assert.Equal(t, signup.OrganizationName, unmarshaled.OrganizationName) + assert.Equal(t, signup.Organization.Name, unmarshaled.Organization.Name) } func TestSignupRequest_InvalidJSON(t *testing.T) { invalidJSONs := []string{ `{"account": invalid}`, - `{"organizationName": 123}`, + `{"organization": 123}`, `{"organizationTimeZone": true}`, `{"defaultPriorityGroup": "not_an_object"}`, `{"anonymousUser": []}`, @@ -125,9 +133,13 @@ func TestSignupRequest_UnicodeCharacters(t *testing.T) { }, Password: "TestPassword123!", }, - Name: "Test 用户 Üser", + Describable: model.Describable{ + Name: "Test 用户 Üser", + }, + }, + Organization: model.Describable{ + Name: "测试 Organization", }, - OrganizationName: "测试 Organization", OrganizationTimeZone: "UTC", AnonymousUser: model.Describable{ Name: "匿名 User", @@ -153,7 +165,7 @@ func TestSignupRequest_UnicodeCharacters(t *testing.T) { // Verify unicode characters are properly handled assert.Equal(t, "测试@example.com", unmarshaled.Account.Login) assert.Equal(t, "Test 用户 Üser", unmarshaled.Account.Name) - assert.Equal(t, "测试 Organization", unmarshaled.OrganizationName) + assert.Equal(t, "测试 Organization", unmarshaled.Organization.Name) assert.Equal(t, "匿名 User", unmarshaled.AnonymousUser.Name) assert.Equal(t, "所有者", unmarshaled.OwnerRole.Name) assert.Equal(t, "匿名", unmarshaled.AnonymousRole.Name) diff --git a/api/server/interface/api/sresponse/signup_availability.go b/api/server/interface/api/sresponse/signup_availability.go new file mode 100644 index 0000000..f5ca161 --- /dev/null +++ b/api/server/interface/api/sresponse/signup_availability.go @@ -0,0 +1,23 @@ +package sresponse + +import ( + "net/http" + + "github.com/tech/sendico/pkg/api/http/response" + "github.com/tech/sendico/pkg/mlogger" +) + +type SignupAvailability struct { + Login string `json:"login"` + Available bool `json:"available"` +} + +func SignUpAvailability(logger mlogger.Logger, login string, available bool) http.HandlerFunc { + return response.Ok( + logger, + SignupAvailability{ + Login: login, + Available: available, + }, + ) +} diff --git a/api/server/internal/server/accountapiimp/service.go b/api/server/internal/server/accountapiimp/service.go index 7425b03..169d464 100644 --- a/api/server/internal/server/accountapiimp/service.go +++ b/api/server/internal/server/accountapiimp/service.go @@ -2,7 +2,12 @@ package accountapiimp import ( "context" + "fmt" + "os" + "strings" + "time" + chaingatewayclient "github.com/tech/sendico/chain/gateway/client" api "github.com/tech/sendico/pkg/api/http" "github.com/tech/sendico/pkg/auth" "github.com/tech/sendico/pkg/db/account" @@ -14,6 +19,7 @@ import ( "github.com/tech/sendico/pkg/messaging" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mservice" + gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1" "github.com/tech/sendico/server/interface/accountservice" eapi "github.com/tech/sendico/server/interface/api" "github.com/tech/sendico/server/interface/services/fileservice" @@ -39,6 +45,13 @@ type AccountAPI struct { tph mutil.ParamHelper accountsPermissionRef primitive.ObjectID accService accountservice.AccountService + chainGateway chainWalletClient + chainAsset *gatewayv1.Asset +} + +type chainWalletClient interface { + CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) + Close() error } func (a *AccountAPI) Name() mservice.Type { @@ -46,7 +59,15 @@ func (a *AccountAPI) Name() mservice.Type { } func (a *AccountAPI) Finish(ctx context.Context) error { - return a.avatars.Finish(ctx) + if err := a.avatars.Finish(ctx); err != nil { + return err + } + if a.chainGateway != nil { + if err := a.chainGateway.Close(); err != nil { + a.logger.Warn("Failed to close chain gateway client", zap.Error(err)) + } + } + return nil } func CreateAPI(a eapi.API) (*AccountAPI, error) { @@ -86,6 +107,7 @@ func CreateAPI(a eapi.API) (*AccountAPI, error) { // Account related api endpoints a.Register().Handler(mservice.Accounts, "/signup", api.Post, p.signup) + a.Register().Handler(mservice.Accounts, "/signup/availability", api.Get, p.signupAvailability) a.Register().AccountHandler(mservice.Accounts, "", api.Put, p.updateProfile) a.Register().AccountHandler(mservice.Accounts, "", api.Get, p.getProfile) @@ -120,5 +142,74 @@ func CreateAPI(a eapi.API) (*AccountAPI, error) { } p.accountsPermissionRef = accountsPolicy.ID + if err := p.initChainGateway(a.Config()); err != nil { + p.logger.Error("Failed to initialize chain gateway client", zap.Error(err)) + return nil, err + } + return p, nil } + +func (a *AccountAPI) initChainGateway(cfg *eapi.Config) error { + if cfg == nil || cfg.ChainGateway == nil { + return fmt.Errorf("chain gateway configuration is not provided") + } + + address := strings.TrimSpace(os.Getenv(cfg.ChainGateway.AddressEnv)) + if address == "" { + return fmt.Errorf("chain gateway address env %s is empty", cfg.ChainGateway.AddressEnv) + } + + clientCfg := chaingatewayclient.Config{ + Address: address, + DialTimeout: time.Duration(cfg.ChainGateway.DialTimeoutSeconds) * time.Second, + CallTimeout: time.Duration(cfg.ChainGateway.CallTimeoutSeconds) * time.Second, + Insecure: cfg.ChainGateway.Insecure, + } + + client, err := chaingatewayclient.New(context.Background(), clientCfg) + if err != nil { + return err + } + + asset, err := buildGatewayAsset(cfg.ChainGateway.DefaultAsset) + if err != nil { + _ = client.Close() + return err + } + + a.chainGateway = client + a.chainAsset = asset + return nil +} + +func buildGatewayAsset(cfg eapi.ChainGatewayAssetConfig) (*gatewayv1.Asset, error) { + chain, err := parseChainNetwork(cfg.Chain) + if err != nil { + return nil, err + } + tokenSymbol := strings.TrimSpace(cfg.TokenSymbol) + if tokenSymbol == "" { + return nil, fmt.Errorf("chain gateway token symbol is required") + } + return &gatewayv1.Asset{ + Chain: chain, + TokenSymbol: strings.ToUpper(tokenSymbol), + ContractAddress: strings.ToLower(strings.TrimSpace(cfg.ContractAddress)), + }, nil +} + +func parseChainNetwork(value string) (gatewayv1.ChainNetwork, error) { + switch strings.ToUpper(strings.TrimSpace(value)) { + case "ETHEREUM_MAINNET", "CHAIN_NETWORK_ETHEREUM_MAINNET": + return gatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, nil + case "ARBITRUM_ONE", "CHAIN_NETWORK_ARBITRUM_ONE": + return gatewayv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE, nil + case "OTHER_EVM", "CHAIN_NETWORK_OTHER_EVM": + return gatewayv1.ChainNetwork_CHAIN_NETWORK_OTHER_EVM, nil + case "", "CHAIN_NETWORK_UNSPECIFIED": + return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, fmt.Errorf("chain network must be specified") + default: + return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, fmt.Errorf("unsupported chain network %s", value) + } +} diff --git a/api/server/internal/server/accountapiimp/signup.go b/api/server/internal/server/accountapiimp/signup.go index a770d12..c57df70 100644 --- a/api/server/internal/server/accountapiimp/signup.go +++ b/api/server/internal/server/accountapiimp/signup.go @@ -6,13 +6,16 @@ import ( "errors" "fmt" "net/http" + "strings" "time" + "github.com/google/uuid" "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/mzap" + gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1" "github.com/tech/sendico/server/interface/api/srequest" "github.com/tech/sendico/server/interface/api/sresponse" "go.mongodb.org/mongo-driver/bson/primitive" @@ -41,6 +44,10 @@ func (a *AccountAPI) createAnonymousAccount(ctx context.Context, org *model.Orga } func (a *AccountAPI) createOrg(ctx context.Context, sr *srequest.Signup, permissionRef primitive.ObjectID) (*model.Organization, error) { + name := strings.TrimSpace(sr.Organization.Name) + if name == "" { + return nil, merrors.InvalidArgument("organization name must not be empty") + } if _, err := time.LoadLocation(sr.OrganizationTimeZone); err != nil { return nil, merrors.DataConflict(fmt.Sprintf("invalid time zone '%s' provided, error %s", sr.OrganizationTimeZone, err.Error())) } @@ -51,7 +58,8 @@ func (a *AccountAPI) createOrg(ctx context.Context, sr *srequest.Signup, permiss PermissionRef: permissionRef, }, Describable: model.Describable{ - Name: sr.OrganizationName, + Name: name, + Description: sr.Organization.Description, }, TimeZone: sr.OrganizationTimeZone, }, @@ -74,6 +82,18 @@ func (a *AccountAPI) signup(r *http.Request) http.HandlerFunc { return response.BadRequest(a.logger, a.Name(), "", err.Error()) } + sr.Account.Login = strings.ToLower(strings.TrimSpace(sr.Account.Login)) + if err := a.ensureLoginAvailable(r.Context(), sr.Account.Login); err != nil { + if errors.Is(err, merrors.ErrDataConflict) { + return response.DataConflict(a.logger, "user_already_registered", "User has already been registered") + } + if errors.Is(err, merrors.ErrInvalidArg) { + return response.BadPayload(a.logger, a.Name(), err) + } + a.logger.Warn("Failed to validate login availability", zap.Error(err), zap.String("login", sr.Account.Login)) + return response.Internal(a.logger, a.Name(), err) + } + newAccount := sr.Account.ToAccount() if res := a.accService.ValidateAccount(newAccount); res != nil { a.logger.Warn("Invalid signup account received", zap.Error(res), zap.String("account", newAccount.Login)) @@ -96,6 +116,26 @@ func (a *AccountAPI) signup(r *http.Request) http.HandlerFunc { return sresponse.SignUp(a.logger, newAccount) } +func (a *AccountAPI) signupAvailability(r *http.Request) http.HandlerFunc { + login := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("login"))) + if login == "" { + return response.BadRequest(a.logger, a.Name(), "missing_login", "login query parameter is required") + } + + err := a.ensureLoginAvailable(r.Context(), login) + switch { + case err == nil: + return sresponse.SignUpAvailability(a.logger, login, true) + case errors.Is(err, merrors.ErrDataConflict): + return sresponse.SignUpAvailability(a.logger, login, false) + case errors.Is(err, merrors.ErrInvalidArg): + return response.BadPayload(a.logger, a.Name(), err) + default: + a.logger.Warn("Failed to check login availability", zap.Error(err), zap.String("login", login)) + return response.Internal(a.logger, a.Name(), err) + } +} + func (a *AccountAPI) executeSignupTransaction(ctxt context.Context, sr *srequest.Signup, newAccount *model.Account) error { _, err := a.tf.CreateTransaction().Execute(ctxt, func(ctx context.Context) (any, error) { return a.signupTransactionBody(ctx, sr, newAccount) @@ -116,6 +156,10 @@ func (a *AccountAPI) signupTransactionBody(ctx context.Context, sr *srequest.Sig return nil, err } + if err := a.openOrgWallet(ctx, org, sr); err != nil { + return nil, err + } + roleDescription, err := a.pmanager.Role().Create(ctx, org.ID, &sr.OwnerRole) if err != nil { a.logger.Warn("Failed to create owner role", zap.Error(err), zap.String("login", newAccount.Login)) @@ -146,8 +190,21 @@ func (a *AccountAPI) grantAllPermissions(ctx context.Context, organizationRef pr return err } + required := map[mservice.Type]bool{ + mservice.Organizations: false, + mservice.Accounts: false, + mservice.LedgerAccounts: false, + } + actions := []model.Action{model.ActionCreate, model.ActionRead, model.ActionUpdate, model.ActionDelete} for _, policy := range policies { + if policy.ResourceTypes != nil { + for _, resource := range *policy.ResourceTypes { + if _, ok := required[resource]; ok { + required[resource] = true + } + } + } for _, action := range actions { a.logger.Debug("Adding permission", mzap.StorableRef(&policy), zap.String("action", string(action)), mzap.ObjRef("role_ref", roleID), mzap.ObjRef("policy_ref", policy.ID), mzap.ObjRef("organization_ref", organizationRef)) @@ -172,5 +229,55 @@ func (a *AccountAPI) grantAllPermissions(ctx context.Context, organizationRef pr return err } + for resource, granted := range required { + if !granted { + a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", string(resource))) + } + } + + return nil +} + +func (a *AccountAPI) ensureLoginAvailable(ctx context.Context, login string) error { + if strings.TrimSpace(login) == "" { + return merrors.InvalidArgument("login must not be empty") + } + if _, err := a.db.GetByEmail(ctx, login); err != nil { + if errors.Is(err, merrors.ErrNoData) { + return nil + } + a.logger.Warn("Failed to lookup account by login", zap.Error(err), zap.String("login", login)) + return err + } + return merrors.DataConflict("account already exists") +} + +func (a *AccountAPI) openOrgWallet(ctx context.Context, org *model.Organization, sr *srequest.Signup) error { + if a.chainGateway == nil || a.chainAsset == nil { + a.logger.Warn("Chain gateway client not configured, skipping wallet creation", mzap.StorableRef(org)) + return merrors.Internal("chain gateway client is not configured") + } + asset := *a.chainAsset + req := &gatewayv1.CreateManagedWalletRequest{ + IdempotencyKey: uuid.NewString(), + OrganizationRef: org.ID.Hex(), + OwnerRef: org.ID.Hex(), + Asset: &asset, + Metadata: map[string]string{ + "source": "signup", + "login": sr.Account.Login, + }, + } + + resp, err := a.chainGateway.CreateManagedWallet(ctx, req) + if err != nil { + a.logger.Warn("Failed to create managed wallet for organization", zap.Error(err), mzap.StorableRef(org)) + return err + } + if resp == nil || resp.Wallet == nil || strings.TrimSpace(resp.Wallet.WalletRef) == "" { + return merrors.Internal("chain gateway returned empty wallet reference") + } + + a.logger.Info("Managed wallet created for organization", mzap.StorableRef(org), zap.String("wallet_ref", resp.Wallet.WalletRef)) return nil } diff --git a/api/server/internal/server/accountapiimp/signup_integration_test.go b/api/server/internal/server/accountapiimp/signup_integration_test.go index 54baae2..3337eb8 100644 --- a/api/server/internal/server/accountapiimp/signup_integration_test.go +++ b/api/server/internal/server/accountapiimp/signup_integration_test.go @@ -65,9 +65,13 @@ func TestSignupRequestSerialization(t *testing.T) { }, Password: "TestPassword123!", }, - Name: "Test User", + Describable: model.Describable{ + Name: "Test User", + }, + }, + Organization: model.Describable{ + Name: "Test Organization", }, - OrganizationName: "Test Organization", OrganizationTimeZone: "UTC", AnonymousUser: model.Describable{ Name: "Anonymous User", @@ -93,7 +97,7 @@ func TestSignupRequestSerialization(t *testing.T) { // Verify data integrity assert.Equal(t, signupRequest.Account.Login, retrieved.Account.Login) assert.Equal(t, signupRequest.Account.Name, retrieved.Account.Name) - assert.Equal(t, signupRequest.OrganizationName, retrieved.OrganizationName) + assert.Equal(t, signupRequest.Organization.Name, retrieved.Organization.Name) assert.Equal(t, signupRequest.OrganizationTimeZone, retrieved.OrganizationTimeZone) }) @@ -109,9 +113,13 @@ func TestSignupHTTPSerialization(t *testing.T) { }, Password: "TestPassword123!", }, - Name: "Test User", + Describable: model.Describable{ + Name: "Test User", + }, + }, + Organization: model.Describable{ + Name: "Test Organization", }, - OrganizationName: "Test Organization", OrganizationTimeZone: "UTC", AnonymousUser: model.Describable{ Name: "Anonymous User", @@ -141,13 +149,13 @@ func TestSignupHTTPSerialization(t *testing.T) { // Verify parsing assert.Equal(t, signupRequest.Account.Login, parsedRequest.Account.Login) assert.Equal(t, signupRequest.Account.Name, parsedRequest.Account.Name) - assert.Equal(t, signupRequest.OrganizationName, parsedRequest.OrganizationName) + assert.Equal(t, signupRequest.Organization.Name, parsedRequest.Organization.Name) }) t.Run("UnicodeCharacters", func(t *testing.T) { unicodeRequest := signupRequest unicodeRequest.Account.Name = "Test 用户 Üser" - unicodeRequest.OrganizationName = "测试 Organization" + unicodeRequest.Organization.Name = "测试 Organization" // Serialize to JSON reqBody, err := json.Marshal(unicodeRequest) @@ -160,7 +168,7 @@ func TestSignupHTTPSerialization(t *testing.T) { // Verify unicode characters are preserved assert.Equal(t, "Test 用户 Üser", parsedRequest.Account.Name) - assert.Equal(t, "测试 Organization", parsedRequest.OrganizationName) + assert.Equal(t, "测试 Organization", parsedRequest.Organization.Name) }) t.Run("InvalidJSONRequest", func(t *testing.T) { @@ -184,7 +192,9 @@ func TestAccountDataConversion(t *testing.T) { }, Password: "TestPassword123!", }, - Name: "Test User", + Describable: model.Describable{ + Name: "Test User", + }, } t.Run("ToAccount", func(t *testing.T) { diff --git a/api/server/internal/server/accountapiimp/signup_test.go b/api/server/internal/server/accountapiimp/signup_test.go index 276ecac..87687ce 100644 --- a/api/server/internal/server/accountapiimp/signup_test.go +++ b/api/server/internal/server/accountapiimp/signup_test.go @@ -1,12 +1,18 @@ package accountapiimp import ( + "context" + "errors" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/tech/sendico/pkg/db/repository/builder" + "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/server/interface/api/srequest" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" ) // Helper function to create string pointers @@ -60,9 +66,13 @@ func TestCreateValidSignupRequest(t *testing.T) { }, Password: "TestPassword123!", }, - Name: "Test User", + Describable: model.Describable{ + Name: "Test User", + }, + }, + Organization: model.Describable{ + Name: "Test Organization", }, - OrganizationName: "Test Organization", OrganizationTimeZone: "UTC", AnonymousUser: model.Describable{ Name: "Anonymous User", @@ -79,7 +89,7 @@ func TestCreateValidSignupRequest(t *testing.T) { assert.Equal(t, "test@example.com", request.Account.Login) assert.Equal(t, "TestPassword123!", request.Account.Password) assert.Equal(t, "Test User", request.Account.Name) - assert.Equal(t, "Test Organization", request.OrganizationName) + assert.Equal(t, "Test Organization", request.Organization.Name) assert.Equal(t, "UTC", request.OrganizationTimeZone) } @@ -94,9 +104,13 @@ func TestSignupRequestValidation(t *testing.T) { }, Password: "TestPassword123!", }, - Name: "Test User", + Describable: model.Describable{ + Name: "Test User", + }, + }, + Organization: model.Describable{ + Name: "Test Organization", }, - OrganizationName: "Test Organization", OrganizationTimeZone: "UTC", } @@ -104,7 +118,7 @@ func TestSignupRequestValidation(t *testing.T) { assert.NotEmpty(t, request.Account.Login) assert.NotEmpty(t, request.Account.Password) assert.NotEmpty(t, request.Account.Name) - assert.NotEmpty(t, request.OrganizationName) + assert.NotEmpty(t, request.Organization.Name) assert.NotEmpty(t, request.OrganizationTimeZone) }) @@ -205,7 +219,9 @@ func TestAccountDataToAccount(t *testing.T) { }, Password: "TestPassword123!", }, - Name: "Test User", + Describable: model.Describable{ + Name: "Test User", + }, } account := accountData.ToAccount() @@ -240,3 +256,106 @@ func TestColorValidation(t *testing.T) { }) } } + +type stubAccountDB struct { + result *model.Account + err error +} + +func (s *stubAccountDB) GetByEmail(ctx context.Context, email string) (*model.Account, error) { + return s.result, s.err +} + +func (s *stubAccountDB) GetByToken(ctx context.Context, email string) (*model.Account, error) { + return nil, merrors.NotImplemented("stub") +} + +func (s *stubAccountDB) GetAccountsByRefs(ctx context.Context, orgRef primitive.ObjectID, refs []primitive.ObjectID) ([]model.Account, error) { + return nil, merrors.NotImplemented("stub") +} + +func (s *stubAccountDB) Create(ctx context.Context, object *model.Account) error { + return merrors.NotImplemented("stub") +} + +func (s *stubAccountDB) InsertMany(ctx context.Context, objects []*model.Account) error { + return merrors.NotImplemented("stub") +} + +func (s *stubAccountDB) Get(ctx context.Context, objectRef primitive.ObjectID, result *model.Account) error { + return merrors.NotImplemented("stub") +} + +func (s *stubAccountDB) Update(ctx context.Context, object *model.Account) error { + return merrors.NotImplemented("stub") +} + +func (s *stubAccountDB) Patch(ctx context.Context, objectRef primitive.ObjectID, patch builder.Patch) error { + return merrors.NotImplemented("stub") +} + +func (s *stubAccountDB) Delete(ctx context.Context, objectRef primitive.ObjectID) error { + return merrors.NotImplemented("stub") +} + +func (s *stubAccountDB) DeleteMany(ctx context.Context, query builder.Query) error { + return merrors.NotImplemented("stub") +} + +func (s *stubAccountDB) DeleteCascade(ctx context.Context, objectRef primitive.ObjectID) error { + return merrors.NotImplemented("stub") +} + +func (s *stubAccountDB) FindOne(ctx context.Context, query builder.Query, result *model.Account) error { + return merrors.NotImplemented("stub") +} + +func TestEnsureLoginAvailable(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + + t.Run("available", func(t *testing.T) { + api := &AccountAPI{ + logger: logger, + db: &stubAccountDB{ + err: merrors.ErrNoData, + }, + } + assert.NoError(t, api.ensureLoginAvailable(ctx, "new@example.com")) + }) + + t.Run("taken", func(t *testing.T) { + api := &AccountAPI{ + logger: logger, + db: &stubAccountDB{ + result: &model.Account{}, + }, + } + err := api.ensureLoginAvailable(ctx, "used@example.com") + assert.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrDataConflict)) + }) + + t.Run("invalid login", func(t *testing.T) { + api := &AccountAPI{ + logger: logger, + db: &stubAccountDB{ + err: merrors.ErrNoData, + }, + } + err := api.ensureLoginAvailable(ctx, " ") + assert.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("db error", func(t *testing.T) { + api := &AccountAPI{ + logger: logger, + db: &stubAccountDB{ + err: errors.New("boom"), + }, + } + err := api.ensureLoginAvailable(ctx, "err@example.com") + assert.EqualError(t, err, "boom") + }) +} diff --git a/frontend/pshared/lib/api/requests/signup.dart b/frontend/pshared/lib/api/requests/signup.dart index 98a817a..6af1cc8 100644 --- a/frontend/pshared/lib/api/requests/signup.dart +++ b/frontend/pshared/lib/api/requests/signup.dart @@ -5,20 +5,20 @@ part 'signup.g.dart'; @JsonSerializable(explicitToJson: true) class SignupRequest { - final String name; - final String login; - final String password; - final String locale; - final String organizationName; + final SignupAccount account; + final DescribableRequest organization; final String organizationTimeZone; + final DescribableRequest anonymousUser; + final DescribableRequest ownerRole; + final DescribableRequest anonymousRole; const SignupRequest({ - required this.name, - required this.login, - required this.password, - required this.locale, - required this.organizationName, + required this.account, + required this.organization, required this.organizationTimeZone, + required this.anonymousUser, + required this.ownerRole, + required this.anonymousRole, }); factory SignupRequest.build({ @@ -28,15 +28,55 @@ class SignupRequest { required String locale, required String organizationName, required String organizationTimeZone, - }) => SignupRequest( - name: name, - login: login, - password: password, - locale: locale, - organizationName: organizationName, - organizationTimeZone: organizationTimeZone, - ); + }) => + SignupRequest( + account: SignupAccount( + name: name, + login: login, + password: password, + locale: locale, + ), + organization: DescribableRequest(name: organizationName), + organizationTimeZone: organizationTimeZone, + anonymousUser: const DescribableRequest(name: 'Anonymous'), + ownerRole: const DescribableRequest(name: 'Owner'), + anonymousRole: const DescribableRequest(name: 'Anonymous'), + ); - factory SignupRequest.fromJson(Map json) => _$SignupRequestFromJson(json); + factory SignupRequest.fromJson(Map json) => + _$SignupRequestFromJson(json); Map toJson() => _$SignupRequestToJson(this); } + +@JsonSerializable() +class SignupAccount { + final String name; + final String login; + final String password; + final String locale; + final String? description; + + const SignupAccount({ + required this.name, + required this.login, + required this.password, + required this.locale, + this.description, + }); + + factory SignupAccount.fromJson(Map json) => + _$SignupAccountFromJson(json); + Map toJson() => _$SignupAccountToJson(this); +} + +@JsonSerializable() +class DescribableRequest { + final String name; + final String? description; + + const DescribableRequest({required this.name, this.description}); + + factory DescribableRequest.fromJson(Map json) => + _$DescribableRequestFromJson(json); + Map toJson() => _$DescribableRequestToJson(this); +} From c6a56071b50351e25c904e01a1fe7c7805e17c5d Mon Sep 17 00:00:00 2001 From: Stephan D Date: Mon, 17 Nov 2025 20:16:45 +0100 Subject: [PATCH 02/32] +signup +login --- .DS_Store | Bin 10244 -> 12292 bytes api/chain/gateway/go.mod | 4 +- api/chain/gateway/go.sum | 4 + api/pkg/model/account.go | 1 + api/pkg/model/userdata.go | 1 + api/server/go.mod | 12 +- api/server/go.sum | 12 + api/server/interface/api/srequest/signup.go | 2 - .../interface/api/srequest/signup_test.go | 22 - ..._availability.go => signupavailability.go} | 0 .../internal/server/accountapiimp/signup.go | 25 - .../accountapiimp/signup_integration_test.go | 12 - .../server/accountapiimp/signup_test.go | 6 - ci/scripts/common/bump_version.sh | 14 +- frontend/.DS_Store | Bin 0 -> 8196 bytes frontend/pshared/lib/.DS_Store | Bin 0 -> 6148 bytes frontend/pshared/lib/api/.DS_Store | Bin 0 -> 6148 bytes .../lib/api/errors/authorization_failed.dart | 22 +- frontend/pshared/lib/api/requests/.DS_Store | Bin 0 -> 6148 bytes frontend/pshared/lib/api/requests/login.dart | 8 +- .../pshared/lib/api/requests/login_data.dart | 76 + .../change.dart} | 2 +- .../lib/api/requests/password/forgot.dart | 20 + .../lib/api/requests/password/reset.dart | 20 + frontend/pshared/lib/api/requests/signup.dart | 79 +- .../pshared/lib/data/dto/account/account.dart | 6 +- .../pshared/lib/data/dto/account/base.dart | 7 +- frontend/pshared/lib/data/dto/date_time.dart | 12 + .../pshared/lib/data/dto/describable.dart | 18 + .../pshared/lib/data/dto/organization.dart | 14 +- .../lib/data/dto/organization/bound.dart | 23 + .../data/dto/organization/description.dart | 4 + .../lib/data/dto/permissions/bound.dart | 26 + .../dto/permissions/description/policy.dart | 8 +- .../dto/permissions/description/role.dart | 8 +- frontend/pshared/lib/data/dto/reference.dart | 16 + frontend/pshared/lib/data/dto/storable.dart | 6 + .../lib/data/dto/storable/describable.dart | 26 + .../lib/data/mapper/account/account.dart | 6 +- .../pshared/lib/data/mapper/account/base.dart | 10 +- .../pshared/lib/data/mapper/describable.dart | 17 + .../pshared/lib/data/mapper/organization.dart | 14 + .../lib/data/mapper/organization/bound.dart | 18 + .../data/mapper/organization/description.dart | 3 + .../permissions/descriptions/policy.dart | 4 + .../mapper/permissions/descriptions/role.dart | 4 + .../pshared/lib/data/mapper/reference.dart | 11 + .../pshared/lib/data/mapper/storable.dart | 2 +- frontend/pshared/lib/models/.DS_Store | Bin 0 -> 6148 bytes .../pshared/lib/models/account/account.dart | 37 +- frontend/pshared/lib/models/account/base.dart | 30 +- frontend/pshared/lib/models/describable.dart | 39 + .../lib/models/organization/bound.dart | 18 + .../lib/models/organization/description.dart | 5 + .../lib/models/organization/organization.dart | 25 +- .../pshared/lib/models/permission_bound.dart | 22 - .../pshared/lib/models/permissions/bound.dart | 32 + .../models/permissions/bound/describable.dart | 6 + .../bound/storable.dart} | 2 +- .../permissions/descriptions/policy.dart | 10 +- .../models/permissions/descriptions/role.dart | 12 +- frontend/pshared/lib/models/reference.dart | 22 + frontend/pshared/lib/models/storable.dart | 5 +- .../pshared/lib/models/storable/.DS_Store | Bin 0 -> 6148 bytes .../lib/models/storable/describable.dart | 6 + frontend/pshared/lib/provider/account.dart | 90 +- .../pshared/lib/provider/organizations.dart | 2 +- .../pshared/lib/provider/pfe/provider.dart | 51 - frontend/pshared/lib/provider/template.dart | 4 +- frontend/pshared/lib/service/account.dart | 21 +- .../authorization/circuit_breaker.dart | 92 + .../service/authorization/retry_helper.dart | 87 + .../lib/service/authorization/service.dart | 125 +- .../lib/service/authorization/storage.dart | 28 + .../lib/service/authorization/token.dart | 106 +- .../service/authorization/token_mutex.dart | 103 ++ .../pshared/lib/service/secure_storage.dart | 6 + frontend/pshared/lib/service/services.dart | 14 - .../lib/{provider => utils}/exception.dart | 0 frontend/pweb/lib/.gitignore | 1 + .../lib/generated/i18n/app_localizations.dart | 1562 ----------------- .../generated/i18n/app_localizations_en.dart | 779 -------- .../generated/i18n/app_localizations_ru.dart | 782 --------- frontend/pweb/lib/l10n/en.arb | 4 +- frontend/pweb/lib/l10n/ru.arb | 5 +- frontend/pweb/lib/main.dart | 3 +- frontend/pweb/lib/pages/login/form.dart | 17 +- .../pweb/lib/pages/signup/form/state.dart | 45 +- version | 2 +- 89 files changed, 1308 insertions(+), 3497 deletions(-) rename api/server/interface/api/sresponse/{signup_availability.go => signupavailability.go} (100%) create mode 100644 frontend/.DS_Store create mode 100644 frontend/pshared/lib/.DS_Store create mode 100644 frontend/pshared/lib/api/.DS_Store create mode 100644 frontend/pshared/lib/api/requests/.DS_Store create mode 100644 frontend/pshared/lib/api/requests/login_data.dart rename frontend/pshared/lib/api/requests/{change_password.dart => password/change.dart} (93%) create mode 100644 frontend/pshared/lib/api/requests/password/forgot.dart create mode 100644 frontend/pshared/lib/api/requests/password/reset.dart create mode 100644 frontend/pshared/lib/data/dto/date_time.dart create mode 100644 frontend/pshared/lib/data/dto/describable.dart create mode 100644 frontend/pshared/lib/data/dto/organization/bound.dart create mode 100644 frontend/pshared/lib/data/dto/permissions/bound.dart create mode 100644 frontend/pshared/lib/data/dto/reference.dart create mode 100644 frontend/pshared/lib/data/dto/storable/describable.dart create mode 100644 frontend/pshared/lib/data/mapper/describable.dart create mode 100644 frontend/pshared/lib/data/mapper/organization/bound.dart create mode 100644 frontend/pshared/lib/data/mapper/reference.dart create mode 100644 frontend/pshared/lib/models/.DS_Store create mode 100644 frontend/pshared/lib/models/describable.dart create mode 100644 frontend/pshared/lib/models/organization/bound.dart delete mode 100644 frontend/pshared/lib/models/permission_bound.dart create mode 100644 frontend/pshared/lib/models/permissions/bound.dart create mode 100644 frontend/pshared/lib/models/permissions/bound/describable.dart rename frontend/pshared/lib/models/{permission_bound_storable.dart => permissions/bound/storable.dart} (69%) create mode 100644 frontend/pshared/lib/models/reference.dart create mode 100644 frontend/pshared/lib/models/storable/.DS_Store create mode 100644 frontend/pshared/lib/models/storable/describable.dart delete mode 100644 frontend/pshared/lib/provider/pfe/provider.dart create mode 100644 frontend/pshared/lib/service/authorization/circuit_breaker.dart create mode 100644 frontend/pshared/lib/service/authorization/retry_helper.dart create mode 100644 frontend/pshared/lib/service/authorization/token_mutex.dart rename frontend/pshared/lib/{provider => utils}/exception.dart (100%) create mode 100644 frontend/pweb/lib/.gitignore delete mode 100644 frontend/pweb/lib/generated/i18n/app_localizations.dart delete mode 100644 frontend/pweb/lib/generated/i18n/app_localizations_en.dart delete mode 100644 frontend/pweb/lib/generated/i18n/app_localizations_ru.dart diff --git a/.DS_Store b/.DS_Store index cd92a4c5339fa0b7d0289b3db35fd4f1cabb7201..db2565e8fb2fe6c788a2c2501f92b2c47a8d5f3d 100644 GIT binary patch literal 12292 zcmeHNU2GIp6uxKL(pkFD0a_@~fei}^*g|djDL=;Twg}~~*lp>y1lk8DKoR% z)`HoX_yVFaK4|=fCx3`Ms3HkDw<%3CK!$KA_=~zJb3QCvu)YJlSC=bB=??s z&%NjVoco>KbLI{q1fps6K|+=iLPSiIN!OFOYw$n7>DL@WjnvMy}5+N2b ziAyxh)d{_QhOYr%19y`K1Y8WY-$G0x=_U4f`_+?~frMctu*>DdfT25XP0bI|3=j$m zi>4HdfvF|IY12!k88c^<%`Bfed-m+IITdr~&7Uv#NV5`3@?f8BrA@WbGAui4q_yro zTUTQPN>Vrb;;uI2#`MRu9%*J?*H%s4H^yok9>6|jV+=OjW?R!tHeO3_ucNu~aple) zsnki@mSOC&9NpC|v$IPXYV+pXu$hz8^s?e&sHAjvF#PaaU z4Xr%~jg)B}*MP5q|E~tP%Xv-T<-8{5`O6*g&aUp@ zZGGsQJ>!m~Wu#N4(<9ADDn>GGD6ZCE7}QrrUcSRVEVm7~ zb{32TB$dxoA6zeYWOxh{khXE=L&0)6p0rXa+@cfUyau^Z-bbh1t~hSXfM(JqUEpnx zU|W~duh?44AUe9(?1@H9LRufVHt0#3qP@D98SXW)JK7%spU@Fjc=SKvqZ34Vs(;7|AqZU{xf z9HBy(D=ZSKglb`huvVxS)(abiokEKc7Zl-uUgfmmakk@|4_py7&&@#fE{lJ4Er}(48uOdMjmc2GHVPj zSeP@vF@xj=+Kxt}4T%A?U8&gX9bPP=(MUyJb?Gv!Zj+|)YGw5b5e-xlST$H#TNgkx zlnT6R^;)a~uLL)244^?ug5@@lK)r61hTtA=fa}2bd1CpaSlP z8pL=Mw!lt?@)!pF`=AH*LjqJtfe98ILcAY_Bk(w6;R!_hQ;7Fx;8}PPUV@hq^RL4j z@Ftvsw-NClz*+bZK7mi+Jbdn9|1u)~DqPP)^Nt*jIfdVEh333ayol)dN|?T2Qtpv} zeB6%iY*_BUDC4-g`(Bjcy2hP)9>V#e{oL1puK`~Jz6N{^OcD(g<2t?jlY7K9^LFFA zW$gO%p1;2w!*8BT=HLJMH}aG6MjjE(u$zbH39pR)>)-48+WM+c?YjDZa<|PsE185B z^5Y`GN%}{FC@aE*&r*8-zv=eh|FKSf=4-&$z`dyfC~S%~MR9N5-TH)G(!2H$ru~@c zL;GbpWg?h)*YT{4UBx5MyUAg^2zIA(Sx%W4*16cf_YuHk_#zKA}4KF!?G9bXg4HOY)*{sg!7i5N*pui2JT|xG57UcNOJegm|bFx1V2L}_x UX&RFgbY^X4Qgva(XKpzo0C-L+MgRZ+ diff --git a/api/chain/gateway/go.mod b/api/chain/gateway/go.mod index 0aa5d97..91cf4f5 100644 --- a/api/chain/gateway/go.mod +++ b/api/chain/gateway/go.mod @@ -22,9 +22,9 @@ require ( require ( github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251117160429-c598d23eddcf // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.24.3 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect github.com/casbin/casbin/v2 v2.134.0 // indirect github.com/casbin/govaluate v1.10.0 // indirect diff --git a/api/chain/gateway/go.sum b/api/chain/gateway/go.sum index 5654a7d..9fdd6f2 100644 --- a/api/chain/gateway/go.sum +++ b/api/chain/gateway/go.sum @@ -8,12 +8,16 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f h1:B/TfTw73mVqWKDzJZhU9Qi9wQyYfmiCz9FnmpQsyv5M= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251117160429-c598d23eddcf h1:aZI2VRIP0LAI6Rw934WEAxxL0SNYSVt9vR9h/cP5Pbo= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251117160429-c598d23eddcf/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y= github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= diff --git a/api/pkg/model/account.go b/api/pkg/model/account.go index 09cae4f..271f843 100755 --- a/api/pkg/model/account.go +++ b/api/pkg/model/account.go @@ -13,6 +13,7 @@ type AccountBase struct { storable.Base `bson:",inline" json:",inline"` ArchivableBase `bson:",inline" json:",inline"` Describable `bson:",inline" json:",inline"` + LastName string `bson:"lastName" json:"lastName"` AvatarURL *string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"` } diff --git a/api/pkg/model/userdata.go b/api/pkg/model/userdata.go index d06095e..761d5d0 100644 --- a/api/pkg/model/userdata.go +++ b/api/pkg/model/userdata.go @@ -13,6 +13,7 @@ type LoginData struct { type AccountData struct { LoginData `bson:",inline" json:",inline"` Describable `bson:",inline" json:",inline"` + LastName string `bson:"lastName" json:"lastName"` } func (ad *AccountData) ToAccount() *Account { diff --git a/api/server/go.mod b/api/server/go.mod index fd249c5..b1fcb5a 100644 --- a/api/server/go.mod +++ b/api/server/go.mod @@ -8,9 +8,9 @@ replace github.com/tech/sendico/chain/gateway => ../chain/gateway require ( github.com/aws/aws-sdk-go-v2 v1.39.6 - github.com/aws/aws-sdk-go-v2/config v1.31.18 - github.com/aws/aws-sdk-go-v2/credentials v1.18.22 - github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 + github.com/aws/aws-sdk-go-v2/config v1.31.20 + github.com/aws/aws-sdk-go-v2/credentials v1.18.24 + github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2 github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/cors v1.2.2 github.com/go-chi/jwtauth/v5 v5.3.3 @@ -49,9 +49,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 // indirect github.com/aws/smithy-go v1.23.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect diff --git a/api/server/go.sum b/api/server/go.sum index 8752498..5377dae 100644 --- a/api/server/go.sum +++ b/api/server/go.sum @@ -12,8 +12,12 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y= github.com/aws/aws-sdk-go-v2/config v1.31.18 h1:RouG3AcF2fLFhw+Z0qbnuIl9HZ0Kh4E/U9sKwTMRpMI= github.com/aws/aws-sdk-go-v2/config v1.31.18/go.mod h1:aXZ13mSQC8S2VEHwGfL1COMuJ1Zty6pX5xU7hyqjvCg= +github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc= +github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0= github.com/aws/aws-sdk-go-v2/credentials v1.18.22 h1:hyIVGBHhQPaNP9D4BaVRwpjLMCwMMdAkHqB3gGMiykU= github.com/aws/aws-sdk-go-v2/credentials v1.18.22/go.mod h1:B9E2qHs3/YGfeQZ4jrIE/nPvqxtyafZrJ5EQiZBG6pk= +github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg= +github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= @@ -34,12 +38,20 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1a github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8= github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8= github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2 h1:DhdbtDl4FdNlj31+xiRXANxEE+eC7n8JQz+/ilwQ8Uc= +github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw= github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 h1:ZGDJVmlpPFiNFCb/I42nYVKUanJAdFUiSmUo/32AqPQ= github.com/aws/aws-sdk-go-v2/service/sts v1.40.0/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= +github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0= +github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= diff --git a/api/server/interface/api/srequest/signup.go b/api/server/interface/api/srequest/signup.go index baac565..208666d 100644 --- a/api/server/interface/api/srequest/signup.go +++ b/api/server/interface/api/srequest/signup.go @@ -6,7 +6,5 @@ type Signup struct { Account model.AccountData `json:"account"` Organization model.Describable `json:"organization"` OrganizationTimeZone string `json:"organizationTimeZone"` - AnonymousUser model.Describable `json:"anonymousUser"` OwnerRole model.Describable `json:"ownerRole"` - AnonymousRole model.Describable `json:"anonymousRole"` } diff --git a/api/server/interface/api/srequest/signup_test.go b/api/server/interface/api/srequest/signup_test.go index 71107b7..f24d66a 100644 --- a/api/server/interface/api/srequest/signup_test.go +++ b/api/server/interface/api/srequest/signup_test.go @@ -28,15 +28,9 @@ func TestSignupRequest_JSONSerialization(t *testing.T) { Name: "Test Organization", }, OrganizationTimeZone: "UTC", - AnonymousUser: model.Describable{ - Name: "Anonymous User", - }, OwnerRole: model.Describable{ Name: "Owner", }, - AnonymousRole: model.Describable{ - Name: "Anonymous", - }, } // Test JSON marshaling @@ -55,9 +49,7 @@ func TestSignupRequest_JSONSerialization(t *testing.T) { assert.Equal(t, signup.Account.Password, unmarshaled.Account.Password) assert.Equal(t, signup.Organization.Name, unmarshaled.Organization.Name) assert.Equal(t, signup.OrganizationTimeZone, unmarshaled.OrganizationTimeZone) - assert.Equal(t, signup.AnonymousUser.Name, unmarshaled.AnonymousUser.Name) assert.Equal(t, signup.OwnerRole.Name, unmarshaled.OwnerRole.Name) - assert.Equal(t, signup.AnonymousRole.Name, unmarshaled.AnonymousRole.Name) } func TestSignupRequest_MinimalValidRequest(t *testing.T) { @@ -77,15 +69,9 @@ func TestSignupRequest_MinimalValidRequest(t *testing.T) { Name: "Test Organization", }, OrganizationTimeZone: "UTC", - AnonymousUser: model.Describable{ - Name: "Anonymous", - }, OwnerRole: model.Describable{ Name: "Owner", }, - AnonymousRole: model.Describable{ - Name: "Anonymous", - }, } // Test JSON marshaling @@ -141,15 +127,9 @@ func TestSignupRequest_UnicodeCharacters(t *testing.T) { Name: "测试 Organization", }, OrganizationTimeZone: "UTC", - AnonymousUser: model.Describable{ - Name: "匿名 User", - }, OwnerRole: model.Describable{ Name: "所有者", }, - AnonymousRole: model.Describable{ - Name: "匿名", - }, } // Test JSON marshaling @@ -166,7 +146,5 @@ func TestSignupRequest_UnicodeCharacters(t *testing.T) { assert.Equal(t, "测试@example.com", unmarshaled.Account.Login) assert.Equal(t, "Test 用户 Üser", unmarshaled.Account.Name) assert.Equal(t, "测试 Organization", unmarshaled.Organization.Name) - assert.Equal(t, "匿名 User", unmarshaled.AnonymousUser.Name) assert.Equal(t, "所有者", unmarshaled.OwnerRole.Name) - assert.Equal(t, "匿名", unmarshaled.AnonymousRole.Name) } diff --git a/api/server/interface/api/sresponse/signup_availability.go b/api/server/interface/api/sresponse/signupavailability.go similarity index 100% rename from api/server/interface/api/sresponse/signup_availability.go rename to api/server/interface/api/sresponse/signupavailability.go diff --git a/api/server/internal/server/accountapiimp/signup.go b/api/server/internal/server/accountapiimp/signup.go index c57df70..2f1474b 100644 --- a/api/server/internal/server/accountapiimp/signup.go +++ b/api/server/internal/server/accountapiimp/signup.go @@ -22,27 +22,6 @@ import ( "go.uber.org/zap" ) -func (a *AccountAPI) createAnonymousAccount(ctx context.Context, org *model.Organization, sr *srequest.Signup) error { - anonymousUser := &model.Account{ - AccountPublic: model.AccountPublic{ - AccountBase: model.AccountBase{ - Describable: sr.AnonymousUser, - }, - UserDataBase: sr.Account.UserDataBase, - }, - } - r, err := a.pmanager.Role().Create(ctx, org.ID, &sr.AnonymousRole) - if err != nil { - a.logger.Warn("Failed to create anonymous role", zap.Error(err)) - return err - } - if err := a.accService.CreateAccount(ctx, org, anonymousUser, r.ID); err != nil { - a.logger.Warn("Failed to create account", zap.Error(err), zap.String("login", anonymousUser.Login)) - return err - } - return nil -} - func (a *AccountAPI) createOrg(ctx context.Context, sr *srequest.Signup, permissionRef primitive.ObjectID) (*model.Organization, error) { name := strings.TrimSpace(sr.Organization.Name) if name == "" { @@ -175,10 +154,6 @@ func (a *AccountAPI) signupTransactionBody(ctx context.Context, sr *srequest.Sig return nil, err } - if err := a.createAnonymousAccount(ctx, org, sr); err != nil { - return nil, err - } - return nil, nil } diff --git a/api/server/internal/server/accountapiimp/signup_integration_test.go b/api/server/internal/server/accountapiimp/signup_integration_test.go index 3337eb8..664162d 100644 --- a/api/server/internal/server/accountapiimp/signup_integration_test.go +++ b/api/server/internal/server/accountapiimp/signup_integration_test.go @@ -73,15 +73,9 @@ func TestSignupRequestSerialization(t *testing.T) { Name: "Test Organization", }, OrganizationTimeZone: "UTC", - AnonymousUser: model.Describable{ - Name: "Anonymous User", - }, OwnerRole: model.Describable{ Name: "Owner", }, - AnonymousRole: model.Describable{ - Name: "Anonymous", - }, } // Store in MongoDB @@ -121,15 +115,9 @@ func TestSignupHTTPSerialization(t *testing.T) { Name: "Test Organization", }, OrganizationTimeZone: "UTC", - AnonymousUser: model.Describable{ - Name: "Anonymous User", - }, OwnerRole: model.Describable{ Name: "Owner", }, - AnonymousRole: model.Describable{ - Name: "Anonymous", - }, } t.Run("ValidJSONRequest", func(t *testing.T) { diff --git a/api/server/internal/server/accountapiimp/signup_test.go b/api/server/internal/server/accountapiimp/signup_test.go index 87687ce..9a366c6 100644 --- a/api/server/internal/server/accountapiimp/signup_test.go +++ b/api/server/internal/server/accountapiimp/signup_test.go @@ -74,15 +74,9 @@ func TestCreateValidSignupRequest(t *testing.T) { Name: "Test Organization", }, OrganizationTimeZone: "UTC", - AnonymousUser: model.Describable{ - Name: "Anonymous User", - }, OwnerRole: model.Describable{ Name: "Owner", }, - AnonymousRole: model.Describable{ - Name: "Anonymous", - }, } // Validate the request structure diff --git a/ci/scripts/common/bump_version.sh b/ci/scripts/common/bump_version.sh index 720d641..50b1656 100755 --- a/ci/scripts/common/bump_version.sh +++ b/ci/scripts/common/bump_version.sh @@ -21,9 +21,21 @@ fi CURRENT_VERSION="$(cat "${VERSION_FILE}")" NEXT_VERSION="$(printf '%s' "${CURRENT_VERSION}" | awk -F. -v OFS=. ' + function pad(value, width, result, i) { + result=value "" + if (length(result) >= width) { + return result + } + i = width - length(result) + while (i-- > 0) { + result = "0" result + } + return result + } NF==1 { print ++$NF; next } { - $NF=sprintf("%0*d", length($NF), ($NF+1)) + last = $NF + 1 + $NF = pad(last, length($NF)) print }')" diff --git a/frontend/.DS_Store b/frontend/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5f303ac654ba7a4dd59af0b734ea6d0542dae5f4 GIT binary patch literal 8196 zcmeHMTWl0n7(U;$&>1?=0a{tG1Dh^Hz!qu&yWEoPO}Q7@mTt>s+1(lGz;veU%x;UN zW@F+Dh{h+4x5$&X$b*U!Uo=q^eN;5T7#}bi^+gkWQF-v6IkV6UBt96UI47C&pZ|8w z{QrFanLV?NF|-%7dl-u_#zeY2s#H^VljiBYYg!WmNhMK`Kg*6d!&%eL5`TW0btnik z5N06EK$w9r17QaK4H=+!w%5d4y!W{_?86L%8Temjz}p|9ba^xp&=RNq)$$+Vzqc6IcSe0-Hcms z+B5lm9@k^rX0|-eWcM2SNLs1wuq`)ZSbE-fFf^S^_V*c z$kN?JU32gFxFpuaR&U&sK57nG_DLQBf^C4!o)XfjiSd|D zv4ek)s7RLM`Kla`G^<*kt@fq*2lmNnWnPhPynEO+Y|GiJ=N+PHrN*HEx8`hXcV74O zeL34K3|UTEnVZX)xq_K-^)}N?8V~6tt8sF9+cbCEj^RzypK|q4*V9c?r3M^3?{@f; zr3{*%TPX!c-=ou{Mp5sish!#>!9hAZXTg%C%bQwSHzm5Z?tI` z@t~pS`!h~Xw=}~#+&f}u?qFZhaEz>}_gI-BU7CwV=~7K(;i5b1Rh1jua44Im3R&Y= z!Elc&F;Vo_h^h_pevR&Fr>%LbXrgkC=AqS>sSWBrPt%dtsn-IB73$q`k3+$WN`@P* zlAGn8qXYp-akylSyiV;kGg;k~6pgPFmp7<=MV^02+0KQVe39?_LY>4T@9+*?@iaH zX?_fgA`5HOq}nSgpNVNjF|49Nu&2k$(J#izk0zE_$%nD`b0vS|CpwmB=3coXOFHr~TUe1cE$Ij-Pae24Gx zGk(V(xF*aH76=hxq0k^S3QfW)p;d?r8-$I*E}>gU2^rytU<;!>0HIuJ2RL8uh2u23 zzI9Nk1)u(evo|lb`un$R-M0O|Q)-vz_{CLQcgKS0vX!gX#y7WJ2O&po8K@KC?_+*a zrTCclF);!ab!@3f#iGmz*ySsT-LA~!Y<<%z zkqT20d{(Y+StC*TDHQ?R)JiNkBX8U!Q6Vao0lPRsEc2QA`pzzz=At70T`0fIuCO22 zFYFqjd=BO#g1gX6*lx!b?DEl_L@y2^jYG&n!w@XkI7T=>fpI*H5*{IxKSnrz0#D)@ zJd5WD>o4JDyn@&8IwAcXoX5NP03YH4J`OPdH6i{dT%CgAon`!TieD$9xR}garfnT2 zSsC?{&2KBNf=3ha&i`Gv{{DYk#T*6`W+2SKwg&1I7+6TPa>ctPDw)TAO8^G{~-ShKefa6fB63Q5qf)ee*y^?Y7YPa literal 0 HcmV?d00001 diff --git a/frontend/pshared/lib/.DS_Store b/frontend/pshared/lib/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..14a15a24afedb9e3f39906eb9ebcaa9ad3a319ba GIT binary patch literal 6148 zcmeHKISv9b4733uBpOP}e1RWC2wt!spa9WuNFb=U;$1wA@zKCS2Mroa&LoZ}QKne0 zMMUS9^-N?UA|tq=+-&HY?VES3mk|ZRamG&0*XDHC9S_@1_WOWwhq9NWtY!1Uw>=sa zpaN8Y3Qz$m@LdJ6zK+JLgt08k_#pjW{wb2$RG{I>0rS` zf6+wWE@22Eyub+i@9z&{9iZFaS@kCJ|Ba((NN6>z;c;Cx zk6P2ZnzWjYx;ofDnocWnduR9bqWcs-C;H8jDUf#~n+^+jM`fWWGFQqBFatkifc6K4jnH$LTQpk-c65Cvf0>X3b$Uw>DuHI&(UuNMWe>R0j%m6d+&lnKpj^AlxQSNM=E04}vh3y_23B~24prAf;3BUpEBQ@i+ cenmRQc@A@nGz-~HIwD^L3?bYx1HZt)7k*Doa{vGU literal 0 HcmV?d00001 diff --git a/frontend/pshared/lib/api/errors/authorization_failed.dart b/frontend/pshared/lib/api/errors/authorization_failed.dart index 7acc439..841ab8d 100644 --- a/frontend/pshared/lib/api/errors/authorization_failed.dart +++ b/frontend/pshared/lib/api/errors/authorization_failed.dart @@ -1,2 +1,22 @@ -class AuthorizationFailed implements Exception { +class AuthenticationFailedException implements Exception { + final String message; + final Exception? originalError; + + const AuthenticationFailedException(this.message, [this.originalError]); + + @override + String toString() { + return 'AuthenticationFailedException: $message${originalError != null ? ' (caused by: $originalError)' : ''}'; + } +} + +class CircuitBreakerOpenException implements Exception { + final String message; + + const CircuitBreakerOpenException(this.message); + + @override + String toString() { + return 'CircuitBreakerOpenException: $message'; + } } \ No newline at end of file diff --git a/frontend/pshared/lib/api/requests/.DS_Store b/frontend/pshared/lib/api/requests/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 LoginData._( + login: login.trim().toLowerCase(), + password: password, + locale: locale, + ); + + factory LoginData.fromJson(Map json) => _$LoginDataFromJson(json); + Map toJson() => _$LoginDataToJson(this); +} + +@JsonSerializable(explicitToJson: true, constructor: 'buildIstance') +class AccountData extends LoginData { + final String name; + final String lastName; + + const AccountData._({ + required super.login, + required super.password, + required super.locale, + required this.name, + required this.lastName, + }) : super._(); + + factory AccountData.buildIstance({ + required String login, + required String password, + required String locale, + required String name, + required String lastName, + }) => AccountData._( + login: login, + password: password, + locale: locale, + name: name.trim(), + lastName: lastName.trim(), + ); + + factory AccountData.build({ + required LoginData login, + required String name, + required String lastName, + }) => AccountData.buildIstance( + login: login.login, + password: login.password, + locale: login.locale, + name: name, + lastName: lastName, + ); + + + factory AccountData.fromJson(Map json) => _$AccountDataFromJson(json); + + @override + Map toJson() => _$AccountDataToJson(this); +} diff --git a/frontend/pshared/lib/api/requests/change_password.dart b/frontend/pshared/lib/api/requests/password/change.dart similarity index 93% rename from frontend/pshared/lib/api/requests/change_password.dart rename to frontend/pshared/lib/api/requests/password/change.dart index 5bdb21f..f4da065 100644 --- a/frontend/pshared/lib/api/requests/change_password.dart +++ b/frontend/pshared/lib/api/requests/password/change.dart @@ -1,6 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; -part 'change_password.g.dart'; +part 'change.g.dart'; @JsonSerializable(explicitToJson: true) diff --git a/frontend/pshared/lib/api/requests/password/forgot.dart b/frontend/pshared/lib/api/requests/password/forgot.dart new file mode 100644 index 0000000..53eb53e --- /dev/null +++ b/frontend/pshared/lib/api/requests/password/forgot.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'forgot.g.dart'; + + +@JsonSerializable() +class ForgotPasswordRequest { + final String login; + + const ForgotPasswordRequest({ + required this.login, + }); + + factory ForgotPasswordRequest.fromJson(Map json) => _$ForgotPasswordRequestFromJson(json); + Map toJson() => _$ForgotPasswordRequestToJson(this); + + static ForgotPasswordRequest build({ + required String login, + }) => ForgotPasswordRequest(login: login); +} diff --git a/frontend/pshared/lib/api/requests/password/reset.dart b/frontend/pshared/lib/api/requests/password/reset.dart new file mode 100644 index 0000000..701a3ba --- /dev/null +++ b/frontend/pshared/lib/api/requests/password/reset.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'reset.g.dart'; + + +@JsonSerializable() +class ResetPasswordRequest { + final String password; + + const ResetPasswordRequest({ + required this.password, + }); + + factory ResetPasswordRequest.fromJson(Map json) => _$ResetPasswordRequestFromJson(json); + Map toJson() => _$ResetPasswordRequestToJson(this); + + static ResetPasswordRequest build({ + required String password, + }) => ResetPasswordRequest(password: password); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/requests/signup.dart b/frontend/pshared/lib/api/requests/signup.dart index 6af1cc8..64d5c2a 100644 --- a/frontend/pshared/lib/api/requests/signup.dart +++ b/frontend/pshared/lib/api/requests/signup.dart @@ -1,82 +1,39 @@ import 'package:json_annotation/json_annotation.dart'; +import 'package:pshared/api/requests/login_data.dart'; +import 'package:pshared/data/dto/describable.dart'; +import 'package:pshared/data/mapper/describable.dart'; +import 'package:pshared/models/describable.dart'; + part 'signup.g.dart'; @JsonSerializable(explicitToJson: true) class SignupRequest { - final SignupAccount account; - final DescribableRequest organization; + final AccountData account; + final DescribableDTO organization; final String organizationTimeZone; - final DescribableRequest anonymousUser; - final DescribableRequest ownerRole; - final DescribableRequest anonymousRole; + final DescribableDTO ownerRole; const SignupRequest({ required this.account, required this.organization, required this.organizationTimeZone, - required this.anonymousUser, required this.ownerRole, - required this.anonymousRole, }); factory SignupRequest.build({ - required String name, - required String login, - required String password, - required String locale, - required String organizationName, + required AccountData account, + required Describable organization, required String organizationTimeZone, - }) => - SignupRequest( - account: SignupAccount( - name: name, - login: login, - password: password, - locale: locale, - ), - organization: DescribableRequest(name: organizationName), - organizationTimeZone: organizationTimeZone, - anonymousUser: const DescribableRequest(name: 'Anonymous'), - ownerRole: const DescribableRequest(name: 'Owner'), - anonymousRole: const DescribableRequest(name: 'Anonymous'), - ); + required Describable ownerRole, + }) => SignupRequest( + account: account, + organization: organization.toDTO(), + organizationTimeZone: organizationTimeZone, + ownerRole: ownerRole.toDTO(), + ); - factory SignupRequest.fromJson(Map json) => - _$SignupRequestFromJson(json); + factory SignupRequest.fromJson(Map json) => _$SignupRequestFromJson(json); Map toJson() => _$SignupRequestToJson(this); } - -@JsonSerializable() -class SignupAccount { - final String name; - final String login; - final String password; - final String locale; - final String? description; - - const SignupAccount({ - required this.name, - required this.login, - required this.password, - required this.locale, - this.description, - }); - - factory SignupAccount.fromJson(Map json) => - _$SignupAccountFromJson(json); - Map toJson() => _$SignupAccountToJson(this); -} - -@JsonSerializable() -class DescribableRequest { - final String name; - final String? description; - - const DescribableRequest({required this.name, this.description}); - - factory DescribableRequest.fromJson(Map json) => - _$DescribableRequestFromJson(json); - Map toJson() => _$DescribableRequestToJson(this); -} diff --git a/frontend/pshared/lib/data/dto/account/account.dart b/frontend/pshared/lib/data/dto/account/account.dart index b1a080b..f89de61 100644 --- a/frontend/pshared/lib/data/dto/account/account.dart +++ b/frontend/pshared/lib/data/dto/account/account.dart @@ -1,6 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:pshared/data/dto/account/base.dart'; +import 'package:pshared/data/dto/date_time.dart'; part 'account.g.dart'; @@ -8,14 +9,17 @@ part 'account.g.dart'; @JsonSerializable() class AccountDTO extends AccountBaseDTO { final String login; + final String locale; const AccountDTO({ required super.id, required super.createdAt, required super.updatedAt, required super.name, + required super.lastName, + required super.description, required super.avatarUrl, - required super.locale, + required this.locale, required this.login, }); diff --git a/frontend/pshared/lib/data/dto/account/base.dart b/frontend/pshared/lib/data/dto/account/base.dart index 0ae451d..453b70e 100644 --- a/frontend/pshared/lib/data/dto/account/base.dart +++ b/frontend/pshared/lib/data/dto/account/base.dart @@ -1,6 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:pshared/data/dto/storable.dart'; +import 'package:pshared/data/dto/date_time.dart'; part 'base.g.dart'; @@ -8,7 +9,8 @@ part 'base.g.dart'; @JsonSerializable() class AccountBaseDTO extends StorableDTO { final String name; - final String locale; + final String lastName; + final String? description; final String? avatarUrl; const AccountBaseDTO({ @@ -16,8 +18,9 @@ class AccountBaseDTO extends StorableDTO { required super.createdAt, required super.updatedAt, required this.name, + required this.description, required this.avatarUrl, - required this.locale, + required this.lastName, }); factory AccountBaseDTO.fromJson(Map json) => _$AccountBaseDTOFromJson(json); diff --git a/frontend/pshared/lib/data/dto/date_time.dart b/frontend/pshared/lib/data/dto/date_time.dart new file mode 100644 index 0000000..f92e62f --- /dev/null +++ b/frontend/pshared/lib/data/dto/date_time.dart @@ -0,0 +1,12 @@ +import 'package:json_annotation/json_annotation.dart'; + + +class UtcIso8601Converter implements JsonConverter { + const UtcIso8601Converter(); + + @override + DateTime fromJson(String json) => DateTime.parse(json).toUtc(); + + @override + String toJson(DateTime value) => value.toUtc().toIso8601String(); +} diff --git a/frontend/pshared/lib/data/dto/describable.dart b/frontend/pshared/lib/data/dto/describable.dart new file mode 100644 index 0000000..39e912d --- /dev/null +++ b/frontend/pshared/lib/data/dto/describable.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'describable.g.dart'; + + +@JsonSerializable() +class DescribableDTO { + final String name; + final String? description; + + const DescribableDTO({ + required this.name, + this.description, + }); + + factory DescribableDTO.fromJson(Map json) => _$DescribableDTOFromJson(json); + Map toJson() => _$DescribableDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/organization.dart b/frontend/pshared/lib/data/dto/organization.dart index 9e5cc76..d482a0c 100644 --- a/frontend/pshared/lib/data/dto/organization.dart +++ b/frontend/pshared/lib/data/dto/organization.dart @@ -1,18 +1,28 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:pshared/data/dto/storable.dart'; + +import 'package:pshared/data/dto/date_time.dart'; +import 'package:pshared/data/dto/permissions/bound.dart'; part 'organization.g.dart'; @JsonSerializable() -class OrganizationDTO extends StorableDTO { +class OrganizationDTO extends PermissionBoundDTO { + final String name; + final String? description; final String timeZone; final String? logoUrl; + final String tenantRef; const OrganizationDTO({ required super.id, required super.createdAt, required super.updatedAt, + required super.permissionRef, + required super.organizationRef, + required this.name, + required this.tenantRef, + this.description, required this.timeZone, this.logoUrl, }); diff --git a/frontend/pshared/lib/data/dto/organization/bound.dart b/frontend/pshared/lib/data/dto/organization/bound.dart new file mode 100644 index 0000000..8c5bd07 --- /dev/null +++ b/frontend/pshared/lib/data/dto/organization/bound.dart @@ -0,0 +1,23 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/storable.dart'; +import 'package:pshared/data/dto/date_time.dart'; + +part 'bound.g.dart'; + + +@JsonSerializable() +class OrganizationBoundDTO extends StorableDTO { + final String organizationRef; + + const OrganizationBoundDTO({ + required super.id, + required super.createdAt, + required super.updatedAt, + required this.organizationRef, + }); + + factory OrganizationBoundDTO.fromJson(Map json) => _$OrganizationBoundDTOFromJson(json); + @override + Map toJson() => _$OrganizationBoundDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/organization/description.dart b/frontend/pshared/lib/data/dto/organization/description.dart index 2814f8b..efe2f62 100644 --- a/frontend/pshared/lib/data/dto/organization/description.dart +++ b/frontend/pshared/lib/data/dto/organization/description.dart @@ -1,13 +1,17 @@ import 'package:json_annotation/json_annotation.dart'; +import 'package:pshared/data/dto/describable.dart'; + part 'description.g.dart'; @JsonSerializable() class OrganizationDescriptionDTO { + final DescribableDTO description; final String? logoUrl; const OrganizationDescriptionDTO({ + required this.description, this.logoUrl, }); diff --git a/frontend/pshared/lib/data/dto/permissions/bound.dart b/frontend/pshared/lib/data/dto/permissions/bound.dart new file mode 100644 index 0000000..02eb0d8 --- /dev/null +++ b/frontend/pshared/lib/data/dto/permissions/bound.dart @@ -0,0 +1,26 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/date_time.dart'; +import 'package:pshared/data/dto/storable.dart'; + +part 'bound.g.dart'; + + +@JsonSerializable() +class PermissionBoundDTO extends StorableDTO { + final String permissionRef; + final String organizationRef; + + const PermissionBoundDTO({ + required super.id, + required super.createdAt, + required super.updatedAt, + required this.permissionRef, + required this.organizationRef, + }); + + factory PermissionBoundDTO.fromJson(Map json) => _$PermissionBoundDTOFromJson(json); + + @override + Map toJson() => _$PermissionBoundDTOToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/dto/permissions/description/policy.dart b/frontend/pshared/lib/data/dto/permissions/description/policy.dart index 04f4e43..1629e6c 100644 --- a/frontend/pshared/lib/data/dto/permissions/description/policy.dart +++ b/frontend/pshared/lib/data/dto/permissions/description/policy.dart @@ -1,12 +1,14 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:pshared/data/dto/storable.dart'; + +import 'package:pshared/data/dto/storable/describable.dart'; +import 'package:pshared/data/dto/date_time.dart'; import 'package:pshared/models/resources.dart'; part 'policy.g.dart'; @JsonSerializable() -class PolicyDescriptionDTO extends StorableDTO { +class PolicyDescriptionDTO extends StorableDescribabaleDTO { final List? resourceTypes; final String? organizationRef; @@ -14,6 +16,8 @@ class PolicyDescriptionDTO extends StorableDTO { required super.id, required super.createdAt, required super.updatedAt, + required super.name, + required super.description, required this.resourceTypes, required this.organizationRef, }); diff --git a/frontend/pshared/lib/data/dto/permissions/description/role.dart b/frontend/pshared/lib/data/dto/permissions/description/role.dart index 8b92caf..d40e0d3 100644 --- a/frontend/pshared/lib/data/dto/permissions/description/role.dart +++ b/frontend/pshared/lib/data/dto/permissions/description/role.dart @@ -1,17 +1,21 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:pshared/data/dto/storable.dart'; + +import 'package:pshared/data/dto/date_time.dart'; +import 'package:pshared/data/dto/storable/describable.dart'; part 'role.g.dart'; @JsonSerializable() -class RoleDescriptionDTO extends StorableDTO { +class RoleDescriptionDTO extends StorableDescribabaleDTO { final String organizationRef; const RoleDescriptionDTO({ required super.id, required super.createdAt, required super.updatedAt, + required super.name, + required super.description, required this.organizationRef, }); diff --git a/frontend/pshared/lib/data/dto/reference.dart b/frontend/pshared/lib/data/dto/reference.dart new file mode 100644 index 0000000..31e7bf8 --- /dev/null +++ b/frontend/pshared/lib/data/dto/reference.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'reference.g.dart'; + + +@JsonSerializable() +class ReferenceDTO { + final String ref; + + const ReferenceDTO({ + required this.ref, + }); + + factory ReferenceDTO.fromJson(Map json) => _$ReferenceDTOFromJson(json); + Map toJson() => _$ReferenceDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/storable.dart b/frontend/pshared/lib/data/dto/storable.dart index b189dff..d9ea76e 100644 --- a/frontend/pshared/lib/data/dto/storable.dart +++ b/frontend/pshared/lib/data/dto/storable.dart @@ -1,12 +1,18 @@ import 'package:json_annotation/json_annotation.dart'; +import 'package:pshared/data/dto/date_time.dart'; + part 'storable.g.dart'; @JsonSerializable() class StorableDTO { final String id; + + @UtcIso8601Converter() final DateTime createdAt; + + @UtcIso8601Converter() final DateTime updatedAt; const StorableDTO({ diff --git a/frontend/pshared/lib/data/dto/storable/describable.dart b/frontend/pshared/lib/data/dto/storable/describable.dart new file mode 100644 index 0000000..42af849 --- /dev/null +++ b/frontend/pshared/lib/data/dto/storable/describable.dart @@ -0,0 +1,26 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/date_time.dart'; +import 'package:pshared/data/dto/storable.dart'; + +part 'describable.g.dart'; + + +@JsonSerializable() +class StorableDescribabaleDTO extends StorableDTO { + final String name; + final String? description; + + const StorableDescribabaleDTO({ + required super.id, + required super.createdAt, + required super.updatedAt, + required this.name, + this.description, + }); + + factory StorableDescribabaleDTO.fromJson(Map json) => _$StorableDescribabaleDTOFromJson(json); + + @override + Map toJson() => _$StorableDescribabaleDTOToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/mapper/account/account.dart b/frontend/pshared/lib/data/mapper/account/account.dart index fd13dca..6ab6523 100644 --- a/frontend/pshared/lib/data/mapper/account/account.dart +++ b/frontend/pshared/lib/data/mapper/account/account.dart @@ -1,5 +1,6 @@ import 'package:pshared/data/dto/account/account.dart'; import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/describable.dart'; import 'package:pshared/models/storable.dart'; @@ -9,6 +10,8 @@ extension AccountMapper on Account { createdAt: createdAt, updatedAt: updatedAt, name: name, + lastName: lastName, + description: description, avatarUrl: avatarUrl, locale: locale, login: login, @@ -19,8 +22,9 @@ extension AccountDTOMapper on AccountDTO { Account toDomain() => Account( storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), avatarUrl: avatarUrl, + describable: newDescribable(name: name, description: description), + lastName: lastName, locale: locale, login: login, - name: name, ); } diff --git a/frontend/pshared/lib/data/mapper/account/base.dart b/frontend/pshared/lib/data/mapper/account/base.dart index f0241ef..430bed7 100644 --- a/frontend/pshared/lib/data/mapper/account/base.dart +++ b/frontend/pshared/lib/data/mapper/account/base.dart @@ -1,5 +1,6 @@ import 'package:pshared/data/dto/account/base.dart'; import 'package:pshared/models/account/base.dart'; +import 'package:pshared/models/describable.dart'; import 'package:pshared/models/storable.dart'; @@ -8,17 +9,18 @@ extension AccountBaseMapper on AccountBase { id: storable.id, createdAt: storable.createdAt, updatedAt: storable.updatedAt, + name: describable.name, + description: describable.description, + lastName: lastName, avatarUrl: avatarUrl, - name: name, - locale: locale, ); } extension AccountDTOMapper on AccountBaseDTO { AccountBase toDomain() => AccountBase( storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), + describable: newDescribable(name: name, description: description), + lastName: lastName, avatarUrl: avatarUrl, - name: name, - locale: locale, ); } diff --git a/frontend/pshared/lib/data/mapper/describable.dart b/frontend/pshared/lib/data/mapper/describable.dart new file mode 100644 index 0000000..d9ab58e --- /dev/null +++ b/frontend/pshared/lib/data/mapper/describable.dart @@ -0,0 +1,17 @@ +import 'package:pshared/data/dto/describable.dart'; +import 'package:pshared/models/describable.dart'; + + +extension DescribableMapper on Describable { + DescribableDTO toDTO() => DescribableDTO( + name: name, + description: description, + ); +} + +extension DescribableDTOMapper on DescribableDTO { + Describable toDomain() => newDescribable( + name: name, + description: description, + ); +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/mapper/organization.dart b/frontend/pshared/lib/data/mapper/organization.dart index 41970d5..71c2c70 100644 --- a/frontend/pshared/lib/data/mapper/organization.dart +++ b/frontend/pshared/lib/data/mapper/organization.dart @@ -1,5 +1,8 @@ import 'package:pshared/data/dto/organization.dart'; +import 'package:pshared/models/describable.dart'; +import 'package:pshared/models/organization/bound.dart'; import 'package:pshared/models/organization/organization.dart'; +import 'package:pshared/models/permissions/bound.dart'; import 'package:pshared/models/storable.dart'; @@ -8,15 +11,26 @@ extension OrganizationMapper on Organization { id: storable.id, createdAt: storable.createdAt, updatedAt: storable.updatedAt, + name: describable.name, + description: describable.description, timeZone: timeZone, logoUrl: logoUrl, + organizationRef: permissionBound.organizationRef, + permissionRef: permissionBound.permissionRef, + tenantRef: tenantRef, ); } extension OrganizationDTOMapper on OrganizationDTO { Organization toDomain() => Organization( storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), + describable: newDescribable(name: name, description: description), timeZone: timeZone, logoUrl: logoUrl, + permissionBound: newPermissionBound( + organizationBound: newOrganizationBound(organizationRef: organizationRef), + permissionRef: permissionRef, + ), + tenantRef: tenantRef, ); } diff --git a/frontend/pshared/lib/data/mapper/organization/bound.dart b/frontend/pshared/lib/data/mapper/organization/bound.dart new file mode 100644 index 0000000..6b81615 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/organization/bound.dart @@ -0,0 +1,18 @@ +import 'package:pshared/data/dto/organization/bound.dart'; +import 'package:pshared/models/organization/bound.dart'; + + +extension OrganizationBoundMapper on OrganizationBound { + OrganizationBoundDTO toDTO() => OrganizationBoundDTO( + id: '', // OrganizationBound doesn't have storable fields, so we need to provide defaults + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + organizationRef: organizationRef, + ); +} + +extension OrganizationBoundDTOMapper on OrganizationBoundDTO { + OrganizationBound toDomain() => newOrganizationBound( + organizationRef: organizationRef, + ); +} diff --git a/frontend/pshared/lib/data/mapper/organization/description.dart b/frontend/pshared/lib/data/mapper/organization/description.dart index d80483f..7e9c753 100644 --- a/frontend/pshared/lib/data/mapper/organization/description.dart +++ b/frontend/pshared/lib/data/mapper/organization/description.dart @@ -1,9 +1,11 @@ import 'package:pshared/data/dto/organization/description.dart'; +import 'package:pshared/data/mapper/describable.dart'; import 'package:pshared/models/organization/description.dart'; extension OrganizationDescriptionMapper on OrganizationDescription { OrganizationDescriptionDTO toDTO() => OrganizationDescriptionDTO( + description: description.toDTO(), logoUrl: logoUrl, ); } @@ -11,5 +13,6 @@ extension OrganizationDescriptionMapper on OrganizationDescription { extension AccountDescriptionDTOMapper on OrganizationDescriptionDTO { OrganizationDescription toDomain() => OrganizationDescription( logoUrl: logoUrl, + description: description.toDomain(), ); } diff --git a/frontend/pshared/lib/data/mapper/permissions/descriptions/policy.dart b/frontend/pshared/lib/data/mapper/permissions/descriptions/policy.dart index e444a9d..92268ba 100644 --- a/frontend/pshared/lib/data/mapper/permissions/descriptions/policy.dart +++ b/frontend/pshared/lib/data/mapper/permissions/descriptions/policy.dart @@ -1,4 +1,5 @@ import 'package:pshared/data/dto/permissions/description/policy.dart'; +import 'package:pshared/models/describable.dart'; import 'package:pshared/models/permissions/descriptions/policy.dart'; import 'package:pshared/models/storable.dart'; @@ -8,6 +9,8 @@ extension PolicyDescriptionMapper on PolicyDescription { id: storable.id, createdAt: storable.createdAt, updatedAt: storable.updatedAt, + name: describable.name, + description: describable.description, resourceTypes: resourceTypes, organizationRef: organizationRef, ); @@ -16,6 +19,7 @@ extension PolicyDescriptionMapper on PolicyDescription { extension PolicyDescriptionDTOMapper on PolicyDescriptionDTO { PolicyDescription toDomain() => PolicyDescription( storable: newStorable(id: id, createdAt: createdAt, updatedAt: createdAt), + describable: newDescribable(name: name, description: description), resourceTypes: resourceTypes, organizationRef: organizationRef, ); diff --git a/frontend/pshared/lib/data/mapper/permissions/descriptions/role.dart b/frontend/pshared/lib/data/mapper/permissions/descriptions/role.dart index d0596cb..b3b8e2a 100644 --- a/frontend/pshared/lib/data/mapper/permissions/descriptions/role.dart +++ b/frontend/pshared/lib/data/mapper/permissions/descriptions/role.dart @@ -1,4 +1,5 @@ import 'package:pshared/data/dto/permissions/description/role.dart'; +import 'package:pshared/models/describable.dart'; import 'package:pshared/models/permissions/descriptions/role.dart'; import 'package:pshared/models/storable.dart'; @@ -8,6 +9,8 @@ extension RoleDescriptionMapper on RoleDescription { id: storable.id, createdAt: storable.createdAt, updatedAt: storable.updatedAt, + name: describable.name, + description: describable.description, organizationRef: organizationRef, ); } @@ -15,6 +18,7 @@ extension RoleDescriptionMapper on RoleDescription { extension RoleDescriptionDTOMapper on RoleDescriptionDTO { RoleDescription toDomain() => RoleDescription( storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), + describable: newDescribable(name: name, description: description), organizationRef: organizationRef, ); } diff --git a/frontend/pshared/lib/data/mapper/reference.dart b/frontend/pshared/lib/data/mapper/reference.dart new file mode 100644 index 0000000..b6fbe5f --- /dev/null +++ b/frontend/pshared/lib/data/mapper/reference.dart @@ -0,0 +1,11 @@ +import 'package:pshared/data/dto/reference.dart'; +import 'package:pshared/models/reference.dart'; + + +extension ReferenceMapper on Reference { + ReferenceDTO toDTO() => ReferenceDTO(ref: ref); +} + +extension ReferenceDTOMapper on ReferenceDTO { + Reference toDomain() => newReference(ref: ref); +} diff --git a/frontend/pshared/lib/data/mapper/storable.dart b/frontend/pshared/lib/data/mapper/storable.dart index 8990947..1696d92 100644 --- a/frontend/pshared/lib/data/mapper/storable.dart +++ b/frontend/pshared/lib/data/mapper/storable.dart @@ -3,7 +3,7 @@ import 'package:pshared/models/storable.dart'; extension StorableMapper on Storable { - StorableDTO toDTO() => StorableDTO(id: id, createdAt: createdAt, updatedAt: updatedAt); + StorableDTO toDTO() => StorableDTO(id: id, createdAt: createdAt.toUtc(), updatedAt: updatedAt.toUtc()); } extension StorableDTOMapper on StorableDTO { diff --git a/frontend/pshared/lib/models/.DS_Store b/frontend/pshared/lib/models/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f469e241e9e56b9f0610ccae423aacdebe1221c4 GIT binary patch literal 6148 zcmeHK%}T>S5T32oZYe?!3Oz1(Em(gj6)&OI7cim+m70*E!I&*gY7eE5v%Zi|;`2DO zy8(+mcoMNQu=~x<&u->}><<8l{xmuOXaa!5Mkq*GA!J_Zs<>c6Vde-Rgak(6EE}(w z=r5Y++a+X>gN6xw`Tj7`A^P6KaS~^Fr}H5y8`aINnykr&yz?LR%+G^-n)QRp4UI0f zPQp?T!mD^R8#Z^&beacoI+~b-I2vKd?RA_+de+y|G|Eh_ZymBO>%(SiKJWFqZFP9u zS+v#s Account( - storable: accountBase.storable, - avatarUrl: accountBase.avatarUrl, - locale: accountBase.locale, - name: accountBase.name, - login: login, - ); - @override Account copyWith({ + Describable? describable, + String? lastName, String? Function()? avatarUrl, - String? name, String? locale, - }) { - final updatedBase = super.copyWith( - avatarUrl: avatarUrl, - name: name, - locale: locale, - ); - return Account.fromBase(updatedBase, login); - } + }) => Account( + storable: storable, + describable: describableCopyWithOther(this.describable, describable), + lastName: lastName ?? this.lastName, + avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl, + login: login, + locale: locale ?? this.locale, + ); } \ No newline at end of file diff --git a/frontend/pshared/lib/models/account/base.dart b/frontend/pshared/lib/models/account/base.dart index 5fdd112..fd5722e 100644 --- a/frontend/pshared/lib/models/account/base.dart +++ b/frontend/pshared/lib/models/account/base.dart @@ -1,8 +1,16 @@ +import 'package:flutter/foundation.dart'; + +import 'package:pshared/models/describable.dart'; import 'package:pshared/models/storable.dart'; +import 'package:pshared/models/storable/describable.dart'; +import 'package:pshared/utils/name_initials.dart'; -class AccountBase implements Storable { +@immutable +class AccountBase implements StorableDescribable { final Storable storable; + final Describable describable; + final String lastName; @override String get id => storable.id; @@ -10,26 +18,30 @@ class AccountBase implements Storable { DateTime get createdAt => storable.createdAt; @override DateTime get updatedAt => storable.updatedAt; + @override + String get name => describable.name; + @override + String? get description => describable.description; final String? avatarUrl; - final String name; - final String locale; const AccountBase({ required this.storable, - required this.name, - required this.locale, + required this.describable, required this.avatarUrl, + required this.lastName, }); + String get nameInitials => getNameInitials(describable.name); + AccountBase copyWith({ + Describable? describable, + String? lastName, String? Function()? avatarUrl, - String? name, - String? locale, }) => AccountBase( storable: storable, avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl, - locale: locale ?? this.locale, - name: name ?? this.name, + describable: describable ?? this.describable, + lastName: lastName ?? this.lastName, ); } diff --git a/frontend/pshared/lib/models/describable.dart b/frontend/pshared/lib/models/describable.dart new file mode 100644 index 0000000..22a7f52 --- /dev/null +++ b/frontend/pshared/lib/models/describable.dart @@ -0,0 +1,39 @@ +import 'package:flutter/foundation.dart'; + + +abstract class Describable { + String get name; + String? get description; +} + +@immutable +class _DescribableImp implements Describable { + @override + final String name; + @override + final String? description; + + const _DescribableImp({ + required this.name, + required this.description, + }); +} + +Describable newDescribable({required String name, String? description}) => + _DescribableImp(name: name, description: description); + + +extension DescribableCopier on Describable { + Describable copyWith({ + String? name, + String? Function()? description, + }) => newDescribable( + name: name ?? this.name, + description: description != null ? description() : this.description, + ); +} + +Describable describableCopyWithOther(Describable current, Describable? other) => current.copyWith( + name: other?.name, + description: () => other?.description, +); \ No newline at end of file diff --git a/frontend/pshared/lib/models/organization/bound.dart b/frontend/pshared/lib/models/organization/bound.dart new file mode 100644 index 0000000..acce7fb --- /dev/null +++ b/frontend/pshared/lib/models/organization/bound.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; + + +abstract class OrganizationBound { + String get organizationRef; +} + +@immutable +class _OrganizationBoundImp implements OrganizationBound { + @override + final String organizationRef; + + const _OrganizationBoundImp({ + required this.organizationRef, + }); +} + +OrganizationBound newOrganizationBound({ required String organizationRef }) => _OrganizationBoundImp(organizationRef: organizationRef); \ No newline at end of file diff --git a/frontend/pshared/lib/models/organization/description.dart b/frontend/pshared/lib/models/organization/description.dart index 7e6f5f9..81d199f 100644 --- a/frontend/pshared/lib/models/organization/description.dart +++ b/frontend/pshared/lib/models/organization/description.dart @@ -1,7 +1,12 @@ +import 'package:pshared/models/describable.dart'; + + class OrganizationDescription { + final Describable description; final String? logoUrl; const OrganizationDescription({ + required this.description, this.logoUrl, }); } \ No newline at end of file diff --git a/frontend/pshared/lib/models/organization/organization.dart b/frontend/pshared/lib/models/organization/organization.dart index b04182a..1150e8b 100644 --- a/frontend/pshared/lib/models/organization/organization.dart +++ b/frontend/pshared/lib/models/organization/organization.dart @@ -1,8 +1,13 @@ +import 'package:pshared/models/describable.dart'; +import 'package:pshared/models/permissions/bound.dart'; +import 'package:pshared/models/permissions/bound/describable.dart'; import 'package:pshared/models/storable.dart'; -class Organization implements Storable { +class Organization implements PermissionBoundStorableDescribable { final Storable storable; + final PermissionBound permissionBound; + final Describable describable; @override String get id => storable.id; @@ -10,25 +15,39 @@ class Organization implements Storable { DateTime get createdAt => storable.createdAt; @override DateTime get updatedAt => storable.updatedAt; + @override + String get organizationRef => permissionBound.organizationRef; + @override + String get permissionRef => permissionBound.permissionRef; + @override + String get name => describable.name; + @override + String? get description => describable.description; final String timeZone; final String? logoUrl; + final String tenantRef; const Organization({ required this.storable, + required this.describable, required this.timeZone, + required this.permissionBound, + required this.tenantRef, this.logoUrl, }); Organization copyWith({ - String? name, - String? Function()? description, + Describable? describable, String? timeZone, String? Function()? logoUrl, }) => Organization( storable: storable, // Same Storable, same id + describable: describableCopyWithOther(this.describable, describable), timeZone: timeZone ?? this.timeZone, logoUrl: logoUrl != null ? logoUrl() : this.logoUrl, + permissionBound: permissionBound, + tenantRef: tenantRef, ); } diff --git a/frontend/pshared/lib/models/permission_bound.dart b/frontend/pshared/lib/models/permission_bound.dart deleted file mode 100644 index 6618f26..0000000 --- a/frontend/pshared/lib/models/permission_bound.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:pshared/config/constants.dart'; - - -abstract class PermissionBound { - String get permissionRef; - String get organizationRef; -} - -class _PermissionBoundImp implements PermissionBound { - @override - final String permissionRef; - @override - final String organizationRef; - - const _PermissionBoundImp({ - required this.permissionRef, - required this.organizationRef, - }); -} - -PermissionBound newPermissionBound({ required String organizationRef, String? permissionRef}) => - _PermissionBoundImp(permissionRef: permissionRef ?? Constants.nilObjectRef, organizationRef: organizationRef); \ No newline at end of file diff --git a/frontend/pshared/lib/models/permissions/bound.dart b/frontend/pshared/lib/models/permissions/bound.dart new file mode 100644 index 0000000..476b2a4 --- /dev/null +++ b/frontend/pshared/lib/models/permissions/bound.dart @@ -0,0 +1,32 @@ +import 'package:flutter/foundation.dart'; + +import 'package:pshared/config/constants.dart'; +import 'package:pshared/models/organization/bound.dart'; + + +abstract class PermissionBound extends OrganizationBound { + String get permissionRef; +} + +@immutable +class _PermissionBoundImp implements PermissionBound { + @override + final String permissionRef; + final OrganizationBound organizationBound; + + @override + get organizationRef => organizationBound.organizationRef; + + const _PermissionBoundImp({ + required this.permissionRef, + required this.organizationBound, + }); +} + +PermissionBound newPermissionBound({ + required OrganizationBound organizationBound, + String? permissionRef, +}) => _PermissionBoundImp( + permissionRef: permissionRef ?? Constants.nilObjectRef, + organizationBound: organizationBound, +); \ No newline at end of file diff --git a/frontend/pshared/lib/models/permissions/bound/describable.dart b/frontend/pshared/lib/models/permissions/bound/describable.dart new file mode 100644 index 0000000..3b141ba --- /dev/null +++ b/frontend/pshared/lib/models/permissions/bound/describable.dart @@ -0,0 +1,6 @@ +import 'package:pshared/models/permissions/bound/storable.dart'; +import 'package:pshared/models/storable/describable.dart'; + + +abstract class PermissionBoundStorableDescribable implements PermissionBoundStorable, StorableDescribable { +} diff --git a/frontend/pshared/lib/models/permission_bound_storable.dart b/frontend/pshared/lib/models/permissions/bound/storable.dart similarity index 69% rename from frontend/pshared/lib/models/permission_bound_storable.dart rename to frontend/pshared/lib/models/permissions/bound/storable.dart index 4ee0d63..47d783c 100644 --- a/frontend/pshared/lib/models/permission_bound_storable.dart +++ b/frontend/pshared/lib/models/permissions/bound/storable.dart @@ -1,4 +1,4 @@ -import 'package:pshared/models/permission_bound.dart'; +import 'package:pshared/models/permissions/bound.dart'; import 'package:pshared/models/storable.dart'; diff --git a/frontend/pshared/lib/models/permissions/descriptions/policy.dart b/frontend/pshared/lib/models/permissions/descriptions/policy.dart index 6aecef0..ac746ce 100644 --- a/frontend/pshared/lib/models/permissions/descriptions/policy.dart +++ b/frontend/pshared/lib/models/permissions/descriptions/policy.dart @@ -1,9 +1,12 @@ +import 'package:pshared/models/describable.dart'; import 'package:pshared/models/resources.dart'; import 'package:pshared/models/storable.dart'; +import 'package:pshared/models/storable/describable.dart'; -class PolicyDescription implements Storable { +class PolicyDescription implements StorableDescribable { final Storable storable; + final Describable describable; final List? resourceTypes; final String? organizationRef; @@ -13,9 +16,14 @@ class PolicyDescription implements Storable { DateTime get createdAt => storable.createdAt; @override DateTime get updatedAt => storable.updatedAt; + @override + String get name => describable.name; + @override + String? get description => describable.description; const PolicyDescription({ required this.storable, + required this.describable, required this.resourceTypes, required this.organizationRef, }); diff --git a/frontend/pshared/lib/models/permissions/descriptions/role.dart b/frontend/pshared/lib/models/permissions/descriptions/role.dart index ecde4fb..1e04a27 100644 --- a/frontend/pshared/lib/models/permissions/descriptions/role.dart +++ b/frontend/pshared/lib/models/permissions/descriptions/role.dart @@ -1,8 +1,11 @@ +import 'package:pshared/models/describable.dart'; import 'package:pshared/models/storable.dart'; +import 'package:pshared/models/storable/describable.dart'; -class RoleDescription implements Storable { +class RoleDescription implements StorableDescribable { final Storable storable; + final Describable describable; @override String get id => storable.id; @@ -10,18 +13,25 @@ class RoleDescription implements Storable { DateTime get createdAt => storable.createdAt; @override DateTime get updatedAt => storable.updatedAt; + @override + String get name => describable.name; + @override + String? get description => describable.description; final String organizationRef; const RoleDescription({ required this.storable, + required this.describable, required this.organizationRef, }); factory RoleDescription.build({ + required Describable roleDescription, required String organizationRef, }) => RoleDescription( storable: newStorable(), + describable: roleDescription, organizationRef: organizationRef ); } diff --git a/frontend/pshared/lib/models/reference.dart b/frontend/pshared/lib/models/reference.dart new file mode 100644 index 0000000..3bcc422 --- /dev/null +++ b/frontend/pshared/lib/models/reference.dart @@ -0,0 +1,22 @@ +abstract class Reference { + String get ref; +} + +class _ReferenceImp implements Reference { + @override + final String ref; + + const _ReferenceImp({ + required this.ref, + }); +} + +Reference newReference({required String ref}) => _ReferenceImp(ref: ref); + +extension ReferenceCopier on Reference { + Reference copyWith({ + String? ref, + }) => newReference( + ref: ref ?? this.ref, + ); +} diff --git a/frontend/pshared/lib/models/storable.dart b/frontend/pshared/lib/models/storable.dart index ce95482..dfd59e3 100644 --- a/frontend/pshared/lib/models/storable.dart +++ b/frontend/pshared/lib/models/storable.dart @@ -1,4 +1,6 @@ -import 'package:pshared/config/constants.dart'; +import 'package:flutter/foundation.dart'; + +import 'package:pshared/config/web.dart'; abstract class Storable { @@ -7,6 +9,7 @@ abstract class Storable { DateTime get updatedAt; } +@immutable class _StorableImp implements Storable { @override final String id; diff --git a/frontend/pshared/lib/models/storable/.DS_Store b/frontend/pshared/lib/models/storable/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Constants.nilObjectRef; + // The resource now wraps our Account? state along with its loading/error state. Resource _resource = Resource(data: null); Resource get resource => _resource; + late LocaleProvider _localeProvider; Account? get account => _resource.data; bool get isLoggedIn => account != null; bool get isLoading => _resource.isLoading; Object? get error => _resource.error; + bool get isReady => (!isLoading) && (account != null); + + Account? currentUser() { + final acc = account; + if (acc == null) return null; + return Account( + storable: newStorable( + id: currentUserRef, + createdAt: acc.createdAt, + updatedAt: acc.updatedAt, + ), + describable: acc.describable, + lastName: acc.lastName, + avatarUrl: acc.avatarUrl, + login: acc.login, + locale: acc.locale, + ); + } // Private helper to update the resource and notify listeners. void _setResource(Resource newResource) { @@ -26,16 +53,24 @@ class AccountProvider extends ChangeNotifier { notifyListeners(); } + void updateProvider(LocaleProvider localeProvider) => _localeProvider = localeProvider; - Future login({ + void _pickupLocale(String locale) => _localeProvider.setLocale(Locale(locale)); + + Future login({ required String email, required String password, required String locale, }) async { _setResource(_resource.copyWith(isLoading: true, error: null)); try { - final acc = await AccountService.login(email, password, locale); + final acc = await AccountService.login(LoginData.build( + login: email, + password: password, + locale: locale, + )); _setResource(Resource(data: acc, isLoading: false)); + _pickupLocale(acc.locale); return acc; } catch (e) { _setResource(_resource.copyWith(isLoading: false, error: toException(e))); @@ -43,11 +78,14 @@ class AccountProvider extends ChangeNotifier { } } + Future isAuthorizationStored() async => AuthorizationService.isAuthorizationStored(); + Future restore() async { _setResource(_resource.copyWith(isLoading: true, error: null)); try { final acc = await AccountService.restore(); _setResource(Resource(data: acc, isLoading: false)); + _pickupLocale(acc.locale); return acc; } catch (e) { _setResource(_resource.copyWith(isLoading: false, error: toException(e))); @@ -55,24 +93,20 @@ class AccountProvider extends ChangeNotifier { } } - Future signup( - String name, - String login, - String password, - String locale, - String organizationName, - String timezone, - ) async { + Future signup({ + required AccountData account, + required Describable organization, + required String timezone, + required Describable ownerRole, + }) async { _setResource(_resource.copyWith(isLoading: true, error: null)); try { await AccountService.signup( SignupRequest.build( - name: name, - login: login.trim().toLowerCase(), - password: password, - locale: locale, - organizationName: organizationName, + account: account, + organization: organization, organizationTimeZone: timezone, + ownerRole: ownerRole, ), ); // Signup might not automatically log in the user, @@ -96,6 +130,7 @@ class AccountProvider extends ChangeNotifier { } Future update({ + Describable? describable, String? locale, String? avatarUrl, String? notificationFrequency, @@ -105,6 +140,7 @@ class AccountProvider extends ChangeNotifier { try { final updated = await AccountService.update( account!.copyWith( + describable: describable, avatarUrl: () => avatarUrl ?? account!.avatarUrl, locale: locale ?? account!.locale, ), @@ -141,4 +177,26 @@ class AccountProvider extends ChangeNotifier { rethrow; } } + + Future forgotPassword(String email) async { + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + await AccountService.forgotPassword(email); + _setResource(_resource.copyWith(isLoading: false)); + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + Future resetPassword(String accountId, String token, String newPassword) async { + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + await AccountService.resetPassword(accountId, token, newPassword); + _setResource(_resource.copyWith(isLoading: false)); + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } } diff --git a/frontend/pshared/lib/provider/organizations.dart b/frontend/pshared/lib/provider/organizations.dart index c338885..2a06728 100644 --- a/frontend/pshared/lib/provider/organizations.dart +++ b/frontend/pshared/lib/provider/organizations.dart @@ -5,9 +5,9 @@ import 'package:collection/collection.dart'; import 'package:pshared/config/constants.dart'; import 'package:pshared/models/organization/organization.dart'; import 'package:pshared/provider/resource.dart'; -import 'package:pshared/provider/exception.dart'; import 'package:pshared/service/organization.dart'; import 'package:pshared/service/secure_storage.dart'; +import 'package:pshared/utils/exception.dart'; class OrganizationsProvider extends ChangeNotifier { diff --git a/frontend/pshared/lib/provider/pfe/provider.dart b/frontend/pshared/lib/provider/pfe/provider.dart deleted file mode 100644 index 6fff15b..0000000 --- a/frontend/pshared/lib/provider/pfe/provider.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/service/pfe/service.dart'; -import 'package:pshared/provider/exception.dart'; -import 'package:pshared/provider/resource.dart'; -import 'package:pshared/service/account.dart'; - - -class PfeProvider extends ChangeNotifier { - // The resource now wraps our Account? state along with its loading/error state. - Resource _resource = Resource(data: null); - Resource get resource => _resource; - - String? get session => _resource.data; - bool get isLoggedIn => session != null; - bool get isLoading => _resource.isLoading; - Object? get error => _resource.error; - - // Private helper to update the resource and notify listeners. - void _setResource(Resource newResource) { - _resource = newResource; - notifyListeners(); - } - - - Future login({ - required String email, - required String password, - }) async { - _setResource(_resource.copyWith(isLoading: true, error: null)); - try { - final acc = await PfeService.login(email, password); - _setResource(Resource(data: acc, isLoading: false)); - return acc; - } catch (e) { - _setResource(_resource.copyWith(isLoading: false, error: toException(e))); - rethrow; - } - } - - Future logout() async { - _setResource(_resource.copyWith(isLoading: true, error: null)); - try { - await AccountService.logout(); - _setResource(Resource(data: null, isLoading: false)); - } catch (e) { - _setResource(_resource.copyWith(isLoading: false, error: toException(e))); - rethrow; - } - } -} diff --git a/frontend/pshared/lib/provider/template.dart b/frontend/pshared/lib/provider/template.dart index 0a8d819..c26e89c 100644 --- a/frontend/pshared/lib/provider/template.dart +++ b/frontend/pshared/lib/provider/template.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; -import 'package:pshared/models/permission_bound_storable.dart'; -import 'package:pshared/provider/exception.dart'; +import 'package:pshared/models/permissions/bound/storable.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/template.dart'; +import 'package:pshared/utils/exception.dart'; List mergeLists({ diff --git a/frontend/pshared/lib/service/account.dart b/frontend/pshared/lib/service/account.dart index a51ccff..a935dc2 100644 --- a/frontend/pshared/lib/service/account.dart +++ b/frontend/pshared/lib/service/account.dart @@ -4,7 +4,10 @@ import 'package:share_plus/share_plus.dart'; import 'package:pshared/api/requests/signup.dart'; import 'package:pshared/api/responses/account.dart'; -import 'package:pshared/api/requests/change_password.dart'; +import 'package:pshared/api/requests/login_data.dart'; +import 'package:pshared/api/requests/password/change.dart'; +import 'package:pshared/api/requests/password/forgot.dart'; +import 'package:pshared/api/requests/password/reset.dart'; import 'package:pshared/data/mapper/account/account.dart'; import 'package:pshared/models/account/account.dart'; import 'package:pshared/service/authorization/service.dart'; @@ -17,9 +20,9 @@ class AccountService { static final _logger = Logger('service.account'); static const String _objectType = Services.account; - static Future login(String email, String password, String locale) async { + static Future login(LoginData login) async { _logger.fine('Logging in'); - return AuthorizationService.login(_objectType, email, password, locale); + return AuthorizationService.login(_objectType, login); } static Future restore() async { @@ -27,6 +30,7 @@ class AccountService { } static Future signup(SignupRequest request) async { + // Use regular HTTP for public signup endpoint (no auth needed) await getPOSTResponse(_objectType, 'signup', request.toJson()); } @@ -42,9 +46,20 @@ class AccountService { static Future update(Account account) async { _logger.fine('Patching account ${account.id}'); + // Use AuthorizationService for authenticated operations return _getAccount(AuthorizationService.getPUTResponse(_objectType, '', account.toDTO().toJson())); } + static Future forgotPassword(String email) async { + _logger.fine('Requesting password reset for email: $email'); + await getPUTResponse(_objectType, 'password', ForgotPasswordRequest.build(login: email).toJson()); + } + + static Future resetPassword(String accountRef, String token, String newPassword) async { + _logger.fine('Resetting password for account: $accountRef'); + await getPOSTResponse(_objectType, 'password/reset/$accountRef/$token', ResetPasswordRequest.build(password: newPassword).toJson()); + } + static Future changePassword(String oldPassword, String newPassword) async { _logger.fine('Changing password'); return _getAccount(AuthorizationService.getPATCHResponse( diff --git a/frontend/pshared/lib/service/authorization/circuit_breaker.dart b/frontend/pshared/lib/service/authorization/circuit_breaker.dart new file mode 100644 index 0000000..d64d48b --- /dev/null +++ b/frontend/pshared/lib/service/authorization/circuit_breaker.dart @@ -0,0 +1,92 @@ +import 'package:logging/logging.dart'; + +/// Circuit breaker pattern implementation for authentication service failures +class AuthCircuitBreaker { + static final _logger = Logger('service.auth_circuit_breaker'); + + static int _failureCount = 0; + static DateTime? _lastFailure; + static const int _failureThreshold = 3; + static const Duration _recoveryTime = Duration(minutes: 5); + + /// Returns true if the circuit breaker is open (blocking operations) + static bool get isOpen { + if (_failureCount < _failureThreshold) return false; + if (_lastFailure == null) return false; + + final isOpen = DateTime.now().difference(_lastFailure!) < _recoveryTime; + if (isOpen) { + _logger.warning('Circuit breaker is OPEN. Failure count: $_failureCount, last failure: $_lastFailure'); + } + return isOpen; + } + + /// Returns true if the circuit breaker is in half-open state (allowing test requests) + static bool get isHalfOpen { + if (_failureCount < _failureThreshold) return false; + if (_lastFailure == null) return false; + + return DateTime.now().difference(_lastFailure!) >= _recoveryTime; + } + + /// Executes an operation with circuit breaker protection + static Future execute(Future Function() operation) async { + if (isOpen) { + final timeSinceFailure = _lastFailure != null + ? DateTime.now().difference(_lastFailure!) + : Duration.zero; + final timeUntilRecovery = _recoveryTime - timeSinceFailure; + + _logger.warning('Circuit breaker blocking operation. Recovery in: ${timeUntilRecovery.inSeconds}s'); + throw Exception('Auth service temporarily unavailable. Try again in ${timeUntilRecovery.inMinutes} minutes.'); + } + + try { + _logger.fine('Executing operation through circuit breaker'); + final result = await operation(); + _reset(); + return result; + } catch (e) { + _recordFailure(); + rethrow; + } + } + + /// Records a failure and updates the circuit breaker state + static void _recordFailure() { + _failureCount++; + _lastFailure = DateTime.now(); + _logger.warning('Auth circuit breaker recorded failure #$_failureCount at $_lastFailure'); + + if (_failureCount >= _failureThreshold) { + _logger.severe('Auth circuit breaker OPENED after $_failureCount failures'); + } + } + + /// Resets the circuit breaker to closed state + static void _reset() { + if (_failureCount > 0) { + _logger.info('Auth circuit breaker CLOSED - resetting failure count from $_failureCount to 0'); + } + _failureCount = 0; + _lastFailure = null; + } + + /// Manual reset (for testing or administrative purposes) + static void manualReset() { + _logger.info('Auth circuit breaker manually reset'); + _reset(); + } + + /// Get current status for debugging + static Map getStatus() { + return { + 'failureCount': _failureCount, + 'lastFailure': _lastFailure?.toIso8601String(), + 'isOpen': isOpen, + 'isHalfOpen': isHalfOpen, + 'threshold': _failureThreshold, + 'recoveryTime': _recoveryTime.inSeconds, + }; + } +} \ No newline at end of file diff --git a/frontend/pshared/lib/service/authorization/retry_helper.dart b/frontend/pshared/lib/service/authorization/retry_helper.dart new file mode 100644 index 0000000..6dcb71b --- /dev/null +++ b/frontend/pshared/lib/service/authorization/retry_helper.dart @@ -0,0 +1,87 @@ +import 'dart:math'; + +import 'package:logging/logging.dart'; + +import 'package:pshared/utils/exception.dart'; + + +class RetryHelper { + static final _logger = Logger('auth.retry'); + + /// Executes an operation with exponential backoff retry logic + static Future withExponentialBackoff( + Future Function() operation, { + int maxRetries = 3, + Duration initialDelay = const Duration(milliseconds: 500), + double backoffMultiplier = 2.0, + Duration maxDelay = const Duration(seconds: 30), + bool Function(Exception)? shouldRetry, + }) async { + Exception? lastException; + + // Total attempts = initial attempt + maxRetries + final totalAttempts = maxRetries + 1; + + for (int attempt = 1; attempt <= totalAttempts; attempt++) { + try { + _logger.fine('Attempting operation (attempt $attempt/$totalAttempts)'); + return await operation(); + } catch (e) { + lastException = toException(e); + + // Don't retry if we've reached max attempts + if (attempt == totalAttempts) { + _logger.warning('Operation failed after $totalAttempts attempts: $lastException'); + rethrow; + } + + // Check if we should retry this specific error + if (shouldRetry != null && !shouldRetry(lastException)) { + _logger.fine('Operation failed with non-retryable error: $lastException'); + rethrow; + } + + // Calculate delay with exponential backoff + final delayMs = min( + initialDelay.inMilliseconds * pow(backoffMultiplier, attempt - 1).toInt(), + maxDelay.inMilliseconds, + ); + final delay = Duration(milliseconds: delayMs); + + _logger.fine('Operation failed (attempt $attempt), retrying in ${delay.inMilliseconds}ms: $lastException'); + await Future.delayed(delay); + } + } + + // This should never be reached due to rethrow above, but just in case + throw lastException ?? Exception('Retry logic error'); + } + + /// Determines if an error is retryable (network/temporary errors) + static bool isRetryableError(Exception error) { + final errorString = error.toString().toLowerCase(); + + // Network connectivity issues + if (errorString.contains('socket') || + errorString.contains('connection') || + errorString.contains('timeout') || + errorString.contains('network')) { + return true; + } + + // Server temporary errors (5xx) + if (errorString.contains('500') || + errorString.contains('502') || + errorString.contains('503') || + errorString.contains('504')) { + return true; + } + + // Rate limiting + if (errorString.contains('429')) { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/frontend/pshared/lib/service/authorization/service.dart b/frontend/pshared/lib/service/authorization/service.dart index 79ba68a..57e615f 100644 --- a/frontend/pshared/lib/service/authorization/service.dart +++ b/frontend/pshared/lib/service/authorization/service.dart @@ -1,20 +1,38 @@ import 'package:logging/logging.dart'; -import 'package:pshared/api/errors/upload_failed.dart'; +import 'package:pshared/api/errors/authorization_failed.dart'; import 'package:pshared/api/requests/login.dart'; +import 'package:pshared/api/requests/login_data.dart'; import 'package:pshared/api/responses/account.dart'; import 'package:pshared/api/responses/login.dart'; -import 'package:pshared/config/constants.dart'; +import 'package:pshared/config/web.dart'; import 'package:pshared/data/mapper/account/account.dart'; import 'package:pshared/models/account/account.dart'; +import 'package:pshared/service/authorization/circuit_breaker.dart'; +import 'package:pshared/service/authorization/retry_helper.dart'; import 'package:pshared/service/authorization/storage.dart'; import 'package:pshared/service/authorization/token.dart'; import 'package:pshared/service/device_id.dart'; +import 'package:pshared/utils/exception.dart'; import 'package:pshared/utils/http/requests.dart' as httpr; +/// AuthorizationService provides centralized authorization management +/// with token refresh, retry logic, and circuit breaker patterns class AuthorizationService { - static final _logger = Logger('service.authorization'); + static final _logger = Logger('service.authorization.auth_service'); + + static Future login(String service, LoginData login) async { + _logger.fine('Logging in ${login.login} with ${login.locale} locale'); + final deviceId = await DeviceIdManager.getDeviceId(); + final response = await httpr.getPOSTResponse( + service, + '/login', + LoginRequest(login: login, deviceId: deviceId, clientId: Constants.clientId).toJson(), + ); + + return (await _completeLogin(response)).account.toDomain(); + } static Future _updateAccessToken(AccountResponse response) async { await AuthorizationStorage.updateToken(response.accessToken); @@ -31,59 +49,86 @@ class AuthorizationService { return lr; } - static Future login(String service, String email, String password, String locale) async { - _logger.fine('Logging in $email with $locale locale'); - final deviceId = await DeviceIdManager.getDeviceId(); - final response = await httpr.getPOSTResponse( - service, - '/login', - LoginRequest( - login: email.toLowerCase(), - password: password, - locale: locale, - deviceId: deviceId, - clientId: Constants.clientId, - ).toJson()); - - return (await _completeLogin(response)).account.toDomain(); - } - static Future restore() async { - return (await TokenService.rotateRefreshToken()).account.toDomain(); + return (await TokenService.refreshAccessToken()).account.toDomain(); } static Future logout() async { return AuthorizationStorage.removeTokens(); } - static Future> _authenticatedRequest( - String service, - String url, - Future> Function(String, String, Map, {String? authToken}) requestType, - {Map? body}) async { - final accessToken = await TokenService.getAccessToken(); - return requestType(service, url, body ?? {}, authToken: accessToken); - } - - static Future> getPOSTResponse(String service, String url, Map body) async => _authenticatedRequest(service, url, httpr.getPOSTResponse, body: body); - + // Original AuthorizationService methods - keeping the interface unchanged static Future> getGETResponse(String service, String url) async { - final accessToken = await TokenService.getAccessToken(); - return httpr.getGETResponse(service, url, authToken: accessToken); + final token = await TokenService.getAccessTokenSafe(); + return httpr.getGETResponse(service, url, authToken: token); } - static Future> getPUTResponse(String service, String url, Map body) async => _authenticatedRequest(service, url, httpr.getPUTResponse, body: body); + static Future> getPOSTResponse(String service, String url, Map body) async { + final token = await TokenService.getAccessTokenSafe(); + return httpr.getPOSTResponse(service, url, body, authToken: token); + } - static Future> getPATCHResponse(String service, String url, Map body) async => _authenticatedRequest(service, url, httpr.getPATCHResponse, body: body); + static Future> getPUTResponse(String service, String url, Map body) async { + final token = await TokenService.getAccessTokenSafe(); + return httpr.getPUTResponse(service, url, body, authToken: token); + } - static Future> getDELETEResponse(String service, String url, Map body) async => _authenticatedRequest(service, url, httpr.getDELETEResponse, body: body); + static Future> getPATCHResponse(String service, String url, Map body) async { + final token = await TokenService.getAccessTokenSafe(); + return httpr.getPATCHResponse(service, url, body, authToken: token); + } + + static Future> getDELETEResponse(String service, String url, Map body) async { + final token = await TokenService.getAccessTokenSafe(); + return httpr.getDELETEResponse(service, url, body, authToken: token); + } static Future getFileUploadResponseAuth(String service, String url, String fileName, String fileType, String mediaType, List bytes) async { - final accessToken = await TokenService.getAccessToken(); - final res = await httpr.getFileUploadResponse(service, url, fileName, fileType, mediaType, bytes, authToken: accessToken); + final token = await TokenService.getAccessTokenSafe(); + final res = await httpr.getFileUploadResponse(service, url, fileName, fileType, mediaType, bytes, authToken: token); if (res == null) { - throw ErrorUploadFailed(); + throw Exception('Upload failed'); } return res.url; } + + static Future isAuthorizationStored() async => AuthorizationStorage.isAuthorizationStored(); + + /// Execute an operation with automatic token management and retry logic + static Future executeWithAuth( + Future Function() operation, + String description, { + int? maxRetries, + }) async => AuthCircuitBreaker.execute(() async => RetryHelper.withExponentialBackoff( + operation, + maxRetries: maxRetries ?? 3, + initialDelay: Duration(milliseconds: 100), + maxDelay: Duration(seconds: 5), + shouldRetry: (error) => RetryHelper.isRetryableError(error), + )); + + + /// Handle 401 unauthorized errors with automatic token recovery + static Future handleUnauthorized( + Future Function() operation, + String description, + ) async { + _logger.warning('Handling unauthorized error with token recovery: $description'); + + return executeWithAuth( + () async { + try { + // Attempt token recovery first + await TokenService.handleUnauthorized(); + + // Retry the original operation + return await operation(); + } catch (e) { + _logger.severe('Token recovery failed', e); + throw AuthenticationFailedException('Token recovery failed', toException(e)); + } + }, + 'unauthorized recovery: $description', + ); + } } diff --git a/frontend/pshared/lib/service/authorization/storage.dart b/frontend/pshared/lib/service/authorization/storage.dart index bb49431..a72cebd 100644 --- a/frontend/pshared/lib/service/authorization/storage.dart +++ b/frontend/pshared/lib/service/authorization/storage.dart @@ -20,6 +20,34 @@ class AuthorizationStorage { return TokenData.fromJson(jsonDecode(tokenJson)); } + + static Future _checkTokenUsable(String keyName) async { + final hasKey = await SecureStorageService.containsKey(keyName); + if (!hasKey) return false; + + try { + final tokenData = await _getTokenData(keyName); + return tokenData.expiration.isAfter(DateTime.now()); + } catch (e, st) { + _logger.warning('Error reading token from $keyName: $e', e, st); + rethrow; + } + } + + + static Future isAuthorizationStored() async { + _logger.fine('Checking if authorization is stored'); + + final accessUsable = await _checkTokenUsable(Constants.accessTokenStorageKey); + if (accessUsable) return true; + + final refreshUsable = await _checkTokenUsable(Constants.refreshTokenStorageKey); + if (refreshUsable) return true; + + return false; + } + + static Future getAccessToken() async { _logger.fine('Getting access token'); return _getTokenData(Constants.accessTokenStorageKey); diff --git a/frontend/pshared/lib/service/authorization/token.dart b/frontend/pshared/lib/service/authorization/token.dart index 0357e98..06ebb49 100644 --- a/frontend/pshared/lib/service/authorization/token.dart +++ b/frontend/pshared/lib/service/authorization/token.dart @@ -1,20 +1,25 @@ - - import 'package:logging/logging.dart'; +import 'package:pshared/api/errors/authorization_failed.dart'; import 'package:pshared/api/errors/unauthorized.dart'; import 'package:pshared/api/requests/tokens/access_refresh.dart'; import 'package:pshared/api/requests/tokens/refresh_rotate.dart'; import 'package:pshared/api/responses/account.dart'; +import 'package:pshared/api/responses/error/server.dart'; import 'package:pshared/api/responses/login.dart'; import 'package:pshared/api/responses/token.dart'; import 'package:pshared/config/constants.dart'; +import 'package:pshared/service/authorization/circuit_breaker.dart'; +import 'package:pshared/service/authorization/retry_helper.dart'; import 'package:pshared/service/authorization/storage.dart'; +import 'package:pshared/service/authorization/token_mutex.dart'; import 'package:pshared/service/device_id.dart'; import 'package:pshared/service/services.dart'; +import 'package:pshared/utils/exception.dart'; import 'package:pshared/utils/http/requests.dart'; + class TokenService { static final _logger = Logger('service.authorization.token'); static const String _objectType = Services.account; @@ -26,7 +31,11 @@ class TokenService { static Future getAccessToken() async { TokenData token = await AuthorizationStorage.getAccessToken(); if (_isTokenExpiringSoon(token, const Duration(hours: 4))) { - token = (await _refreshAccessToken()).accessToken; + // Use mutex to prevent concurrent refresh operations + final refreshedToken = await TokenRefreshMutex().executeRefresh(() async { + return (await refreshAccessToken()).accessToken.token; + }); + return refreshedToken; } return token.token; } @@ -36,13 +45,13 @@ class TokenService { await AuthorizationStorage.updateRefreshToken(response.refreshToken); } - static Future _refreshAccessToken() async { + static Future refreshAccessToken() async { _logger.fine('Refreshing access token...'); final deviceId = await DeviceIdManager.getDeviceId(); final refresh = await AuthorizationStorage.getRefreshToken(); if (_isTokenExpiringSoon(refresh, const Duration(days: 7))) { - return await rotateRefreshToken(); + return await _rotateRefreshToken(); } final response = await getPOSTResponse( @@ -60,7 +69,7 @@ class TokenService { return accountResp; } - static Future rotateRefreshToken() async { + static Future _rotateRefreshToken() async { _logger.fine('Rotating refresh token...'); final refresh = await AuthorizationStorage.getRefreshToken(); @@ -82,4 +91,89 @@ class TokenService { return loginResponse; } + /// Enhanced method to handle unexpected 401 errors with fallback logic + static Future handleUnauthorized() async { + _logger.warning('Handling unexpected 401 unauthorized error'); + + return AuthCircuitBreaker.execute(() async { + return RetryHelper.withExponentialBackoff( + () async { + try { + // Try refresh first (faster) + final currentRefresh = await AuthorizationStorage.getRefreshToken(); + if (!_isTokenExpiringSoon(currentRefresh, const Duration(days: 1))) { + _logger.fine('Attempting access token refresh for 401 recovery'); + await TokenRefreshMutex().executeRefresh(() async { + await refreshAccessToken(); + return 'refreshed'; + }); + return; + } + + // Fallback to rotation if refresh token expiring soon + _logger.fine('Attempting refresh token rotation for 401 recovery'); + await TokenRefreshMutex().executeRotation(() async { + await _rotateRefreshToken(); + }); + } catch (e) { + _logger.severe('Token recovery failed: $e'); + throw AuthenticationFailedException('Token recovery failed', toException(e)); + } + }, + maxRetries: 2, + shouldRetry: (error) { + // Only retry on network errors, not auth errors + return RetryHelper.isRetryableError(error) && !_isAuthError(error); + }, + ); + }); + } + + /// Enhanced getAccessToken with better error handling + static Future getAccessTokenSafe() async { + return AuthCircuitBreaker.execute(() async { + return RetryHelper.withExponentialBackoff( + () async { + TokenData token = await AuthorizationStorage.getAccessToken(); + if (_isTokenExpiringSoon(token, const Duration(hours: 4))) { + // Use mutex to prevent concurrent refresh operations + final refreshedToken = await TokenRefreshMutex().executeRefresh(() async { + return (await refreshAccessToken()).accessToken.token; + }); + return refreshedToken; + } + return token.token; + }, + maxRetries: 2, + shouldRetry: (error) => RetryHelper.isRetryableError(error), + ); + }); + } + + /// Check if error is authentication-related (non-retryable) + static bool _isAuthError(Exception error) { + if (error is ErrorUnauthorized || error is AuthenticationFailedException) { + return true; + } + + if (error is ErrorResponse && error.code == 401) { + return true; + } + + final errorString = error.toString().toLowerCase(); + return errorString.contains('unauthorized') || + errorString.contains('401') || + errorString.contains('authentication') || + errorString.contains('token'); + } + + /// Get circuit breaker status for debugging + static Map getAuthStatus() { + return { + 'circuitBreaker': AuthCircuitBreaker.getStatus(), + 'tokenMutex': TokenRefreshMutex().getStatus(), + 'timestamp': DateTime.now().toIso8601String(), + }; + } + } diff --git a/frontend/pshared/lib/service/authorization/token_mutex.dart b/frontend/pshared/lib/service/authorization/token_mutex.dart new file mode 100644 index 0000000..35ac296 --- /dev/null +++ b/frontend/pshared/lib/service/authorization/token_mutex.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:logging/logging.dart'; + +/// Mutex to prevent concurrent token refresh operations +/// This ensures only one refresh operation happens at a time, +/// preventing race conditions during app startup when multiple +/// providers try to refresh tokens simultaneously. +class TokenRefreshMutex { + static final _instance = TokenRefreshMutex._(); + factory TokenRefreshMutex() => _instance; + TokenRefreshMutex._(); + + static final _logger = Logger('service.authorization.token_mutex'); + + Completer? _currentRefresh; + Completer? _currentRotation; + + /// Execute a token refresh operation with mutex protection + /// If another refresh is in progress, wait for it to complete + Future executeRefresh(Future Function() refreshOperation) async { + if (_currentRefresh != null) { + _logger.fine('Token refresh already in progress, waiting for completion'); + return await _currentRefresh!.future; + } + + _logger.fine('Starting new token refresh operation'); + _currentRefresh = Completer(); + + try { + final result = await refreshOperation(); + if (_currentRefresh != null) { + _currentRefresh!.complete(result); + _logger.fine('Token refresh completed successfully'); + } + return result; + } catch (e, st) { + _logger.warning('Token refresh failed', e, st); + if (_currentRefresh != null) { + _currentRefresh!.completeError(e, st); + } + rethrow; + } finally { + _currentRefresh = null; + } + } + + /// Execute a token rotation operation with mutex protection + /// If another rotation is in progress, wait for it to complete + Future executeRotation(Future Function() rotationOperation) async { + if (_currentRotation != null) { + _logger.fine('Token rotation already in progress, waiting for completion'); + return await _currentRotation!.future; + } + + _logger.fine('Starting new token rotation operation'); + _currentRotation = Completer(); + + try { + await rotationOperation(); + if (_currentRotation != null) { + _currentRotation!.complete(); + _logger.fine('Token rotation completed successfully'); + } + } catch (e, st) { + _logger.warning('Token rotation failed', e, st); + if (_currentRotation != null) { + _currentRotation!.completeError(e, st); + } + rethrow; + } finally { + _currentRotation = null; + } + } + + /// Check if a refresh operation is currently in progress + bool get isRefreshInProgress => _currentRefresh != null; + + /// Check if a rotation operation is currently in progress + bool get isRotationInProgress => _currentRotation != null; + + /// Get current status for debugging + Map getStatus() { + return { + 'refreshInProgress': isRefreshInProgress, + 'rotationInProgress': isRotationInProgress, + 'timestamp': DateTime.now().toIso8601String(), + }; + } + + /// Force reset the mutex (for testing or emergency situations) + void forceReset() { + _logger.warning('Force resetting token refresh mutex'); + if (_currentRefresh != null && !_currentRefresh!.isCompleted) { + _currentRefresh!.completeError(Exception('Mutex force reset')); + } + if (_currentRotation != null && !_currentRotation!.isCompleted) { + _currentRotation!.completeError(Exception('Mutex force reset')); + } + _currentRefresh = null; + _currentRotation = null; + } +} \ No newline at end of file diff --git a/frontend/pshared/lib/service/secure_storage.dart b/frontend/pshared/lib/service/secure_storage.dart index fa0f9e0..90df91b 100644 --- a/frontend/pshared/lib/service/secure_storage.dart +++ b/frontend/pshared/lib/service/secure_storage.dart @@ -1,5 +1,6 @@ import 'package:shared_preferences/shared_preferences.dart'; + class SecureStorageService { static Future get(String key) async { final prefs = await SharedPreferences.getInstance(); @@ -18,6 +19,11 @@ class SecureStorageService { return _setImp(prefs, key, value); } + static Future containsKey(String key) async { + final prefs = await SharedPreferences.getInstance(); + return prefs.containsKey(key); + } + static Future delete(String key) async { final prefs = await SharedPreferences.getInstance(); await prefs.remove(key); diff --git a/frontend/pshared/lib/service/services.dart b/frontend/pshared/lib/service/services.dart index efeb745..f6c9564 100644 --- a/frontend/pshared/lib/service/services.dart +++ b/frontend/pshared/lib/service/services.dart @@ -6,27 +6,13 @@ class Services { static const String invitations = 'invitations'; static const String organization = 'organizations'; static const String permission = 'permissions'; - static const String project = 'projects'; - static const String pgroup = 'priority_groups'; - static const String priorities = 'priorities'; - static const String reactions = 'reactions'; static const String storage = 'storage'; - static const String taskStatus = 'statuses'; - static const String tasks = 'tasks'; static const String amplitude = 'amplitude'; - static const String automations = 'automation'; - static const String changes = 'changes'; static const String clients = 'clients'; - static const String invoices = 'invoices'; static const String logo = 'logo'; static const String notifications = 'notifications'; static const String policies = 'policies'; - static const String properties = 'properties'; static const String refreshTokens = 'refresh_tokens'; static const String roles = 'roles'; - static const String steps = 'steps'; - static const String teams = 'teams'; - static const String workflows = 'workflows'; - static const String workspaces = 'workspaces'; } diff --git a/frontend/pshared/lib/provider/exception.dart b/frontend/pshared/lib/utils/exception.dart similarity index 100% rename from frontend/pshared/lib/provider/exception.dart rename to frontend/pshared/lib/utils/exception.dart diff --git a/frontend/pweb/lib/.gitignore b/frontend/pweb/lib/.gitignore new file mode 100644 index 0000000..86d4c2d --- /dev/null +++ b/frontend/pweb/lib/.gitignore @@ -0,0 +1 @@ +generated diff --git a/frontend/pweb/lib/generated/i18n/app_localizations.dart b/frontend/pweb/lib/generated/i18n/app_localizations.dart deleted file mode 100644 index 91889a2..0000000 --- a/frontend/pweb/lib/generated/i18n/app_localizations.dart +++ /dev/null @@ -1,1562 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:intl/intl.dart' as intl; - -import 'app_localizations_en.dart'; -import 'app_localizations_ru.dart'; - -// ignore_for_file: type=lint - -/// Callers can lookup localized strings with an instance of AppLocalizations -/// returned by `AppLocalizations.of(context)`. -/// -/// Applications need to include `AppLocalizations.delegate()` in their app's -/// `localizationDelegates` list, and the locales they support in the app's -/// `supportedLocales` list. For example: -/// -/// ```dart -/// import 'i18n/app_localizations.dart'; -/// -/// return MaterialApp( -/// localizationsDelegates: AppLocalizations.localizationsDelegates, -/// supportedLocales: AppLocalizations.supportedLocales, -/// home: MyApplicationHome(), -/// ); -/// ``` -/// -/// ## Update pubspec.yaml -/// -/// Please make sure to update your pubspec.yaml to include the following -/// packages: -/// -/// ```yaml -/// dependencies: -/// # Internationalization support. -/// flutter_localizations: -/// sdk: flutter -/// intl: any # Use the pinned version from flutter_localizations -/// -/// # Rest of dependencies -/// ``` -/// -/// ## iOS Applications -/// -/// iOS applications define key application metadata, including supported -/// locales, in an Info.plist file that is built into the application bundle. -/// To configure the locales supported by your app, you’ll need to edit this -/// file. -/// -/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. -/// Then, in the Project Navigator, open the Info.plist file under the Runner -/// project’s Runner folder. -/// -/// Next, select the Information Property List item, select Add Item from the -/// Editor menu, then select Localizations from the pop-up menu. -/// -/// Select and expand the newly-created Localizations item then, for each -/// locale your application supports, add a new item and select the locale -/// you wish to add from the pop-up menu in the Value field. This list should -/// be consistent with the languages listed in the AppLocalizations.supportedLocales -/// property. -abstract class AppLocalizations { - AppLocalizations(String locale) - : localeName = intl.Intl.canonicalizedLocale(locale.toString()); - - final String localeName; - - static AppLocalizations? of(BuildContext context) { - return Localizations.of(context, AppLocalizations); - } - - static const LocalizationsDelegate delegate = - _AppLocalizationsDelegate(); - - /// A list of this localizations delegate along with the default localizations - /// delegates. - /// - /// Returns a list of localizations delegates containing this delegate along with - /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, - /// and GlobalWidgetsLocalizations.delegate. - /// - /// Additional delegates can be added by appending to this list in - /// MaterialApp. This list does not have to be used at all if a custom list - /// of delegates is preferred or required. - static const List> localizationsDelegates = - >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; - - /// A list of this localizations delegate's supported locales. - static const List supportedLocales = [ - Locale('en'), - Locale('ru'), - ]; - - /// No description provided for @login. - /// - /// In en, this message translates to: - /// **'Login'** - String get login; - - /// No description provided for @logout. - /// - /// In en, this message translates to: - /// **'Logout'** - String get logout; - - /// No description provided for @profile. - /// - /// In en, this message translates to: - /// **'Profile'** - String get profile; - - /// No description provided for @signup. - /// - /// In en, this message translates to: - /// **'Sign up'** - String get signup; - - /// No description provided for @username. - /// - /// In en, this message translates to: - /// **'Email'** - String get username; - - /// No description provided for @usernameHint. - /// - /// In en, this message translates to: - /// **'email@example.com'** - String get usernameHint; - - /// No description provided for @usernameErrorInvalid. - /// - /// In en, this message translates to: - /// **'Provide a valid email address'** - String get usernameErrorInvalid; - - /// No description provided for @usernameUnknownTLD. - /// - /// In en, this message translates to: - /// **'Domain .{domain} is not known, please, check it'** - String usernameUnknownTLD(Object domain); - - /// No description provided for @password. - /// - /// In en, this message translates to: - /// **'Password'** - String get password; - - /// No description provided for @confirmPassword. - /// - /// In en, this message translates to: - /// **'Confirm password'** - String get confirmPassword; - - /// No description provided for @passwordValidationRuleDigit. - /// - /// In en, this message translates to: - /// **'has digit'** - String get passwordValidationRuleDigit; - - /// No description provided for @passwordValidationRuleUpperCase. - /// - /// In en, this message translates to: - /// **'has uppercase letter'** - String get passwordValidationRuleUpperCase; - - /// No description provided for @passwordValidationRuleLowerCase. - /// - /// In en, this message translates to: - /// **'has lowercase letter'** - String get passwordValidationRuleLowerCase; - - /// No description provided for @passwordValidationRuleSpecialCharacter. - /// - /// In en, this message translates to: - /// **'has special character letter'** - String get passwordValidationRuleSpecialCharacter; - - /// No description provided for @passwordValidationRuleMinCharacters. - /// - /// In en, this message translates to: - /// **'is {charNum} characters long at least'** - String passwordValidationRuleMinCharacters(Object charNum); - - /// No description provided for @passwordsDoNotMatch. - /// - /// In en, this message translates to: - /// **'Passwords do not match'** - String get passwordsDoNotMatch; - - /// No description provided for @passwordValidationError. - /// - /// In en, this message translates to: - /// **'Check that your password {matchesCriteria}'** - String passwordValidationError(Object matchesCriteria); - - /// No description provided for @notificationError. - /// - /// In en, this message translates to: - /// **'Error occurred: {error}'** - String notificationError(Object error); - - /// No description provided for @loginUserNotFound. - /// - /// In en, this message translates to: - /// **'Account {account} has not been registered in the system'** - String loginUserNotFound(Object account); - - /// No description provided for @loginPasswordIncorrect. - /// - /// In en, this message translates to: - /// **'Authorization failed, please check your password'** - String get loginPasswordIncorrect; - - /// No description provided for @internalErrorOccurred. - /// - /// In en, this message translates to: - /// **'An internal server error occurred: {error}, we already know about it and working hard to fix it'** - String internalErrorOccurred(Object error); - - /// No description provided for @noErrorInformation. - /// - /// In en, this message translates to: - /// **'Some error occurred, but we have not error information. We are already investigating the issue'** - String get noErrorInformation; - - /// No description provided for @yourName. - /// - /// In en, this message translates to: - /// **'Your name'** - String get yourName; - - /// No description provided for @nameHint. - /// - /// In en, this message translates to: - /// **'John Doe'** - String get nameHint; - - /// No description provided for @errorPageNotFoundTitle. - /// - /// In en, this message translates to: - /// **'Page Not Found'** - String get errorPageNotFoundTitle; - - /// No description provided for @errorPageNotFoundMessage. - /// - /// In en, this message translates to: - /// **'Oops! We couldn\'t find that page.'** - String get errorPageNotFoundMessage; - - /// No description provided for @errorPageNotFoundHint. - /// - /// In en, this message translates to: - /// **'The page you\'re looking for doesn\'t exist or has been moved. Please check the URL or return to the home page.'** - String get errorPageNotFoundHint; - - /// No description provided for @errorUnknown. - /// - /// In en, this message translates to: - /// **'Unknown error occurred'** - String get errorUnknown; - - /// No description provided for @unknown. - /// - /// In en, this message translates to: - /// **'unknown'** - String get unknown; - - /// No description provided for @goToLogin. - /// - /// In en, this message translates to: - /// **'Go to Login'** - String get goToLogin; - - /// No description provided for @goBack. - /// - /// In en, this message translates to: - /// **'Go Back'** - String get goBack; - - /// No description provided for @goToMainPage. - /// - /// In en, this message translates to: - /// **'Go to Main Page'** - String get goToMainPage; - - /// No description provided for @goToSignUp. - /// - /// In en, this message translates to: - /// **'Go to Sign Up'** - String get goToSignUp; - - /// No description provided for @signupError. - /// - /// In en, this message translates to: - /// **'Failed to signup: {error}'** - String signupError(Object error); - - /// No description provided for @signupSuccess. - /// - /// In en, this message translates to: - /// **'Email confirmation message has been sent to {email}. Please, open it and click link to activate your account.'** - String signupSuccess(Object email); - - /// No description provided for @connectivityError. - /// - /// In en, this message translates to: - /// **'Cannot reach the server at {serverAddress}. Check your network and try again.'** - String connectivityError(Object serverAddress); - - /// No description provided for @errorAccountExists. - /// - /// In en, this message translates to: - /// **'Account already exists'** - String get errorAccountExists; - - /// No description provided for @errorAccountNotVerified. - /// - /// In en, this message translates to: - /// **'Your account hasn\'t been verified yet. Please check your email to complete the verification'** - String get errorAccountNotVerified; - - /// No description provided for @errorLoginUnauthorized. - /// - /// In en, this message translates to: - /// **'Login or password is incorrect. Please try again'** - String get errorLoginUnauthorized; - - /// No description provided for @errorInternalError. - /// - /// In en, this message translates to: - /// **'An internal error occurred. We\'re aware of the issue and working to resolve it. Please try again later'** - String get errorInternalError; - - /// No description provided for @errorVerificationTokenNotFound. - /// - /// In en, this message translates to: - /// **'Account for verification not found. Sign up again'** - String get errorVerificationTokenNotFound; - - /// No description provided for @created. - /// - /// In en, this message translates to: - /// **'Created'** - String get created; - - /// No description provided for @edited. - /// - /// In en, this message translates to: - /// **'Edited'** - String get edited; - - /// No description provided for @errorDataConflict. - /// - /// In en, this message translates to: - /// **'We can’t process your data because it has conflicting or contradictory information.'** - String get errorDataConflict; - - /// No description provided for @errorAccessDenied. - /// - /// In en, this message translates to: - /// **'You do not have permission to access this resource. If you need access, please contact an administrator.'** - String get errorAccessDenied; - - /// No description provided for @errorBrokenPayload. - /// - /// In en, this message translates to: - /// **'The data you sent is invalid or incomplete. Please check your submission and try again.'** - String get errorBrokenPayload; - - /// No description provided for @errorInvalidArgument. - /// - /// In en, this message translates to: - /// **'One or more arguments are invalid. Verify your input and try again.'** - String get errorInvalidArgument; - - /// No description provided for @errorBrokenReference. - /// - /// In en, this message translates to: - /// **'The resource you\'re trying to access could not be referenced. It may have been moved or deleted.'** - String get errorBrokenReference; - - /// No description provided for @errorInvalidQueryParameter. - /// - /// In en, this message translates to: - /// **'One or more query parameters are missing or incorrect. Check them and try again.'** - String get errorInvalidQueryParameter; - - /// No description provided for @errorNotImplemented. - /// - /// In en, this message translates to: - /// **'This feature is not yet available. Please try again later or contact support.'** - String get errorNotImplemented; - - /// No description provided for @errorLicenseRequired. - /// - /// In en, this message translates to: - /// **'A valid license is required to perform this action. Please contact your administrator.'** - String get errorLicenseRequired; - - /// No description provided for @errorNotFound. - /// - /// In en, this message translates to: - /// **'We couldn\'t find the resource you requested. It may have been removed or is temporarily unavailable.'** - String get errorNotFound; - - /// No description provided for @errorNameMissing. - /// - /// In en, this message translates to: - /// **'Please provide a name before continuing.'** - String get errorNameMissing; - - /// No description provided for @errorEmailMissing. - /// - /// In en, this message translates to: - /// **'Please provide an email address before continuing.'** - String get errorEmailMissing; - - /// No description provided for @errorPasswordMissing. - /// - /// In en, this message translates to: - /// **'Please provide a password before continuing.'** - String get errorPasswordMissing; - - /// No description provided for @errorEmailNotRegistered. - /// - /// In en, this message translates to: - /// **'We could not find an account associated with that email address.'** - String get errorEmailNotRegistered; - - /// No description provided for @errorDuplicateEmail. - /// - /// In en, this message translates to: - /// **'This email address is already in use. Try another one or reset your password.'** - String get errorDuplicateEmail; - - /// No description provided for @showDetailsAction. - /// - /// In en, this message translates to: - /// **'Show Details'** - String get showDetailsAction; - - /// No description provided for @errorLogin. - /// - /// In en, this message translates to: - /// **'Error logging in'** - String get errorLogin; - - /// Error message displayed when invitation creation fails - /// - /// In en, this message translates to: - /// **'Failed to create invitaiton'** - String get errorCreatingInvitation; - - /// No description provided for @footerCompanyName. - /// - /// In en, this message translates to: - /// **'Sibilla Solutions LTD'** - String get footerCompanyName; - - /// No description provided for @footerAddress. - /// - /// In en, this message translates to: - /// **'27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus'** - String get footerAddress; - - /// No description provided for @footerSupport. - /// - /// In en, this message translates to: - /// **'Support'** - String get footerSupport; - - /// No description provided for @footerEmail. - /// - /// In en, this message translates to: - /// **'Email TBD'** - String get footerEmail; - - /// No description provided for @footerPhoneLabel. - /// - /// In en, this message translates to: - /// **'Phone'** - String get footerPhoneLabel; - - /// No description provided for @footerPhone. - /// - /// In en, this message translates to: - /// **'+357 22 000 253'** - String get footerPhone; - - /// No description provided for @footerTermsOfService. - /// - /// In en, this message translates to: - /// **'Terms of Service'** - String get footerTermsOfService; - - /// No description provided for @footerPrivacyPolicy. - /// - /// In en, this message translates to: - /// **'Privacy Policy'** - String get footerPrivacyPolicy; - - /// No description provided for @footerCookiePolicy. - /// - /// In en, this message translates to: - /// **'Cookie Policy'** - String get footerCookiePolicy; - - /// No description provided for @navigationLogout. - /// - /// In en, this message translates to: - /// **'Logout'** - String get navigationLogout; - - /// No description provided for @dashboard. - /// - /// In en, this message translates to: - /// **'Dashboard'** - String get dashboard; - - /// No description provided for @navigationUsersSettings. - /// - /// In en, this message translates to: - /// **'Users'** - String get navigationUsersSettings; - - /// No description provided for @navigationRolesSettings. - /// - /// In en, this message translates to: - /// **'Roles'** - String get navigationRolesSettings; - - /// No description provided for @navigationPermissionsSettings. - /// - /// In en, this message translates to: - /// **'Permissions'** - String get navigationPermissionsSettings; - - /// No description provided for @usersManagement. - /// - /// In en, this message translates to: - /// **'User Management'** - String get usersManagement; - - /// No description provided for @navigationOrganizationSettings. - /// - /// In en, this message translates to: - /// **'Organization settings'** - String get navigationOrganizationSettings; - - /// No description provided for @navigationAccountSettings. - /// - /// In en, this message translates to: - /// **'Profile settings'** - String get navigationAccountSettings; - - /// No description provided for @twoFactorPrompt. - /// - /// In en, this message translates to: - /// **'Enter the 6-digit code we sent to your device'** - String get twoFactorPrompt; - - /// No description provided for @twoFactorResend. - /// - /// In en, this message translates to: - /// **'Didn’t receive a code? Resend'** - String get twoFactorResend; - - /// No description provided for @twoFactorTitle. - /// - /// In en, this message translates to: - /// **'Two-Factor Authentication'** - String get twoFactorTitle; - - /// No description provided for @twoFactorError. - /// - /// In en, this message translates to: - /// **'Invalid code. Please try again.'** - String get twoFactorError; - - /// No description provided for @payoutNavDashboard. - /// - /// In en, this message translates to: - /// **'Dashboard'** - String get payoutNavDashboard; - - /// No description provided for @payoutNavSendPayout. - /// - /// In en, this message translates to: - /// **'Send payout'** - String get payoutNavSendPayout; - - /// No description provided for @payoutNavRecipients. - /// - /// In en, this message translates to: - /// **'Recipients'** - String get payoutNavRecipients; - - /// No description provided for @payoutNavReports. - /// - /// In en, this message translates to: - /// **'Reports'** - String get payoutNavReports; - - /// No description provided for @payoutNavSettings. - /// - /// In en, this message translates to: - /// **'Settings'** - String get payoutNavSettings; - - /// No description provided for @payoutNavLogout. - /// - /// In en, this message translates to: - /// **'Logout'** - String get payoutNavLogout; - - /// No description provided for @payoutNavMethods. - /// - /// In en, this message translates to: - /// **'Payouts'** - String get payoutNavMethods; - - /// No description provided for @expand. - /// - /// In en, this message translates to: - /// **'Expand'** - String get expand; - - /// No description provided for @collapse. - /// - /// In en, this message translates to: - /// **'Collapse'** - String get collapse; - - /// Title of the recipient address book page - /// - /// In en, this message translates to: - /// **'Recipient address book'** - String get pageTitleRecipients; - - /// Tooltip and button label to add a new recipient - /// - /// In en, this message translates to: - /// **'Add new'** - String get actionAddNew; - - /// Column header for who manages the payout data - /// - /// In en, this message translates to: - /// **'Data owner'** - String get colDataOwner; - - /// Column header for recipient avatar - /// - /// In en, this message translates to: - /// **'Avatar'** - String get colAvatar; - - /// Column header for recipient name - /// - /// In en, this message translates to: - /// **'Name'** - String get colName; - - /// Column header for recipient email address - /// - /// In en, this message translates to: - /// **'Email'** - String get colEmail; - - /// Column header for payout readiness status - /// - /// In en, this message translates to: - /// **'Status'** - String get colStatus; - - /// Status indicating payouts can be sent immediately - /// - /// In en, this message translates to: - /// **'Ready'** - String get statusReady; - - /// Status indicating recipient is registered but not yet fully ready - /// - /// In en, this message translates to: - /// **'Registered'** - String get statusRegistered; - - /// Status indicating recipient has not completed registration - /// - /// In en, this message translates to: - /// **'Not registered'** - String get statusNotRegistered; - - /// Label for recipients whose payout data is managed internally by the user/company - /// - /// In en, this message translates to: - /// **'Managed by me'** - String get typeInternal; - - /// Label for recipients who manage their own payout data - /// - /// In en, this message translates to: - /// **'Self‑managed'** - String get typeExternal; - - /// No description provided for @searchHint. - /// - /// In en, this message translates to: - /// **'Search recipients'** - String get searchHint; - - /// No description provided for @colActions. - /// - /// In en, this message translates to: - /// **'Actions'** - String get colActions; - - /// No description provided for @menuEdit. - /// - /// In en, this message translates to: - /// **'Edit'** - String get menuEdit; - - /// No description provided for @menuSendPayout. - /// - /// In en, this message translates to: - /// **'Send payout'** - String get menuSendPayout; - - /// No description provided for @tooltipRowActions. - /// - /// In en, this message translates to: - /// **'More actions'** - String get tooltipRowActions; - - /// No description provided for @accountSettings. - /// - /// In en, this message translates to: - /// **'Account Settings'** - String get accountSettings; - - /// No description provided for @accountNameUpdateError. - /// - /// In en, this message translates to: - /// **'Failed to update account name'** - String get accountNameUpdateError; - - /// No description provided for @settingsSuccessfullyUpdated. - /// - /// In en, this message translates to: - /// **'Settings successfully updated'** - String get settingsSuccessfullyUpdated; - - /// No description provided for @language. - /// - /// In en, this message translates to: - /// **'Language'** - String get language; - - /// No description provided for @failedToUpdateLanguage. - /// - /// In en, this message translates to: - /// **'Failed to update language'** - String get failedToUpdateLanguage; - - /// No description provided for @settingsImageUpdateError. - /// - /// In en, this message translates to: - /// **'Couldn\'t update the image'** - String get settingsImageUpdateError; - - /// No description provided for @settingsImageTitle. - /// - /// In en, this message translates to: - /// **'Image'** - String get settingsImageTitle; - - /// No description provided for @settingsImageHint. - /// - /// In en, this message translates to: - /// **'Tap to change the image'** - String get settingsImageHint; - - /// No description provided for @accountName. - /// - /// In en, this message translates to: - /// **'Name'** - String get accountName; - - /// No description provided for @accountNameHint. - /// - /// In en, this message translates to: - /// **'Specify your name'** - String get accountNameHint; - - /// No description provided for @avatar. - /// - /// In en, this message translates to: - /// **'Profile photo'** - String get avatar; - - /// No description provided for @avatarHint. - /// - /// In en, this message translates to: - /// **'Tap to update'** - String get avatarHint; - - /// No description provided for @avatarUpdateError. - /// - /// In en, this message translates to: - /// **'Failed to update profile photo'** - String get avatarUpdateError; - - /// No description provided for @settings. - /// - /// In en, this message translates to: - /// **'Settings'** - String get settings; - - /// No description provided for @notSet. - /// - /// In en, this message translates to: - /// **'not set'** - String get notSet; - - /// No description provided for @search. - /// - /// In en, this message translates to: - /// **'Search...'** - String get search; - - /// No description provided for @ok. - /// - /// In en, this message translates to: - /// **'Ok'** - String get ok; - - /// No description provided for @cancel. - /// - /// In en, this message translates to: - /// **'Cancel'** - String get cancel; - - /// No description provided for @confirm. - /// - /// In en, this message translates to: - /// **'Confirm'** - String get confirm; - - /// No description provided for @back. - /// - /// In en, this message translates to: - /// **'Back'** - String get back; - - /// Title of the operation history page - /// - /// In en, this message translates to: - /// **'Operation history'** - String get operationfryTitle; - - /// Label for the filters expansion panel - /// - /// In en, this message translates to: - /// **'Filters'** - String get filters; - - /// Label for the date‐range filter - /// - /// In en, this message translates to: - /// **'Period'** - String get period; - - /// Placeholder when no period is selected - /// - /// In en, this message translates to: - /// **'Select period'** - String get selectPeriod; - - /// Button text to apply the filters - /// - /// In en, this message translates to: - /// **'Apply'** - String get apply; - - /// Template for a single status filter chip - /// - /// In en, this message translates to: - /// **'{status}'** - String status(String status); - - /// Status indicating the operation succeeded - /// - /// In en, this message translates to: - /// **'Successful'** - String get operationStatusSuccessful; - - /// Status indicating the operation is pending - /// - /// In en, this message translates to: - /// **'Pending'** - String get operationStatusPending; - - /// Status indicating the operation failed - /// - /// In en, this message translates to: - /// **'Unsuccessful'** - String get operationStatusUnsuccessful; - - /// Table column header for status - /// - /// In en, this message translates to: - /// **'Status'** - String get statusColumn; - - /// Table column header for file name - /// - /// In en, this message translates to: - /// **'File name'** - String get fileNameColumn; - - /// Table column header for the original amount - /// - /// In en, this message translates to: - /// **'Amount'** - String get amountColumn; - - /// Table column header for the converted amount - /// - /// In en, this message translates to: - /// **'To amount'** - String get toAmountColumn; - - /// Table column header for the payment ID - /// - /// In en, this message translates to: - /// **'Pay ID'** - String get payIdColumn; - - /// Table column header for the masked card number - /// - /// In en, this message translates to: - /// **'Card number'** - String get cardNumberColumn; - - /// Table column header for recipient name - /// - /// In en, this message translates to: - /// **'Name'** - String get nameColumn; - - /// Table column header for the date/time - /// - /// In en, this message translates to: - /// **'Date'** - String get dateColumn; - - /// Table column header for any comment - /// - /// In en, this message translates to: - /// **'Comment'** - String get commentColumn; - - /// No description provided for @paymentConfigTitle. - /// - /// In en, this message translates to: - /// **'Where to receive money'** - String get paymentConfigTitle; - - /// No description provided for @paymentConfigSubtitle. - /// - /// In en, this message translates to: - /// **'Add multiple methods and choose your primary one.'** - String get paymentConfigSubtitle; - - /// No description provided for @addPaymentMethod. - /// - /// In en, this message translates to: - /// **'Add payment method'** - String get addPaymentMethod; - - /// No description provided for @makeMain. - /// - /// In en, this message translates to: - /// **'Make primary'** - String get makeMain; - - /// No description provided for @advanced. - /// - /// In en, this message translates to: - /// **'Advanced'** - String get advanced; - - /// No description provided for @fallbackExplanation. - /// - /// In en, this message translates to: - /// **'If the primary method is unavailable, we will try the next enabled one in the list.'** - String get fallbackExplanation; - - /// Button label to delete a payment method - /// - /// In en, this message translates to: - /// **'Delete'** - String get delete; - - /// Confirmation dialog message shown before a payment method is removed - /// - /// In en, this message translates to: - /// **'Are you sure you want to delete this payment method?'** - String get deletePaymentConfirmation; - - /// Button label to edit a payment method - /// - /// In en, this message translates to: - /// **'Edit'** - String get edit; - - /// Tooltip for an overflow menu button that reveals extra actions for a payment method - /// - /// In en, this message translates to: - /// **'More actions'** - String get moreActions; - - /// No description provided for @noPayouts. - /// - /// In en, this message translates to: - /// **'No Payouts'** - String get noPayouts; - - /// No description provided for @enterBankName. - /// - /// In en, this message translates to: - /// **'Enter bank name'** - String get enterBankName; - - /// No description provided for @paymentType. - /// - /// In en, this message translates to: - /// **'Payment Method Type'** - String get paymentType; - - /// No description provided for @selectPaymentType. - /// - /// In en, this message translates to: - /// **'Please select a payment method type'** - String get selectPaymentType; - - /// No description provided for @paymentTypeCard. - /// - /// In en, this message translates to: - /// **'Credit Card'** - String get paymentTypeCard; - - /// No description provided for @paymentTypeBankAccount. - /// - /// In en, this message translates to: - /// **'Russian Bank Account'** - String get paymentTypeBankAccount; - - /// No description provided for @paymentTypeIban. - /// - /// In en, this message translates to: - /// **'IBAN'** - String get paymentTypeIban; - - /// No description provided for @paymentTypeWallet. - /// - /// In en, this message translates to: - /// **'Wallet'** - String get paymentTypeWallet; - - /// No description provided for @cardNumber. - /// - /// In en, this message translates to: - /// **'Card Number'** - String get cardNumber; - - /// No description provided for @enterCardNumber. - /// - /// In en, this message translates to: - /// **'Enter the card number'** - String get enterCardNumber; - - /// No description provided for @cardholderName. - /// - /// In en, this message translates to: - /// **'Cardholder Name'** - String get cardholderName; - - /// No description provided for @iban. - /// - /// In en, this message translates to: - /// **'IBAN'** - String get iban; - - /// No description provided for @enterIban. - /// - /// In en, this message translates to: - /// **'Enter IBAN'** - String get enterIban; - - /// No description provided for @bic. - /// - /// In en, this message translates to: - /// **'BIC'** - String get bic; - - /// No description provided for @bankName. - /// - /// In en, this message translates to: - /// **'Bank Name'** - String get bankName; - - /// No description provided for @accountHolder. - /// - /// In en, this message translates to: - /// **'Account Holder'** - String get accountHolder; - - /// No description provided for @enterAccountHolder. - /// - /// In en, this message translates to: - /// **'Enter account holder'** - String get enterAccountHolder; - - /// No description provided for @enterBic. - /// - /// In en, this message translates to: - /// **'Enter BIC'** - String get enterBic; - - /// No description provided for @walletId. - /// - /// In en, this message translates to: - /// **'Wallet ID'** - String get walletId; - - /// No description provided for @enterWalletId. - /// - /// In en, this message translates to: - /// **'Enter wallet ID'** - String get enterWalletId; - - /// No description provided for @recipients. - /// - /// In en, this message translates to: - /// **'Recipients'** - String get recipients; - - /// No description provided for @recipientName. - /// - /// In en, this message translates to: - /// **'Recipient Name'** - String get recipientName; - - /// No description provided for @enterRecipientName. - /// - /// In en, this message translates to: - /// **'Enter recipient name'** - String get enterRecipientName; - - /// No description provided for @inn. - /// - /// In en, this message translates to: - /// **'INN'** - String get inn; - - /// No description provided for @enterInn. - /// - /// In en, this message translates to: - /// **'Enter INN'** - String get enterInn; - - /// No description provided for @kpp. - /// - /// In en, this message translates to: - /// **'KPP'** - String get kpp; - - /// No description provided for @enterKpp. - /// - /// In en, this message translates to: - /// **'Enter KPP'** - String get enterKpp; - - /// No description provided for @accountNumber. - /// - /// In en, this message translates to: - /// **'Account Number'** - String get accountNumber; - - /// No description provided for @enterAccountNumber. - /// - /// In en, this message translates to: - /// **'Enter account number'** - String get enterAccountNumber; - - /// No description provided for @correspondentAccount. - /// - /// In en, this message translates to: - /// **'Correspondent Account'** - String get correspondentAccount; - - /// No description provided for @enterCorrespondentAccount. - /// - /// In en, this message translates to: - /// **'Enter correspondent account'** - String get enterCorrespondentAccount; - - /// No description provided for @bik. - /// - /// In en, this message translates to: - /// **'BIK'** - String get bik; - - /// No description provided for @enterBik. - /// - /// In en, this message translates to: - /// **'Enter BIK'** - String get enterBik; - - /// No description provided for @add. - /// - /// In en, this message translates to: - /// **'Add'** - String get add; - - /// No description provided for @expiryDate. - /// - /// In en, this message translates to: - /// **'Expiry (MM/YY)'** - String get expiryDate; - - /// No description provided for @firstName. - /// - /// In en, this message translates to: - /// **'First Name'** - String get firstName; - - /// No description provided for @enterFirstName. - /// - /// In en, this message translates to: - /// **'Enter First Name'** - String get enterFirstName; - - /// No description provided for @lastName. - /// - /// In en, this message translates to: - /// **'Last Name'** - String get lastName; - - /// No description provided for @enterLastName. - /// - /// In en, this message translates to: - /// **'Enter Last Name'** - String get enterLastName; - - /// No description provided for @sendSingle. - /// - /// In en, this message translates to: - /// **'Send single transaction'** - String get sendSingle; - - /// No description provided for @sendMultiple. - /// - /// In en, this message translates to: - /// **'Send multiple transactions'** - String get sendMultiple; - - /// No description provided for @addFunds. - /// - /// In en, this message translates to: - /// **'Add Funds'** - String get addFunds; - - /// No description provided for @close. - /// - /// In en, this message translates to: - /// **'Close'** - String get close; - - /// No description provided for @multiplePayout. - /// - /// In en, this message translates to: - /// **'Multiple Payout'** - String get multiplePayout; - - /// No description provided for @howItWorks. - /// - /// In en, this message translates to: - /// **'How it works?'** - String get howItWorks; - - /// No description provided for @exampleTitle. - /// - /// In en, this message translates to: - /// **'File Format & Sample'** - String get exampleTitle; - - /// No description provided for @downloadSampleCSV. - /// - /// In en, this message translates to: - /// **'Download sample.csv'** - String get downloadSampleCSV; - - /// No description provided for @tokenColumn. - /// - /// In en, this message translates to: - /// **'Token (required)'** - String get tokenColumn; - - /// No description provided for @currency. - /// - /// In en, this message translates to: - /// **'Currency'** - String get currency; - - /// No description provided for @amount. - /// - /// In en, this message translates to: - /// **'Amount'** - String get amount; - - /// No description provided for @comment. - /// - /// In en, this message translates to: - /// **'Comment'** - String get comment; - - /// No description provided for @uploadCSV. - /// - /// In en, this message translates to: - /// **'Upload your CSV'** - String get uploadCSV; - - /// No description provided for @upload. - /// - /// In en, this message translates to: - /// **'Upload'** - String get upload; - - /// No description provided for @hintUpload. - /// - /// In en, this message translates to: - /// **'Supported format: .CSV · Max size 1 MB'** - String get hintUpload; - - /// No description provided for @uploadHistory. - /// - /// In en, this message translates to: - /// **'Upload History'** - String get uploadHistory; - - /// No description provided for @payout. - /// - /// In en, this message translates to: - /// **'Payout'** - String get payout; - - /// No description provided for @sendTo. - /// - /// In en, this message translates to: - /// **'Send Payout To'** - String get sendTo; - - /// No description provided for @send. - /// - /// In en, this message translates to: - /// **'Send Payout'** - String get send; - - /// No description provided for @recipientPaysFee. - /// - /// In en, this message translates to: - /// **'Recipient pays the fee'** - String get recipientPaysFee; - - /// Label showing the amount sent - /// - /// In en, this message translates to: - /// **'Sent amount: \${amount}'** - String sentAmount(String amount); - - /// Label showing the transaction fee - /// - /// In en, this message translates to: - /// **'Fee: \${fee}'** - String fee(String fee); - - /// Label showing how much the recipient will receive - /// - /// In en, this message translates to: - /// **'Recipient will receive: \${amount}'** - String recipientWillReceive(String amount); - - /// Label showing the total amount of the transaction - /// - /// In en, this message translates to: - /// **'Total: \${total}'** - String total(String total); - - /// No description provided for @hideDetails. - /// - /// In en, this message translates to: - /// **'Hide Details'** - String get hideDetails; - - /// No description provided for @showDetails. - /// - /// In en, this message translates to: - /// **'Show Details'** - String get showDetails; - - /// No description provided for @whereGetMoney. - /// - /// In en, this message translates to: - /// **'Source of funds for debit'** - String get whereGetMoney; - - /// No description provided for @details. - /// - /// In en, this message translates to: - /// **'Details'** - String get details; - - /// No description provided for @addRecipient. - /// - /// In en, this message translates to: - /// **'Add Recipient'** - String get addRecipient; - - /// No description provided for @editRecipient. - /// - /// In en, this message translates to: - /// **'Edit Recipient'** - String get editRecipient; - - /// No description provided for @saveRecipient. - /// - /// In en, this message translates to: - /// **'Save Recipient'** - String get saveRecipient; - - /// No description provided for @choosePaymentMethod. - /// - /// In en, this message translates to: - /// **'Payment Methods (choose at least 1)'** - String get choosePaymentMethod; - - /// No description provided for @recipientFormRule. - /// - /// In en, this message translates to: - /// **'Recipient must have at least one payment method'** - String get recipientFormRule; - - /// No description provided for @allStatus. - /// - /// In en, this message translates to: - /// **'All'** - String get allStatus; - - /// No description provided for @readyStatus. - /// - /// In en, this message translates to: - /// **'Ready'** - String get readyStatus; - - /// No description provided for @registeredStatus. - /// - /// In en, this message translates to: - /// **'Registered'** - String get registeredStatus; - - /// No description provided for @notRegisteredStatus. - /// - /// In en, this message translates to: - /// **'Not registered'** - String get notRegisteredStatus; - - /// No description provided for @noRecipientSelected. - /// - /// In en, this message translates to: - /// **'No recipient selected'** - String get noRecipientSelected; - - /// No description provided for @companyName. - /// - /// In en, this message translates to: - /// **'Name of your company'** - String get companyName; - - /// No description provided for @companynameRequired. - /// - /// In en, this message translates to: - /// **'Company name required'** - String get companynameRequired; - - /// No description provided for @errorSignUp. - /// - /// In en, this message translates to: - /// **'Error occured while signing up, try again later'** - String get errorSignUp; - - /// No description provided for @companyDescription. - /// - /// In en, this message translates to: - /// **'Company Description'** - String get companyDescription; - - /// No description provided for @companyDescriptionHint. - /// - /// In en, this message translates to: - /// **'Describe any of the fields of the Company\'s business'** - String get companyDescriptionHint; - - /// No description provided for @optional. - /// - /// In en, this message translates to: - /// **'optional'** - String get optional; -} - -class _AppLocalizationsDelegate - extends LocalizationsDelegate { - const _AppLocalizationsDelegate(); - - @override - Future load(Locale locale) { - return SynchronousFuture(lookupAppLocalizations(locale)); - } - - @override - bool isSupported(Locale locale) => - ['en', 'ru'].contains(locale.languageCode); - - @override - bool shouldReload(_AppLocalizationsDelegate old) => false; -} - -AppLocalizations lookupAppLocalizations(Locale locale) { - // Lookup logic when only language code is specified. - switch (locale.languageCode) { - case 'en': - return AppLocalizationsEn(); - case 'ru': - return AppLocalizationsRu(); - } - - throw FlutterError( - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.', - ); -} diff --git a/frontend/pweb/lib/generated/i18n/app_localizations_en.dart b/frontend/pweb/lib/generated/i18n/app_localizations_en.dart deleted file mode 100644 index ddcad3b..0000000 --- a/frontend/pweb/lib/generated/i18n/app_localizations_en.dart +++ /dev/null @@ -1,779 +0,0 @@ -// ignore: unused_import -import 'package:intl/intl.dart' as intl; -import 'app_localizations.dart'; - -// ignore_for_file: type=lint - -/// The translations for English (`en`). -class AppLocalizationsEn extends AppLocalizations { - AppLocalizationsEn([String locale = 'en']) : super(locale); - - @override - String get login => 'Login'; - - @override - String get logout => 'Logout'; - - @override - String get profile => 'Profile'; - - @override - String get signup => 'Sign up'; - - @override - String get username => 'Email'; - - @override - String get usernameHint => 'email@example.com'; - - @override - String get usernameErrorInvalid => 'Provide a valid email address'; - - @override - String usernameUnknownTLD(Object domain) { - return 'Domain .$domain is not known, please, check it'; - } - - @override - String get password => 'Password'; - - @override - String get confirmPassword => 'Confirm password'; - - @override - String get passwordValidationRuleDigit => 'has digit'; - - @override - String get passwordValidationRuleUpperCase => 'has uppercase letter'; - - @override - String get passwordValidationRuleLowerCase => 'has lowercase letter'; - - @override - String get passwordValidationRuleSpecialCharacter => - 'has special character letter'; - - @override - String passwordValidationRuleMinCharacters(Object charNum) { - return 'is $charNum characters long at least'; - } - - @override - String get passwordsDoNotMatch => 'Passwords do not match'; - - @override - String passwordValidationError(Object matchesCriteria) { - return 'Check that your password $matchesCriteria'; - } - - @override - String notificationError(Object error) { - return 'Error occurred: $error'; - } - - @override - String loginUserNotFound(Object account) { - return 'Account $account has not been registered in the system'; - } - - @override - String get loginPasswordIncorrect => - 'Authorization failed, please check your password'; - - @override - String internalErrorOccurred(Object error) { - return 'An internal server error occurred: $error, we already know about it and working hard to fix it'; - } - - @override - String get noErrorInformation => - 'Some error occurred, but we have not error information. We are already investigating the issue'; - - @override - String get yourName => 'Your name'; - - @override - String get nameHint => 'John Doe'; - - @override - String get errorPageNotFoundTitle => 'Page Not Found'; - - @override - String get errorPageNotFoundMessage => 'Oops! We couldn\'t find that page.'; - - @override - String get errorPageNotFoundHint => - 'The page you\'re looking for doesn\'t exist or has been moved. Please check the URL or return to the home page.'; - - @override - String get errorUnknown => 'Unknown error occurred'; - - @override - String get unknown => 'unknown'; - - @override - String get goToLogin => 'Go to Login'; - - @override - String get goBack => 'Go Back'; - - @override - String get goToMainPage => 'Go to Main Page'; - - @override - String get goToSignUp => 'Go to Sign Up'; - - @override - String signupError(Object error) { - return 'Failed to signup: $error'; - } - - @override - String signupSuccess(Object email) { - return 'Email confirmation message has been sent to $email. Please, open it and click link to activate your account.'; - } - - @override - String connectivityError(Object serverAddress) { - return 'Cannot reach the server at $serverAddress. Check your network and try again.'; - } - - @override - String get errorAccountExists => 'Account already exists'; - - @override - String get errorAccountNotVerified => - 'Your account hasn\'t been verified yet. Please check your email to complete the verification'; - - @override - String get errorLoginUnauthorized => - 'Login or password is incorrect. Please try again'; - - @override - String get errorInternalError => - 'An internal error occurred. We\'re aware of the issue and working to resolve it. Please try again later'; - - @override - String get errorVerificationTokenNotFound => - 'Account for verification not found. Sign up again'; - - @override - String get created => 'Created'; - - @override - String get edited => 'Edited'; - - @override - String get errorDataConflict => - 'We can’t process your data because it has conflicting or contradictory information.'; - - @override - String get errorAccessDenied => - 'You do not have permission to access this resource. If you need access, please contact an administrator.'; - - @override - String get errorBrokenPayload => - 'The data you sent is invalid or incomplete. Please check your submission and try again.'; - - @override - String get errorInvalidArgument => - 'One or more arguments are invalid. Verify your input and try again.'; - - @override - String get errorBrokenReference => - 'The resource you\'re trying to access could not be referenced. It may have been moved or deleted.'; - - @override - String get errorInvalidQueryParameter => - 'One or more query parameters are missing or incorrect. Check them and try again.'; - - @override - String get errorNotImplemented => - 'This feature is not yet available. Please try again later or contact support.'; - - @override - String get errorLicenseRequired => - 'A valid license is required to perform this action. Please contact your administrator.'; - - @override - String get errorNotFound => - 'We couldn\'t find the resource you requested. It may have been removed or is temporarily unavailable.'; - - @override - String get errorNameMissing => 'Please provide a name before continuing.'; - - @override - String get errorEmailMissing => - 'Please provide an email address before continuing.'; - - @override - String get errorPasswordMissing => - 'Please provide a password before continuing.'; - - @override - String get errorEmailNotRegistered => - 'We could not find an account associated with that email address.'; - - @override - String get errorDuplicateEmail => - 'This email address is already in use. Try another one or reset your password.'; - - @override - String get showDetailsAction => 'Show Details'; - - @override - String get errorLogin => 'Error logging in'; - - @override - String get errorCreatingInvitation => 'Failed to create invitaiton'; - - @override - String get footerCompanyName => 'Sibilla Solutions LTD'; - - @override - String get footerAddress => - '27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus'; - - @override - String get footerSupport => 'Support'; - - @override - String get footerEmail => 'Email TBD'; - - @override - String get footerPhoneLabel => 'Phone'; - - @override - String get footerPhone => '+357 22 000 253'; - - @override - String get footerTermsOfService => 'Terms of Service'; - - @override - String get footerPrivacyPolicy => 'Privacy Policy'; - - @override - String get footerCookiePolicy => 'Cookie Policy'; - - @override - String get navigationLogout => 'Logout'; - - @override - String get dashboard => 'Dashboard'; - - @override - String get navigationUsersSettings => 'Users'; - - @override - String get navigationRolesSettings => 'Roles'; - - @override - String get navigationPermissionsSettings => 'Permissions'; - - @override - String get usersManagement => 'User Management'; - - @override - String get navigationOrganizationSettings => 'Organization settings'; - - @override - String get navigationAccountSettings => 'Profile settings'; - - @override - String get twoFactorPrompt => 'Enter the 6-digit code we sent to your device'; - - @override - String get twoFactorResend => 'Didn’t receive a code? Resend'; - - @override - String get twoFactorTitle => 'Two-Factor Authentication'; - - @override - String get twoFactorError => 'Invalid code. Please try again.'; - - @override - String get payoutNavDashboard => 'Dashboard'; - - @override - String get payoutNavSendPayout => 'Send payout'; - - @override - String get payoutNavRecipients => 'Recipients'; - - @override - String get payoutNavReports => 'Reports'; - - @override - String get payoutNavSettings => 'Settings'; - - @override - String get payoutNavLogout => 'Logout'; - - @override - String get payoutNavMethods => 'Payouts'; - - @override - String get expand => 'Expand'; - - @override - String get collapse => 'Collapse'; - - @override - String get pageTitleRecipients => 'Recipient address book'; - - @override - String get actionAddNew => 'Add new'; - - @override - String get colDataOwner => 'Data owner'; - - @override - String get colAvatar => 'Avatar'; - - @override - String get colName => 'Name'; - - @override - String get colEmail => 'Email'; - - @override - String get colStatus => 'Status'; - - @override - String get statusReady => 'Ready'; - - @override - String get statusRegistered => 'Registered'; - - @override - String get statusNotRegistered => 'Not registered'; - - @override - String get typeInternal => 'Managed by me'; - - @override - String get typeExternal => 'Self‑managed'; - - @override - String get searchHint => 'Search recipients'; - - @override - String get colActions => 'Actions'; - - @override - String get menuEdit => 'Edit'; - - @override - String get menuSendPayout => 'Send payout'; - - @override - String get tooltipRowActions => 'More actions'; - - @override - String get accountSettings => 'Account Settings'; - - @override - String get accountNameUpdateError => 'Failed to update account name'; - - @override - String get settingsSuccessfullyUpdated => 'Settings successfully updated'; - - @override - String get language => 'Language'; - - @override - String get failedToUpdateLanguage => 'Failed to update language'; - - @override - String get settingsImageUpdateError => 'Couldn\'t update the image'; - - @override - String get settingsImageTitle => 'Image'; - - @override - String get settingsImageHint => 'Tap to change the image'; - - @override - String get accountName => 'Name'; - - @override - String get accountNameHint => 'Specify your name'; - - @override - String get avatar => 'Profile photo'; - - @override - String get avatarHint => 'Tap to update'; - - @override - String get avatarUpdateError => 'Failed to update profile photo'; - - @override - String get settings => 'Settings'; - - @override - String get notSet => 'not set'; - - @override - String get search => 'Search...'; - - @override - String get ok => 'Ok'; - - @override - String get cancel => 'Cancel'; - - @override - String get confirm => 'Confirm'; - - @override - String get back => 'Back'; - - @override - String get operationfryTitle => 'Operation history'; - - @override - String get filters => 'Filters'; - - @override - String get period => 'Period'; - - @override - String get selectPeriod => 'Select period'; - - @override - String get apply => 'Apply'; - - @override - String status(String status) { - return '$status'; - } - - @override - String get operationStatusSuccessful => 'Successful'; - - @override - String get operationStatusPending => 'Pending'; - - @override - String get operationStatusUnsuccessful => 'Unsuccessful'; - - @override - String get statusColumn => 'Status'; - - @override - String get fileNameColumn => 'File name'; - - @override - String get amountColumn => 'Amount'; - - @override - String get toAmountColumn => 'To amount'; - - @override - String get payIdColumn => 'Pay ID'; - - @override - String get cardNumberColumn => 'Card number'; - - @override - String get nameColumn => 'Name'; - - @override - String get dateColumn => 'Date'; - - @override - String get commentColumn => 'Comment'; - - @override - String get paymentConfigTitle => 'Where to receive money'; - - @override - String get paymentConfigSubtitle => - 'Add multiple methods and choose your primary one.'; - - @override - String get addPaymentMethod => 'Add payment method'; - - @override - String get makeMain => 'Make primary'; - - @override - String get advanced => 'Advanced'; - - @override - String get fallbackExplanation => - 'If the primary method is unavailable, we will try the next enabled one in the list.'; - - @override - String get delete => 'Delete'; - - @override - String get deletePaymentConfirmation => - 'Are you sure you want to delete this payment method?'; - - @override - String get edit => 'Edit'; - - @override - String get moreActions => 'More actions'; - - @override - String get noPayouts => 'No Payouts'; - - @override - String get enterBankName => 'Enter bank name'; - - @override - String get paymentType => 'Payment Method Type'; - - @override - String get selectPaymentType => 'Please select a payment method type'; - - @override - String get paymentTypeCard => 'Credit Card'; - - @override - String get paymentTypeBankAccount => 'Russian Bank Account'; - - @override - String get paymentTypeIban => 'IBAN'; - - @override - String get paymentTypeWallet => 'Wallet'; - - @override - String get cardNumber => 'Card Number'; - - @override - String get enterCardNumber => 'Enter the card number'; - - @override - String get cardholderName => 'Cardholder Name'; - - @override - String get iban => 'IBAN'; - - @override - String get enterIban => 'Enter IBAN'; - - @override - String get bic => 'BIC'; - - @override - String get bankName => 'Bank Name'; - - @override - String get accountHolder => 'Account Holder'; - - @override - String get enterAccountHolder => 'Enter account holder'; - - @override - String get enterBic => 'Enter BIC'; - - @override - String get walletId => 'Wallet ID'; - - @override - String get enterWalletId => 'Enter wallet ID'; - - @override - String get recipients => 'Recipients'; - - @override - String get recipientName => 'Recipient Name'; - - @override - String get enterRecipientName => 'Enter recipient name'; - - @override - String get inn => 'INN'; - - @override - String get enterInn => 'Enter INN'; - - @override - String get kpp => 'KPP'; - - @override - String get enterKpp => 'Enter KPP'; - - @override - String get accountNumber => 'Account Number'; - - @override - String get enterAccountNumber => 'Enter account number'; - - @override - String get correspondentAccount => 'Correspondent Account'; - - @override - String get enterCorrespondentAccount => 'Enter correspondent account'; - - @override - String get bik => 'BIK'; - - @override - String get enterBik => 'Enter BIK'; - - @override - String get add => 'Add'; - - @override - String get expiryDate => 'Expiry (MM/YY)'; - - @override - String get firstName => 'First Name'; - - @override - String get enterFirstName => 'Enter First Name'; - - @override - String get lastName => 'Last Name'; - - @override - String get enterLastName => 'Enter Last Name'; - - @override - String get sendSingle => 'Send single transaction'; - - @override - String get sendMultiple => 'Send multiple transactions'; - - @override - String get addFunds => 'Add Funds'; - - @override - String get close => 'Close'; - - @override - String get multiplePayout => 'Multiple Payout'; - - @override - String get howItWorks => 'How it works?'; - - @override - String get exampleTitle => 'File Format & Sample'; - - @override - String get downloadSampleCSV => 'Download sample.csv'; - - @override - String get tokenColumn => 'Token (required)'; - - @override - String get currency => 'Currency'; - - @override - String get amount => 'Amount'; - - @override - String get comment => 'Comment'; - - @override - String get uploadCSV => 'Upload your CSV'; - - @override - String get upload => 'Upload'; - - @override - String get hintUpload => 'Supported format: .CSV · Max size 1 MB'; - - @override - String get uploadHistory => 'Upload History'; - - @override - String get payout => 'Payout'; - - @override - String get sendTo => 'Send Payout To'; - - @override - String get send => 'Send Payout'; - - @override - String get recipientPaysFee => 'Recipient pays the fee'; - - @override - String sentAmount(String amount) { - return 'Sent amount: \$$amount'; - } - - @override - String fee(String fee) { - return 'Fee: \$$fee'; - } - - @override - String recipientWillReceive(String amount) { - return 'Recipient will receive: \$$amount'; - } - - @override - String total(String total) { - return 'Total: \$$total'; - } - - @override - String get hideDetails => 'Hide Details'; - - @override - String get showDetails => 'Show Details'; - - @override - String get whereGetMoney => 'Source of funds for debit'; - - @override - String get details => 'Details'; - - @override - String get addRecipient => 'Add Recipient'; - - @override - String get editRecipient => 'Edit Recipient'; - - @override - String get saveRecipient => 'Save Recipient'; - - @override - String get choosePaymentMethod => 'Payment Methods (choose at least 1)'; - - @override - String get recipientFormRule => - 'Recipient must have at least one payment method'; - - @override - String get allStatus => 'All'; - - @override - String get readyStatus => 'Ready'; - - @override - String get registeredStatus => 'Registered'; - - @override - String get notRegisteredStatus => 'Not registered'; - - @override - String get noRecipientSelected => 'No recipient selected'; - - @override - String get companyName => 'Name of your company'; - - @override - String get companynameRequired => 'Company name required'; - - @override - String get errorSignUp => 'Error occured while signing up, try again later'; - - @override - String get companyDescription => 'Company Description'; - - @override - String get companyDescriptionHint => - 'Describe any of the fields of the Company\'s business'; - - @override - String get optional => 'optional'; -} diff --git a/frontend/pweb/lib/generated/i18n/app_localizations_ru.dart b/frontend/pweb/lib/generated/i18n/app_localizations_ru.dart deleted file mode 100644 index caf30fe..0000000 --- a/frontend/pweb/lib/generated/i18n/app_localizations_ru.dart +++ /dev/null @@ -1,782 +0,0 @@ -// ignore: unused_import -import 'package:intl/intl.dart' as intl; -import 'app_localizations.dart'; - -// ignore_for_file: type=lint - -/// The translations for Russian (`ru`). -class AppLocalizationsRu extends AppLocalizations { - AppLocalizationsRu([String locale = 'ru']) : super(locale); - - @override - String get login => 'Войти'; - - @override - String get logout => 'Выйти'; - - @override - String get profile => 'Профиль'; - - @override - String get signup => 'Регистрация'; - - @override - String get username => 'Email'; - - @override - String get usernameHint => 'email@example.com'; - - @override - String get usernameErrorInvalid => - 'Укажите действительный адрес электронной почты'; - - @override - String usernameUnknownTLD(Object domain) { - return 'Домен .$domain неизвестен, пожалуйста, проверьте его'; - } - - @override - String get password => 'Пароль'; - - @override - String get confirmPassword => 'Подтвердите пароль'; - - @override - String get passwordValidationRuleDigit => 'содержит цифру'; - - @override - String get passwordValidationRuleUpperCase => 'содержит заглавную букву'; - - @override - String get passwordValidationRuleLowerCase => 'содержит строчную букву'; - - @override - String get passwordValidationRuleSpecialCharacter => - 'содержит специальный символ'; - - @override - String passwordValidationRuleMinCharacters(Object charNum) { - return 'длина не менее $charNum символов'; - } - - @override - String get passwordsDoNotMatch => 'Пароли не совпадают'; - - @override - String passwordValidationError(Object matchesCriteria) { - return 'Убедитесь, что ваш пароль $matchesCriteria'; - } - - @override - String notificationError(Object error) { - return 'Произошла ошибка: $error'; - } - - @override - String loginUserNotFound(Object account) { - return 'Аккаунт $account не зарегистрирован в системе'; - } - - @override - String get loginPasswordIncorrect => - 'Ошибка авторизации, пожалуйста, проверьте пароль'; - - @override - String internalErrorOccurred(Object error) { - return 'Произошла внутренняя ошибка сервера: $error, мы уже знаем о ней и усердно работаем над исправлением'; - } - - @override - String get noErrorInformation => - 'Произошла ошибка, но у нас нет информации о ней. Мы уже расследуем этот вопрос'; - - @override - String get yourName => 'Ваше имя'; - - @override - String get nameHint => 'Иван Иванов'; - - @override - String get errorPageNotFoundTitle => 'Страница не найдена'; - - @override - String get errorPageNotFoundMessage => - 'Упс! Мы не смогли найти эту страницу.'; - - @override - String get errorPageNotFoundHint => - 'Запрашиваемая страница не существует или была перемещена. Пожалуйста, проверьте URL или вернитесь на главную страницу.'; - - @override - String get errorUnknown => 'Произошла неизвестная ошибка'; - - @override - String get unknown => 'неизвестно'; - - @override - String get goToLogin => 'Перейти к входу'; - - @override - String get goBack => 'Назад'; - - @override - String get goToMainPage => 'На главную'; - - @override - String get goToSignUp => 'Перейти к регистрации'; - - @override - String signupError(Object error) { - return 'Не удалось зарегистрироваться: $error'; - } - - @override - String signupSuccess(Object email) { - return 'Письмо с подтверждением email отправлено на $email. Пожалуйста, откройте его и перейдите по ссылке для активации вашего аккаунта.'; - } - - @override - String connectivityError(Object serverAddress) { - return 'Не удается связаться с сервером $serverAddress. Проверьте ваше интернет-соединение и попробуйте снова.'; - } - - @override - String get errorAccountExists => 'Account already exists'; - - @override - String get errorAccountNotVerified => - 'Ваш аккаунт еще не подтвержден. Пожалуйста, проверьте вашу электронную почту для завершения верификации'; - - @override - String get errorLoginUnauthorized => - 'Неверный логин или пароль. Пожалуйста, попробуйте снова'; - - @override - String get errorInternalError => - 'Произошла внутренняя ошибка. Мы в курсе проблемы и работаем над ее решением. Пожалуйста, попробуйте позже'; - - @override - String get errorVerificationTokenNotFound => - 'Аккаунт для верификации не найден. Зарегистрируйтесь снова'; - - @override - String get created => 'Создано'; - - @override - String get edited => 'Изменено'; - - @override - String get errorDataConflict => - 'Мы не можем обработать ваши данные, так как они содержат конфликтующую или противоречивую информацию.'; - - @override - String get errorAccessDenied => - 'У вас нет разрешения на доступ к этому ресурсу. Если вам нужен доступ, пожалуйста, обратитесь к администратору.'; - - @override - String get errorBrokenPayload => - 'Отправленные данные недействительны или неполны. Пожалуйста, проверьте введенные данные и попробуйте снова.'; - - @override - String get errorInvalidArgument => - 'Один или несколько аргументов недействительны. Проверьте введенные данные и попробуйте снова.'; - - @override - String get errorBrokenReference => - 'Ресурс, к которому вы пытаетесь получить доступ, не может быть найден. Возможно, он был перемещен или удален.'; - - @override - String get errorInvalidQueryParameter => - 'Один или несколько параметров запроса отсутствуют или указаны неверно. Проверьте их и попробуйте снова.'; - - @override - String get errorNotImplemented => - 'Эта функция еще недоступна. Пожалуйста, попробуйте позже или обратитесь в службу поддержки.'; - - @override - String get errorLicenseRequired => - 'Для выполнения этого действия требуется действующая лицензия. Пожалуйста, обратитесь к вашему администратору.'; - - @override - String get errorNotFound => - 'Мы не смогли найти запрошенный ресурс. Возможно, он был удален или временно недоступен.'; - - @override - String get errorNameMissing => 'Пожалуйста, укажите имя для продолжения.'; - - @override - String get errorEmailMissing => - 'Пожалуйста, укажите адрес электронной почты для продолжения.'; - - @override - String get errorPasswordMissing => - 'Пожалуйста, укажите пароль для продолжения.'; - - @override - String get errorEmailNotRegistered => - 'Мы не нашли аккаунт, связанный с этим адресом электронной почты.'; - - @override - String get errorDuplicateEmail => - 'Этот адрес электронной почты уже используется. Попробуйте другой или восстановите пароль.'; - - @override - String get showDetailsAction => 'Показать детали'; - - @override - String get errorLogin => 'Ошибка входа'; - - @override - String get errorCreatingInvitation => 'Не удалось создать приглашение'; - - @override - String get footerCompanyName => 'Sibilla Solutions LTD'; - - @override - String get footerAddress => - '27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus'; - - @override - String get footerSupport => 'Поддержка'; - - @override - String get footerEmail => 'Email TBD'; - - @override - String get footerPhoneLabel => 'Телефон'; - - @override - String get footerPhone => '+357 22 000 253'; - - @override - String get footerTermsOfService => 'Условия обслуживания'; - - @override - String get footerPrivacyPolicy => 'Политика конфиденциальности'; - - @override - String get footerCookiePolicy => 'Политика использования файлов cookie'; - - @override - String get navigationLogout => 'Выйти'; - - @override - String get dashboard => 'Дашборд'; - - @override - String get navigationUsersSettings => 'Пользователи'; - - @override - String get navigationRolesSettings => 'Роли'; - - @override - String get navigationPermissionsSettings => 'Разрешения'; - - @override - String get usersManagement => 'Управление пользователями'; - - @override - String get navigationOrganizationSettings => 'Настройки организации'; - - @override - String get navigationAccountSettings => 'Настройки профиля'; - - @override - String get twoFactorPrompt => - 'Введите 6-значный код, отправленный на ваше устройство'; - - @override - String get twoFactorResend => 'Не получили код? Отправить снова'; - - @override - String get twoFactorTitle => 'Двухфакторная аутентификация'; - - @override - String get twoFactorError => 'Неверный код. Пожалуйста, попробуйте снова.'; - - @override - String get payoutNavDashboard => 'Дашборд'; - - @override - String get payoutNavSendPayout => 'Отправить выплату'; - - @override - String get payoutNavRecipients => 'Получатели'; - - @override - String get payoutNavReports => 'Отчеты'; - - @override - String get payoutNavSettings => 'Настройки'; - - @override - String get payoutNavLogout => 'Выйти'; - - @override - String get payoutNavMethods => 'Выплаты'; - - @override - String get expand => 'Развернуть'; - - @override - String get collapse => 'Свернуть'; - - @override - String get pageTitleRecipients => 'Адресная книга получателей'; - - @override - String get actionAddNew => 'Добавить'; - - @override - String get colDataOwner => 'Владелец данных'; - - @override - String get colAvatar => 'Аватар'; - - @override - String get colName => 'Имя'; - - @override - String get colEmail => 'Email'; - - @override - String get colStatus => 'Статус'; - - @override - String get statusReady => 'Готов'; - - @override - String get statusRegistered => 'Зарегистрирован'; - - @override - String get statusNotRegistered => 'Не зарегистрирован'; - - @override - String get typeInternal => 'Управляется мной'; - - @override - String get typeExternal => 'Самоуправляемый'; - - @override - String get searchHint => 'Поиск получателей'; - - @override - String get colActions => 'Действия'; - - @override - String get menuEdit => 'Редактировать'; - - @override - String get menuSendPayout => 'Отправить выплату'; - - @override - String get tooltipRowActions => 'Другие действия'; - - @override - String get accountSettings => 'Настройки аккаунта'; - - @override - String get accountNameUpdateError => 'Не удалось обновить имя аккаунта'; - - @override - String get settingsSuccessfullyUpdated => 'Настройки успешно обновлены'; - - @override - String get language => 'Язык'; - - @override - String get failedToUpdateLanguage => 'Не удалось обновить язык'; - - @override - String get settingsImageUpdateError => 'Не удалось обновить изображение'; - - @override - String get settingsImageTitle => 'Изображение'; - - @override - String get settingsImageHint => 'Нажмите, чтобы изменить изображение'; - - @override - String get accountName => 'Имя'; - - @override - String get accountNameHint => 'Укажите ваше имя'; - - @override - String get avatar => 'Фото профиля'; - - @override - String get avatarHint => 'Нажмите для обновления'; - - @override - String get avatarUpdateError => 'Не удалось обновить фото профиля'; - - @override - String get settings => 'Настройки'; - - @override - String get notSet => 'не задано'; - - @override - String get search => 'Поиск...'; - - @override - String get ok => 'Ок'; - - @override - String get cancel => 'Отмена'; - - @override - String get confirm => 'Подтвердить'; - - @override - String get back => 'Назад'; - - @override - String get operationfryTitle => 'История операций'; - - @override - String get filters => 'Фильтры'; - - @override - String get period => 'Период'; - - @override - String get selectPeriod => 'Выберите период'; - - @override - String get apply => 'Применить'; - - @override - String status(String status) { - return '$status'; - } - - @override - String get operationStatusSuccessful => 'Успешно'; - - @override - String get operationStatusPending => 'В ожидании'; - - @override - String get operationStatusUnsuccessful => 'Неуспешно'; - - @override - String get statusColumn => 'Статус'; - - @override - String get fileNameColumn => 'Имя файла'; - - @override - String get amountColumn => 'Сумма'; - - @override - String get toAmountColumn => 'На сумму'; - - @override - String get payIdColumn => 'Pay ID'; - - @override - String get cardNumberColumn => 'Номер карты'; - - @override - String get nameColumn => 'Имя'; - - @override - String get dateColumn => 'Дата'; - - @override - String get commentColumn => 'Комментарий'; - - @override - String get paymentConfigTitle => 'Куда получать деньги'; - - @override - String get paymentConfigSubtitle => - 'Добавьте несколько методов и выберите основной.'; - - @override - String get addPaymentMethod => 'Добавить способ оплаты'; - - @override - String get makeMain => 'Сделать основным'; - - @override - String get advanced => 'Дополнительно'; - - @override - String get fallbackExplanation => - 'Если основной метод недоступен, мы попробуем следующий включенный метод в списке.'; - - @override - String get delete => 'Удалить'; - - @override - String get deletePaymentConfirmation => - 'Вы уверены, что хотите удалить этот способ оплаты?'; - - @override - String get edit => 'Редактировать'; - - @override - String get moreActions => 'Еще действия'; - - @override - String get noPayouts => 'Нет выплат'; - - @override - String get enterBankName => 'Введите название банка'; - - @override - String get paymentType => 'Тип способа оплаты'; - - @override - String get selectPaymentType => 'Пожалуйста, выберите тип способа оплаты'; - - @override - String get paymentTypeCard => 'Кредитная карта'; - - @override - String get paymentTypeBankAccount => 'Российский банковский счет'; - - @override - String get paymentTypeIban => 'IBAN'; - - @override - String get paymentTypeWallet => 'Кошелек'; - - @override - String get cardNumber => 'Номер карты'; - - @override - String get enterCardNumber => 'Введите номер карты'; - - @override - String get cardholderName => 'Имя держателя карты'; - - @override - String get iban => 'IBAN'; - - @override - String get enterIban => 'Введите IBAN'; - - @override - String get bic => 'BIC'; - - @override - String get bankName => 'Название банка'; - - @override - String get accountHolder => 'Владелец счета'; - - @override - String get enterAccountHolder => 'Введите владельца счета'; - - @override - String get enterBic => 'Введите BIC'; - - @override - String get walletId => 'ID кошелька'; - - @override - String get enterWalletId => 'Введите ID кошелька'; - - @override - String get recipients => 'Получатели'; - - @override - String get recipientName => 'Имя получателя'; - - @override - String get enterRecipientName => 'Введите имя получателя'; - - @override - String get inn => 'ИНН'; - - @override - String get enterInn => 'Введите ИНН'; - - @override - String get kpp => 'КПП'; - - @override - String get enterKpp => 'Введите КПП'; - - @override - String get accountNumber => 'Номер счета'; - - @override - String get enterAccountNumber => 'Введите номер счета'; - - @override - String get correspondentAccount => 'Корреспондентский счет'; - - @override - String get enterCorrespondentAccount => 'Введите корреспондентский счет'; - - @override - String get bik => 'БИК'; - - @override - String get enterBik => 'Введите БИК'; - - @override - String get add => 'Добавить'; - - @override - String get expiryDate => 'Срок действия (ММ/ГГ)'; - - @override - String get firstName => 'Имя'; - - @override - String get enterFirstName => 'Введите имя'; - - @override - String get lastName => 'Фамилия'; - - @override - String get enterLastName => 'Введите фамилию'; - - @override - String get sendSingle => 'Отправить одну транзакцию'; - - @override - String get sendMultiple => 'Отправить несколько транзакций'; - - @override - String get addFunds => 'Пополнить счет'; - - @override - String get close => 'Закрыть'; - - @override - String get multiplePayout => 'Множественная выплата'; - - @override - String get howItWorks => 'Как это работает?'; - - @override - String get exampleTitle => 'Формат файла и образец'; - - @override - String get downloadSampleCSV => 'Скачать sample.csv'; - - @override - String get tokenColumn => 'Токен (обязательно)'; - - @override - String get currency => 'Валюта'; - - @override - String get amount => 'Сумма'; - - @override - String get comment => 'Комментарий'; - - @override - String get uploadCSV => 'Загрузите ваш CSV'; - - @override - String get upload => 'Загрузить'; - - @override - String get hintUpload => 'Поддерживаемый формат: .CSV · Макс. размер 1 МБ'; - - @override - String get uploadHistory => 'История загрузок'; - - @override - String get payout => 'Выплата'; - - @override - String get sendTo => 'Отправить выплату'; - - @override - String get send => 'Отправить выплату'; - - @override - String get recipientPaysFee => 'Получатель оплачивает комиссию'; - - @override - String sentAmount(String amount) { - return 'Отправленная сумма: \$$amount'; - } - - @override - String fee(String fee) { - return 'Комиссия: \$$fee'; - } - - @override - String recipientWillReceive(String amount) { - return 'Получатель получит: \$$amount'; - } - - @override - String total(String total) { - return 'Итого: \$$total'; - } - - @override - String get hideDetails => 'Скрыть детали'; - - @override - String get showDetails => 'Показать детали'; - - @override - String get whereGetMoney => 'Источник средств для списания'; - - @override - String get details => 'Детали'; - - @override - String get addRecipient => 'Добавить получателя'; - - @override - String get editRecipient => 'Редактировать получателя'; - - @override - String get saveRecipient => 'Сохранить получателя'; - - @override - String get choosePaymentMethod => 'Способы оплаты (выберите хотя бы 1)'; - - @override - String get recipientFormRule => - 'Получатель должен иметь хотя бы один способ оплаты'; - - @override - String get allStatus => 'Все'; - - @override - String get readyStatus => 'Готов'; - - @override - String get registeredStatus => 'Зарегистрирован'; - - @override - String get notRegisteredStatus => 'Не зарегистрирован'; - - @override - String get noRecipientSelected => 'Получатель не выбран'; - - @override - String get companyName => 'Name of your company'; - - @override - String get companynameRequired => 'Company name required'; - - @override - String get errorSignUp => 'Error occured while signing up, try again later'; - - @override - String get companyDescription => 'Company Description'; - - @override - String get companyDescriptionHint => - 'Describe any of the fields of the Company\'s business'; - - @override - String get optional => 'optional'; -} diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index b152d02..b5d53d1 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -431,5 +431,7 @@ "errorSignUp": "Error occured while signing up, try again later", "companyDescription": "Company Description", "companyDescriptionHint": "Describe any of the fields of the Company's business", - "optional": "optional" + "optional": "optional", + "ownerRole": "Organization Owner", + "ownerRoleDescription": "This role is granted to the organization’s creator, providing full administrative privileges" } \ No newline at end of file diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index def1290..5303005 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -422,5 +422,8 @@ "registeredStatus": "Зарегистрирован", "notRegisteredStatus": "Не зарегистрирован", - "noRecipientSelected": "Получатель не выбран" + "noRecipientSelected": "Получатель не выбран", + + "ownerRole": "Владелец организации", + "ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права" } \ No newline at end of file diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index 0ee7f60..3eaca66 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -11,7 +11,6 @@ import 'package:pshared/config/constants.dart'; import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/locale.dart'; import 'package:pshared/provider/organizations.dart'; -import 'package:pshared/provider/pfe/provider.dart'; import 'package:pweb/app/app.dart'; import 'package:pweb/app/timeago.dart'; @@ -66,7 +65,7 @@ void main() async { ChangeNotifierProvider(create: (_) => LocaleProvider(null)), ChangeNotifierProvider(create: (_) => AccountProvider()), ChangeNotifierProvider(create: (_) => OrganizationsProvider()), - ChangeNotifierProvider(create: (_) => PfeProvider()), + ChangeNotifierProvider(create: (_) => AccountProvider()), ChangeNotifierProvider(create: (_) => CarouselIndexProvider()), ChangeNotifierProvider( diff --git a/frontend/pweb/lib/pages/login/form.dart b/frontend/pweb/lib/pages/login/form.dart index e221ea6..a145cb6 100644 --- a/frontend/pweb/lib/pages/login/form.dart +++ b/frontend/pweb/lib/pages/login/form.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/pfe/provider.dart'; +import 'package:pshared/provider/account.dart'; +import 'package:pshared/provider/locale.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/pages/login/buttons.dart'; @@ -34,17 +35,19 @@ class _LoginFormState extends State { final ValueNotifier _isPasswordAcceptable = ValueNotifier(false); Future _login(BuildContext context, VoidCallback onLogin, void Function(Object e) onError) async { - final pfeProvider = Provider.of(context, listen: false); + final provider = Provider.of(context, listen: false); try { - // final account = await pfeProvider.login( - // email: _usernameController.text, - // password: _passwordController.text, - // ); + //final account = + await provider.login( + email: _usernameController.text, + password: _passwordController.text, + locale: context.read().locale.languageCode, + ); onLogin(); return 'ok'; } catch (e) { - onError(pfeProvider.error == null ? e : pfeProvider.error!); + onError(provider.error ?? e); } return null; } diff --git a/frontend/pweb/lib/pages/signup/form/state.dart b/frontend/pweb/lib/pages/signup/form/state.dart index a25ec84..38f651d 100644 --- a/frontend/pweb/lib/pages/signup/form/state.dart +++ b/frontend/pweb/lib/pages/signup/form/state.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_timezone/flutter_timezone.dart'; + import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/pfe/provider.dart'; +import 'package:pshared/api/requests/login_data.dart'; +import 'package:pshared/models/describable.dart'; +import 'package:pshared/provider/account.dart'; +import 'package:pshared/provider/locale.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/pages/signup/form/content.dart'; @@ -31,7 +36,7 @@ class SignUpFormState extends State { VoidCallback onSignUp, void Function(Object e) onError, ) async { - final pfeProvider = Provider.of(context, listen: false); + final provider = Provider.of(context, listen: false); setState(() { _autoValidateMode = true; @@ -42,20 +47,34 @@ class SignUpFormState extends State { } try { - // final account = await pfeProvider.signUp( - // companyName: controllers.companyName.text.trim(), - // description: controllers.description.text.trim().isEmpty - // ? null - // : controllers.description.text.trim(), - // firstName: controllers.firstName.text.trim(), - // lastName: controllers.lastName.text.trim(), - // email: controllers.email.text.trim(), - // password: controllers.password.text, - // ); + final orgDescription = controllers.description.text.trim(); + final locs = AppLocalizations.of(context)!; + final locale = context.read().locale; + final timezone = await FlutterTimezone.getLocalTimezone(locale.toString()); + await provider.signup( + account: AccountData.build( + login: LoginData.build( + login: controllers.email.text.trim(), + password: controllers.password.text, + locale: locale.toLanguageTag(), + ), + name: controllers.password.text, + lastName: controllers.lastName.text.trim(), + ), + organization: newDescribable( + name: controllers.companyName.text.trim(), + description: orgDescription.isEmpty ? null : orgDescription, + ), + timezone: timezone.identifier, + ownerRole: newDescribable( + name: locs.ownerRole, + description: locs.ownerRoleDescription, + ), + ); onSignUp(); return 'ok'; } catch (e) { - onError(pfeProvider.error ?? e); + onError(provider.error ?? e); } return null; } diff --git a/version b/version index ac346d4..b621109 100644 --- a/version +++ b/version @@ -1 +1 @@ -2.0.856 \ No newline at end of file +2.0.857 \ No newline at end of file From 9dbf77a9a829457c5dcfdc266c294d9de0c17800 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Mon, 17 Nov 2025 22:20:17 +0100 Subject: [PATCH 03/32] +notification from site +version bump fix --- .woodpecker/notification.yml | 1 + api/notification/config.yml | 13 +- .../services/notification/config/config.go | 14 +- .../server/notificationimp/notification.go | 33 ++++ .../server/notificationimp/telegram/client.go | 149 ++++++++++++++++++ .../notifications/site/notification.go | 47 ++++++ .../notifications/site/demo_request.go | 11 ++ .../notifications/site/handler/handler.go | 9 ++ .../messaging/notifications/site/processor.go | 50 ++++++ api/pkg/model/demorequest.go | 53 +++++++ api/pkg/model/demorequest_test.go | 31 ++++ api/pkg/mservice/services.go | 3 +- api/proto/demo_request.proto | 12 ++ api/server/interface/services/site/site.go | 11 ++ api/server/internal/api/api.go | 2 + api/server/internal/server/siteimp/service.go | 60 +++++++ ci/prod/.env.runtime | 4 +- ci/prod/compose/notification.yml | 3 + ci/prod/scripts/deploy/notification.sh | 12 ++ ci/scripts/common/bump_version.sh | 25 ++- ci/scripts/notification/deploy.sh | 9 ++ 21 files changed, 543 insertions(+), 9 deletions(-) create mode 100644 api/notification/internal/server/notificationimp/telegram/client.go create mode 100644 api/pkg/messaging/internal/notifications/site/notification.go create mode 100644 api/pkg/messaging/notifications/site/demo_request.go create mode 100644 api/pkg/messaging/notifications/site/handler/handler.go create mode 100644 api/pkg/messaging/notifications/site/processor.go create mode 100644 api/pkg/model/demorequest.go create mode 100644 api/pkg/model/demorequest_test.go create mode 100644 api/proto/demo_request.proto create mode 100644 api/server/interface/services/site/site.go create mode 100644 api/server/internal/server/siteimp/service.go diff --git a/.woodpecker/notification.yml b/.woodpecker/notification.yml index db4133c..1a586f2 100644 --- a/.woodpecker/notification.yml +++ b/.woodpecker/notification.yml @@ -5,6 +5,7 @@ matrix: NOTIFICATION_MONGO_SECRET_PATH: sendico/db NOTIFICATION_MAIL_SECRET_PATH: sendico/notification/mail NOTIFICATION_API_SECRET_PATH: sendico/api/endpoint + NOTIFICATION_TELEGRAM_SECRET_PATH: sendico/notification/telegram NOTIFICATION_ENV: prod when: diff --git a/api/notification/config.yml b/api/notification/config.yml index bbbd57b..7e7c8d7 100755 --- a/api/notification/config.yml +++ b/api/notification/config.yml @@ -53,14 +53,21 @@ api: password_env: MAIL_SECRET host: "smtp.mail.ru" port: 465 - from: "MeetX Tech" + from: "Sendico Tech" network_timeout: 10 + telegram: + bot_token_env: TELEGRAM_BOT_TOKEN + chat_id_env: TELEGRAM_CHAT_ID + thread_id_env: TELEGRAM_THREAD_ID + api_url: "https://api.telegram.org" + timeout_seconds: 10 + parse_mode: "" localizer: path: "./i18n" languages: ["en", "ru", "uk"] service_name: "Sendico" - support: "support@meetx.space" + support: "support@sendico.io" app: @@ -82,4 +89,4 @@ database: collection_name_env: PERMISSION_COLLECTION database_name_env: MONGO_DATABASE timeout_seconds_env: PERMISSION_TIMEOUT - is_filtered_env: PERMISSION_IS_FILTERED \ No newline at end of file + is_filtered_env: PERMISSION_IS_FILTERED diff --git a/api/notification/interface/services/notification/config/config.go b/api/notification/interface/services/notification/config/config.go index 2e34fd9..61d576c 100644 --- a/api/notification/interface/services/notification/config/config.go +++ b/api/notification/interface/services/notification/config/config.go @@ -1,6 +1,16 @@ package notificationimp type Config struct { - Driver string `yaml:"driver"` - Settings map[string]any `yaml:"settings,omitempty"` + Driver string `yaml:"driver"` + Settings map[string]any `yaml:"settings,omitempty"` + Telegram *TelegramConfig `yaml:"telegram"` +} + +type TelegramConfig struct { + BotTokenEnv string `yaml:"bot_token_env"` + ChatIDEnv string `yaml:"chat_id_env"` + ThreadIDEnv string `yaml:"thread_id_env,omitempty"` + APIURL string `yaml:"api_url,omitempty"` + ParseMode string `yaml:"parse_mode,omitempty"` + TimeoutSeconds int `yaml:"timeout_seconds"` } diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index e6d8f5b..35913b4 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -2,13 +2,17 @@ package notificationimp import ( "context" + "fmt" "github.com/tech/sendico/notification/interface/api" mmail "github.com/tech/sendico/notification/internal/server/notificationimp/mail" + "github.com/tech/sendico/notification/internal/server/notificationimp/telegram" "github.com/tech/sendico/pkg/domainprovider" na "github.com/tech/sendico/pkg/messaging/notifications/account" ni "github.com/tech/sendico/pkg/messaging/notifications/invitation" + snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" "go.uber.org/zap" ) @@ -17,6 +21,7 @@ type NotificationAPI struct { logger mlogger.Logger client mmail.Client dp domainprovider.DomainProvider + tg telegram.Client } func (a *NotificationAPI) Name() mservice.Type { @@ -33,11 +38,22 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { } p.logger = a.Logger().Named(p.Name()) + if a.Config().Notification == nil { + return nil, fmt.Errorf("notification configuration is missing") + } + if a.Config().Notification.Telegram == nil { + return nil, fmt.Errorf("telegram configuration is missing") + } + var err error if p.client, err = mmail.CreateMailClient(p.logger.Named("mailer"), p.Name(), a.Register().Producer(), a.Localizer(), a.DomainProvider(), a.Config().Notification); err != nil { p.logger.Error("Failed to create mail connection", zap.Error(err), zap.String("driver", a.Config().Notification.Driver)) return nil, err } + if p.tg, err = telegram.NewClient(p.logger.Named("telegram"), a.Config().Notification.Telegram); err != nil { + p.logger.Error("Failed to create telegram client", zap.Error(err)) + return nil, err + } db, err := a.DBFactory().NewAccountDB() if err != nil { @@ -64,5 +80,22 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { return nil, err } + if err := a.Register().Consumer(snotifications.NewDemoRequestProcessor(p.logger, p.onDemoRequest)); err != nil { + p.logger.Error("Failed to register demo request handler", zap.Error(err)) + return nil, err + } + return p, nil } + +func (a *NotificationAPI) onDemoRequest(ctx context.Context, request *model.DemoRequest) error { + if a.tg == nil { + return fmt.Errorf("telegram client is not configured") + } + if err := a.tg.SendDemoRequest(ctx, request); err != nil { + a.logger.Warn("Failed to send demo request via telegram", zap.Error(err)) + return err + } + a.logger.Info("Demo request sent via Telegram", zap.String("name", request.Name), zap.String("organization", request.OrganizationName)) + return nil +} diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go new file mode 100644 index 0000000..d91c7b9 --- /dev/null +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -0,0 +1,149 @@ +package telegram + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + "time" + + notconfig "github.com/tech/sendico/notification/interface/services/notification/config" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" +) + +const defaultAPIURL = "https://api.telegram.org" + +type Client interface { + SendDemoRequest(ctx context.Context, request *model.DemoRequest) error +} + +type client struct { + logger mlogger.Logger + httpClient *http.Client + apiURL string + botToken string + chatID string + threadID *int64 + parseMode string +} + +type sendMessagePayload struct { + ChatID string `json:"chat_id"` + Text string `json:"text"` + ParseMode string `json:"parse_mode,omitempty"` + ThreadID *int64 `json:"message_thread_id,omitempty"` + DisablePreview bool `json:"disable_web_page_preview,omitempty"` + DisableNotify bool `json:"disable_notification,omitempty"` + ProtectContent bool `json:"protect_content,omitempty"` +} + +func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, error) { + if cfg == nil { + return nil, fmt.Errorf("telegram configuration is not provided") + } + token := strings.TrimSpace(os.Getenv(cfg.BotTokenEnv)) + if token == "" { + return nil, fmt.Errorf("telegram bot token env %s is empty", cfg.BotTokenEnv) + } + chatID := strings.TrimSpace(os.Getenv(cfg.ChatIDEnv)) + if chatID == "" { + return nil, fmt.Errorf("telegram chat id env %s is empty", cfg.ChatIDEnv) + } + + var threadID *int64 + if env := strings.TrimSpace(cfg.ThreadIDEnv); env != "" { + raw := strings.TrimSpace(os.Getenv(env)) + if raw != "" { + val, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return nil, fmt.Errorf("telegram thread id env %s is invalid: %w", env, err) + } + threadID = &val + } + } + + timeout := time.Duration(cfg.TimeoutSeconds) * time.Second + if timeout <= 0 { + timeout = 10 * time.Second + } + + apiURL := strings.TrimSpace(cfg.APIURL) + if apiURL == "" { + apiURL = defaultAPIURL + } + + return &client{ + logger: logger.Named("telegram"), + httpClient: &http.Client{ + Timeout: timeout, + }, + apiURL: strings.TrimRight(apiURL, "/"), + botToken: token, + chatID: chatID, + threadID: threadID, + parseMode: strings.TrimSpace(cfg.ParseMode), + }, nil +} + +func (c *client) SendDemoRequest(ctx context.Context, request *model.DemoRequest) error { + if request == nil { + return fmt.Errorf("demo request payload is nil") + } + message := buildMessage(request) + payload := sendMessagePayload{ + ChatID: c.chatID, + Text: message, + ParseMode: c.parseMode, + ThreadID: c.threadID, + DisablePreview: true, + } + return c.sendMessage(ctx, payload) +} + +func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) error { + body, err := json.Marshal(&payload) + if err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint(), bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { + return nil + } + respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10)) + return fmt.Errorf("telegram sendMessage failed with status %d: %s", resp.StatusCode, string(respBody)) +} + +func (c *client) endpoint() string { + return fmt.Sprintf("%s/bot%s/sendMessage", c.apiURL, c.botToken) +} + +func buildMessage(req *model.DemoRequest) string { + var builder strings.Builder + builder.WriteString("New demo request received\n") + builder.WriteString(fmt.Sprintf("Name: %s\n", req.Name)) + builder.WriteString(fmt.Sprintf("Organization: %s\n", req.OrganizationName)) + builder.WriteString(fmt.Sprintf("Phone: %s\n", req.Phone)) + builder.WriteString(fmt.Sprintf("Work email: %s\n", req.WorkEmail)) + builder.WriteString(fmt.Sprintf("Payout volume: %s\n", req.PayoutVolume)) + if req.Comment != "" { + builder.WriteString(fmt.Sprintf("Comment: %s\n", req.Comment)) + } + return builder.String() +} diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go new file mode 100644 index 0000000..0225c23 --- /dev/null +++ b/api/pkg/messaging/internal/notifications/site/notification.go @@ -0,0 +1,47 @@ +package notifications + +import ( + "fmt" + + gmessaging "github.com/tech/sendico/pkg/generated/gmessaging" + messaging "github.com/tech/sendico/pkg/messaging/envelope" + "github.com/tech/sendico/pkg/model" + nm "github.com/tech/sendico/pkg/model/notification" + "github.com/tech/sendico/pkg/mservice" + "google.golang.org/protobuf/proto" +) + +type DemoRequestNotification struct { + messaging.Envelope + request *model.DemoRequest +} + +func (drn *DemoRequestNotification) Serialize() ([]byte, error) { + if drn.request == nil { + return nil, fmt.Errorf("demo request payload is empty") + } + msg := gmessaging.DemoRequestEvent{ + Name: drn.request.Name, + OrganizationName: drn.request.OrganizationName, + Phone: drn.request.Phone, + WorkEmail: drn.request.WorkEmail, + PayoutVolume: drn.request.PayoutVolume, + Comment: drn.request.Comment, + } + data, err := proto.Marshal(&msg) + if err != nil { + return nil, err + } + return drn.Envelope.Wrap(data) +} + +func NewDemoRequestEvent() model.NotificationEvent { + return model.NewNotification(mservice.Site, nm.NACreated) +} + +func NewDemoRequestEnvelope(sender string, request *model.DemoRequest) messaging.Envelope { + return &DemoRequestNotification{ + Envelope: messaging.CreateEnvelope(sender, NewDemoRequestEvent()), + request: request, + } +} diff --git a/api/pkg/messaging/notifications/site/demo_request.go b/api/pkg/messaging/notifications/site/demo_request.go new file mode 100644 index 0000000..bb21277 --- /dev/null +++ b/api/pkg/messaging/notifications/site/demo_request.go @@ -0,0 +1,11 @@ +package notifications + +import ( + messaging "github.com/tech/sendico/pkg/messaging/envelope" + internalsite "github.com/tech/sendico/pkg/messaging/internal/notifications/site" + "github.com/tech/sendico/pkg/model" +) + +func DemoRequest(sender string, request *model.DemoRequest) messaging.Envelope { + return internalsite.NewDemoRequestEnvelope(sender, request) +} diff --git a/api/pkg/messaging/notifications/site/handler/handler.go b/api/pkg/messaging/notifications/site/handler/handler.go new file mode 100644 index 0000000..ef1b3ef --- /dev/null +++ b/api/pkg/messaging/notifications/site/handler/handler.go @@ -0,0 +1,9 @@ +package notifications + +import ( + "context" + + "github.com/tech/sendico/pkg/model" +) + +type DemoRequestHandler = func(context.Context, *model.DemoRequest) error diff --git a/api/pkg/messaging/notifications/site/processor.go b/api/pkg/messaging/notifications/site/processor.go new file mode 100644 index 0000000..33e5eab --- /dev/null +++ b/api/pkg/messaging/notifications/site/processor.go @@ -0,0 +1,50 @@ +package notifications + +import ( + "context" + + gmessaging "github.com/tech/sendico/pkg/generated/gmessaging" + me "github.com/tech/sendico/pkg/messaging/envelope" + internalsite "github.com/tech/sendico/pkg/messaging/internal/notifications/site" + np "github.com/tech/sendico/pkg/messaging/notifications/processor" + handler "github.com/tech/sendico/pkg/messaging/notifications/site/handler" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" +) + +type DemoRequestProcessor struct { + logger mlogger.Logger + handler handler.DemoRequestHandler + event model.NotificationEvent +} + +func (drp *DemoRequestProcessor) Process(ctx context.Context, envelope me.Envelope) error { + var msg gmessaging.DemoRequestEvent + if err := proto.Unmarshal(envelope.GetData(), &msg); err != nil { + drp.logger.Warn("Failed to decode demo request envelope", zap.Error(err), zap.String("topic", drp.event.ToString())) + return err + } + request := &model.DemoRequest{ + Name: msg.GetName(), + OrganizationName: msg.GetOrganizationName(), + Phone: msg.GetPhone(), + WorkEmail: msg.GetWorkEmail(), + PayoutVolume: msg.GetPayoutVolume(), + Comment: msg.GetComment(), + } + return drp.handler(ctx, request) +} + +func (drp *DemoRequestProcessor) GetSubject() model.NotificationEvent { + return drp.event +} + +func NewDemoRequestProcessor(logger mlogger.Logger, handler handler.DemoRequestHandler) np.EnvelopeProcessor { + return &DemoRequestProcessor{ + logger: logger.Named("demo_request_processor"), + handler: handler, + event: internalsite.NewDemoRequestEvent(), + } +} diff --git a/api/pkg/model/demorequest.go b/api/pkg/model/demorequest.go new file mode 100644 index 0000000..007b357 --- /dev/null +++ b/api/pkg/model/demorequest.go @@ -0,0 +1,53 @@ +package model + +import ( + "strings" + + "github.com/tech/sendico/pkg/merrors" +) + +// DemoRequest represents a request submitted from the marketing site to request a demo. +type DemoRequest struct { + Name string `json:"name"` + OrganizationName string `json:"organizationName"` + Phone string `json:"phone"` + WorkEmail string `json:"workEmail"` + PayoutVolume string `json:"payoutVolume"` + Comment string `json:"comment,omitempty"` +} + +// Normalize trims whitespace from all string fields. +func (dr *DemoRequest) Normalize() { + if dr == nil { + return + } + dr.Name = strings.TrimSpace(dr.Name) + dr.OrganizationName = strings.TrimSpace(dr.OrganizationName) + dr.Phone = strings.TrimSpace(dr.Phone) + dr.WorkEmail = strings.TrimSpace(dr.WorkEmail) + dr.PayoutVolume = strings.TrimSpace(dr.PayoutVolume) + dr.Comment = strings.TrimSpace(dr.Comment) +} + +// Validate ensures that all required fields are present. +func (dr *DemoRequest) Validate() error { + if dr == nil { + return merrors.InvalidArgument("request payload is empty") + } + if dr.Name == "" { + return merrors.InvalidArgument("name must not be empty") + } + if dr.OrganizationName == "" { + return merrors.InvalidArgument("organization name must not be empty") + } + if dr.Phone == "" { + return merrors.InvalidArgument("phone must not be empty") + } + if dr.WorkEmail == "" { + return merrors.InvalidArgument("work email must not be empty") + } + if dr.PayoutVolume == "" { + return merrors.InvalidArgument("payout volume must not be empty") + } + return nil +} diff --git a/api/pkg/model/demorequest_test.go b/api/pkg/model/demorequest_test.go new file mode 100644 index 0000000..428f386 --- /dev/null +++ b/api/pkg/model/demorequest_test.go @@ -0,0 +1,31 @@ +package model + +import "testing" + +func TestDemoRequestNormalizeAndValidate(t *testing.T) { + req := &DemoRequest{ + Name: " Alice ", + OrganizationName: " Sendico ", + Phone: " +1 234 ", + WorkEmail: " demo@sendico.io ", + PayoutVolume: " 100k ", + Comment: " Excited ", + } + + req.Normalize() + if err := req.Validate(); err != nil { + t.Fatalf("expected request to be valid, got error: %v", err) + } + + if req.Name != "Alice" || req.OrganizationName != "Sendico" || req.Phone != "+1 234" || req.WorkEmail != "demo@sendico.io" || req.PayoutVolume != "100k" || req.Comment != "Excited" { + t.Fatalf("normalize failed: %+v", req) + } +} + +func TestDemoRequestValidateMissing(t *testing.T) { + req := &DemoRequest{} + req.Normalize() + if err := req.Validate(); err == nil { + t.Fatalf("expected validation error for empty request") + } +} diff --git a/api/pkg/mservice/services.go b/api/pkg/mservice/services.go index 9154426..be4e30f 100644 --- a/api/pkg/mservice/services.go +++ b/api/pkg/mservice/services.go @@ -7,6 +7,7 @@ type Type = string const ( Accounts Type = "accounts" // Represents user accounts in the system Amplitude Type = "amplitude" // Represents analytics integration with Amplitude + Site Type = "site" // Represents public site endpoints Automations Type = "automation" // Represents automation workflows Changes Type = "changes" // Tracks changes made to resources Clients Type = "clients" // Represents client information @@ -59,7 +60,7 @@ const ( func StringToSType(s string) (Type, error) { switch Type(s) { - case Accounts, Amplitude, Automations, Changes, Clients, Comments, ChainGateway, ChainWallets, ChainWalletBalances, + case Accounts, Amplitude, Site, Automations, Changes, Clients, Comments, ChainGateway, ChainWallets, ChainWalletBalances, ChainTransfers, ChainDeposits, FXOracle, FeePlans, FilterProjects, Invitations, Invoices, Logo, Ledger, LedgerAccounts, LedgerBalances, LedgerEntries, LedgerOutbox, LedgerParties, LedgerPlines, Notifications, Organizations, Payments, PaymentOrchestrator, Permissions, Policies, PolicyAssignements, Priorities, diff --git a/api/proto/demo_request.proto b/api/proto/demo_request.proto new file mode 100644 index 0000000..68aeea5 --- /dev/null +++ b/api/proto/demo_request.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +option go_package = "github.com/tech/sendico/pkg/generated/gmessaging"; + +message DemoRequestEvent { + string Name = 1; + string OrganizationName = 2; + string Phone = 3; + string WorkEmail = 4; + string PayoutVolume = 5; + string Comment = 6; +} diff --git a/api/server/interface/services/site/site.go b/api/server/interface/services/site/site.go new file mode 100644 index 0000000..5b1ff53 --- /dev/null +++ b/api/server/interface/services/site/site.go @@ -0,0 +1,11 @@ +package site + +import ( + "github.com/tech/sendico/pkg/mservice" + eapi "github.com/tech/sendico/server/interface/api" + "github.com/tech/sendico/server/internal/server/siteimp" +) + +func Create(a eapi.API) (mservice.MicroService, error) { + return siteimp.CreateAPI(a) +} diff --git a/api/server/internal/api/api.go b/api/server/internal/api/api.go index 6a255ec..822e514 100644 --- a/api/server/internal/api/api.go +++ b/api/server/internal/api/api.go @@ -16,6 +16,7 @@ import ( "github.com/tech/sendico/server/interface/services/logo" "github.com/tech/sendico/server/interface/services/organization" "github.com/tech/sendico/server/interface/services/permission" + "github.com/tech/sendico/server/interface/services/site" "go.uber.org/zap" ) @@ -79,6 +80,7 @@ func (a *APIImp) installServices() error { srvf = append(srvf, invitation.Create) srvf = append(srvf, logo.Create) srvf = append(srvf, permission.Create) + srvf = append(srvf, site.Create) for _, v := range srvf { if err := a.addMicroservice(v); err != nil { diff --git a/api/server/internal/server/siteimp/service.go b/api/server/internal/server/siteimp/service.go new file mode 100644 index 0000000..fce8434 --- /dev/null +++ b/api/server/internal/server/siteimp/service.go @@ -0,0 +1,60 @@ +package siteimp + +import ( + "context" + "encoding/json" + "net/http" + + api "github.com/tech/sendico/pkg/api/http" + "github.com/tech/sendico/pkg/api/http/response" + "github.com/tech/sendico/pkg/messaging" + snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + eapi "github.com/tech/sendico/server/interface/api" + "go.uber.org/zap" +) + +type SiteAPI struct { + logger mlogger.Logger + producer messaging.Producer +} + +func (a *SiteAPI) Name() mservice.Type { + return mservice.Site +} + +func (a *SiteAPI) Finish(_ context.Context) error { + return nil +} + +func (a *SiteAPI) demoRequest(r *http.Request) http.HandlerFunc { + var request model.DemoRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + a.logger.Warn("Failed to decode demo request payload", zap.Error(err)) + return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode demo request payload") + } + request.Normalize() + if err := request.Validate(); err != nil { + a.logger.Warn("Demo request validation failed", zap.Error(err)) + return response.BadPayload(a.logger, a.Name(), err) + } + + if err := a.producer.SendMessage(snotifications.DemoRequest(a.Name(), &request)); err != nil { + a.logger.Warn("Failed to enqueue demo request notification", zap.Error(err)) + return response.Internal(a.logger, a.Name(), err) + } + + return response.Accepted(a.logger, map[string]string{"status": "queued"}) +} + +func CreateAPI(a eapi.API) (*SiteAPI, error) { + p := &SiteAPI{ + logger: a.Logger().Named(mservice.Site), + producer: a.Register().Messaging().Producer(), + } + + a.Register().Handler(mservice.Site, "/demo/request", api.Post, p.demoRequest) + return p, nil +} diff --git a/ci/prod/.env.runtime b/ci/prod/.env.runtime index 497b2ad..13712ad 100644 --- a/ci/prod/.env.runtime +++ b/ci/prod/.env.runtime @@ -15,9 +15,9 @@ PERMISSION_IS_FILTERED=false AMPLI_ENVIRONMENT=production API_PROTOCOL=https SERVICE_HOST=app.sendico.io -API_ENDPOINT=https://app.sendico.io/api +API_ENDPOINT=/api/v1 WS_PROTOCOL=wss -WS_ENDPOINT=wss://app.sendico.io/ws +WS_ENDPOINT=/ws AMPLITUDE_SECRET=c3d75b3e2520d708440acbb16b923e79 DEFAULT_LOCALE=en DEFAULT_CURRENCY=EUR diff --git a/ci/prod/compose/notification.yml b/ci/prod/compose/notification.yml index 391ba94..2ad532b 100644 --- a/ci/prod/compose/notification.yml +++ b/ci/prod/compose/notification.yml @@ -31,6 +31,9 @@ services: NATS_URL: ${NATS_URL} MAIL_USER: ${MAIL_USER} MAIL_SECRET: ${MAIL_SECRET} + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN} + TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID} + TELEGRAM_THREAD_ID: ${TELEGRAM_THREAD_ID} MONGO_HOST: ${MONGO_HOST} MONGO_PORT: ${MONGO_PORT} MONGO_DATABASE: ${MONGO_DATABASE} diff --git a/ci/prod/scripts/deploy/notification.sh b/ci/prod/scripts/deploy/notification.sh index 8e5ab99..5f9a75a 100755 --- a/ci/prod/scripts/deploy/notification.sh +++ b/ci/prod/scripts/deploy/notification.sh @@ -24,6 +24,8 @@ REQUIRED_SECRETS=( NATS_USER NATS_PASSWORD NATS_URL + TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID ) for var in "${REQUIRED_SECRETS[@]}"; do @@ -50,6 +52,9 @@ API_ENDPOINT_SECRET_B64="$(b64enc "${API_ENDPOINT_SECRET}")" NATS_USER_B64="$(b64enc "${NATS_USER}")" NATS_PASSWORD_B64="$(b64enc "${NATS_PASSWORD}")" NATS_URL_B64="$(b64enc "${NATS_URL}")" +TELEGRAM_BOT_TOKEN_B64="$(b64enc "${TELEGRAM_BOT_TOKEN}")" +TELEGRAM_CHAT_ID_B64="$(b64enc "${TELEGRAM_CHAT_ID}")" +TELEGRAM_THREAD_ID_B64="$(b64enc "${TELEGRAM_THREAD_ID:-}")" SSH_OPTS=( -i /root/.ssh/id_rsa @@ -86,6 +91,9 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" \ NATS_USER_B64="$NATS_USER_B64" \ NATS_PASSWORD_B64="$NATS_PASSWORD_B64" \ NATS_URL_B64="$NATS_URL_B64" \ + TELEGRAM_BOT_TOKEN_B64="$TELEGRAM_BOT_TOKEN_B64" \ + TELEGRAM_CHAT_ID_B64="$TELEGRAM_CHAT_ID_B64" \ + TELEGRAM_THREAD_ID_B64="$TELEGRAM_THREAD_ID_B64" \ bash -s <<'EOSSH' set -euo pipefail cd "${REMOTE_DIR}/compose" @@ -135,10 +143,14 @@ API_ENDPOINT_SECRET="$(decode_b64 "$API_ENDPOINT_SECRET_B64")" NATS_USER="$(decode_b64 "$NATS_USER_B64")" NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")" NATS_URL="$(decode_b64 "$NATS_URL_B64")" +TELEGRAM_BOT_TOKEN="$(decode_b64 "$TELEGRAM_BOT_TOKEN_B64")" +TELEGRAM_CHAT_ID="$(decode_b64 "$TELEGRAM_CHAT_ID_B64")" +TELEGRAM_THREAD_ID="$(decode_b64 "$TELEGRAM_THREAD_ID_B64")" export MONGO_USER MONGO_PASSWORD export MAIL_USER MAIL_SECRET API_ENDPOINT_SECRET export NATS_USER NATS_PASSWORD NATS_URL +export TELEGRAM_BOT_TOKEN TELEGRAM_CHAT_ID TELEGRAM_THREAD_ID COMPOSE_PROJECT_NAME="$COMPOSE_PROJECT" export COMPOSE_PROJECT_NAME read -r -a SERVICES <<<"${SERVICES_LINE}" diff --git a/ci/scripts/common/bump_version.sh b/ci/scripts/common/bump_version.sh index 50b1656..d8c6c7b 100755 --- a/ci/scripts/common/bump_version.sh +++ b/ci/scripts/common/bump_version.sh @@ -4,7 +4,14 @@ set -eu START_DIR="$(pwd)" echo "[bump-version] invoked from ${START_DIR}" -REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="" +if command -v git >/dev/null 2>&1; then + REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" +fi +if [ -z "${REPO_ROOT}" ]; then + REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" +fi echo "[bump-version] repo root resolved to ${REPO_ROOT}" cd "${REPO_ROOT}" @@ -60,4 +67,20 @@ if [ -z "${BRANCH}" ] || [ "${BRANCH}" = "HEAD" ]; then BRANCH="$(git rev-parse --abbrev-ref HEAD)" fi +NETRC_MACHINE="${CI_NETRC_MACHINE:-${WOODPECKER_NETRC_MACHINE:-}}" +NETRC_USERNAME="${CI_NETRC_USERNAME:-${WOODPECKER_NETRC_USERNAME:-${CI_NETRC_LOGIN:-${WOODPECKER_NETRC_LOGIN:-}}}}" +NETRC_PASSWORD="${CI_NETRC_PASSWORD:-${WOODPECKER_NETRC_PASSWORD:-}}" +if [ -n "${NETRC_MACHINE}" ] && [ -n "${NETRC_USERNAME}" ] && [ -n "${NETRC_PASSWORD}" ]; then + NETRC_FILE="${HOME:-/root}/.netrc" + if [ ! -f "${NETRC_FILE}" ]; then + { + printf 'machine %s\n' "${NETRC_MACHINE}" + printf 'login %s\n' "${NETRC_USERNAME}" + printf 'password %s\n' "${NETRC_PASSWORD}" + } > "${NETRC_FILE}" + chmod 600 "${NETRC_FILE}" + echo "[bump-version] wrote credentials for ${NETRC_MACHINE}" + fi +fi + git push origin "HEAD:${BRANCH}" diff --git a/ci/scripts/notification/deploy.sh b/ci/scripts/notification/deploy.sh index 4b97613..b354a66 100755 --- a/ci/scripts/notification/deploy.sh +++ b/ci/scripts/notification/deploy.sh @@ -49,6 +49,7 @@ load_env_file ./.env.version NOTIFICATION_MONGO_SECRET_PATH="${NOTIFICATION_MONGO_SECRET_PATH:?missing NOTIFICATION_MONGO_SECRET_PATH}" NOTIFICATION_MAIL_SECRET_PATH="${NOTIFICATION_MAIL_SECRET_PATH:?missing NOTIFICATION_MAIL_SECRET_PATH}" NOTIFICATION_API_SECRET_PATH="${NOTIFICATION_API_SECRET_PATH:?missing NOTIFICATION_API_SECRET_PATH}" +NOTIFICATION_TELEGRAM_SECRET_PATH="${NOTIFICATION_TELEGRAM_SECRET_PATH:?missing NOTIFICATION_TELEGRAM_SECRET_PATH}" : "${NATS_HOST:?missing NATS_HOST}" : "${NATS_PORT:?missing NATS_PORT}" @@ -60,6 +61,14 @@ export MAIL_SECRET="$(./ci/vlt kv_get kv "${NOTIFICATION_MAIL_SECRET_PATH}" pass export API_ENDPOINT_SECRET="$(./ci/vlt kv_get kv "${NOTIFICATION_API_SECRET_PATH}" secret)" +export TELEGRAM_BOT_TOKEN="$(./ci/vlt kv_get kv "${NOTIFICATION_TELEGRAM_SECRET_PATH}" bot_token)" +export TELEGRAM_CHAT_ID="$(./ci/vlt kv_get kv "${NOTIFICATION_TELEGRAM_SECRET_PATH}" chat_id)" +TELEGRAM_THREAD_ID="" +if TELEGRAM_THREAD_ID_VALUE="$(./ci/vlt kv_get kv "${NOTIFICATION_TELEGRAM_SECRET_PATH}" thread_id 2>/dev/null)"; then + TELEGRAM_THREAD_ID="$TELEGRAM_THREAD_ID_VALUE" +fi +export TELEGRAM_THREAD_ID + export NATS_USER="$(./ci/vlt kv_get kv sendico/nats user)" export NATS_PASSWORD="$(./ci/vlt kv_get kv sendico/nats password)" export NATS_URL="nats://${NATS_USER}:${NATS_PASSWORD}@${NATS_HOST}:${NATS_PORT}" From ebb40c8e9b2df6b0152f6b48358b9037667dac4c Mon Sep 17 00:00:00 2001 From: Stephan D Date: Mon, 17 Nov 2025 23:40:12 +0100 Subject: [PATCH 04/32] fixed chain address --- api/server/config.yml | 2 +- frontend/pshared/lib/.DS_Store | Bin 6148 -> 0 bytes frontend/pshared/lib/api/.DS_Store | Bin 6148 -> 0 bytes frontend/pshared/lib/models/.DS_Store | Bin 6148 -> 0 bytes 4 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 frontend/pshared/lib/.DS_Store delete mode 100644 frontend/pshared/lib/api/.DS_Store delete mode 100644 frontend/pshared/lib/models/.DS_Store diff --git a/api/server/config.yml b/api/server/config.yml index 80950c8..07f95c0 100755 --- a/api/server/config.yml +++ b/api/server/config.yml @@ -75,7 +75,7 @@ api: root_path: ./storage chain_gateway: - address_env: CHAIN_GATEWAY_ADDRESS + address: sendico-chain-gateway dial_timeout_seconds: 5 call_timeout_seconds: 5 insecure: true diff --git a/frontend/pshared/lib/.DS_Store b/frontend/pshared/lib/.DS_Store deleted file mode 100644 index 14a15a24afedb9e3f39906eb9ebcaa9ad3a319ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKISv9b4733uBpOP}e1RWC2wt!spa9WuNFb=U;$1wA@zKCS2Mroa&LoZ}QKne0 zMMUS9^-N?UA|tq=+-&HY?VES3mk|ZRamG&0*XDHC9S_@1_WOWwhq9NWtY!1Uw>=sa zpaN8Y3Qz$m@LdJ6zK+JLgt08k_#pjW{wb2$RG{I>0rS` zf6+wWE@22Eyub+i@9z&{9iZFaS@kCJ|Ba((NN6>z;c;Cx zk6P2ZnzWjYx;ofDnocWnduR9bqWcs-C;H8jDUf#~n+^+jM`fWWGFQqBFatkifc6K4jnH$LTQpk-c65Cvf0>X3b$Uw>DuHI&(UuNMWe>R0j%m6d+&lnKpj^AlxQSNM=E04}vh3y_23B~24prAf;3BUpEBQ@i+ cenmRQc@A@nGz-~HIwD^L3?bYx1HZt)7k*Doa{vGU diff --git a/frontend/pshared/lib/models/.DS_Store b/frontend/pshared/lib/models/.DS_Store deleted file mode 100644 index f469e241e9e56b9f0610ccae423aacdebe1221c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}T>S5T32oZYe?!3Oz1(Em(gj6)&OI7cim+m70*E!I&*gY7eE5v%Zi|;`2DO zy8(+mcoMNQu=~x<&u->}><<8l{xmuOXaa!5Mkq*GA!J_Zs<>c6Vde-Rgak(6EE}(w z=r5Y++a+X>gN6xw`Tj7`A^P6KaS~^Fr}H5y8`aINnykr&yz?LR%+G^-n)QRp4UI0f zPQp?T!mD^R8#Z^&beacoI+~b-I2vKd?RA_+de+y|G|Eh_ZymBO>%(SiKJWFqZFP9u zS+v#s Date: Tue, 18 Nov 2025 00:20:25 +0100 Subject: [PATCH 05/32] removed obsolete errors --- api/.DS_Store | Bin 8196 -> 0 bytes api/fx/oracle/client/client.go | 22 ++++++------ api/ledger/client/client.go | 6 ++-- api/ledger/internal/service/ledger/helpers.go | 2 +- .../service/ledger/outbox_publisher.go | 5 +-- api/ledger/internal/service/ledger/queries.go | 6 ++-- api/ledger/internal/service/ledger/service.go | 9 ++--- .../server/notificationimp/notification.go | 8 ++--- .../server/notificationimp/telegram/client.go | 13 +++---- api/payments/orchestrator/client/client.go | 6 ++-- api/pkg/merrors/errors.go | 20 +++++++++-- .../notifications/site/notification.go | 5 ++- api/server/interface/api/config.go | 5 +-- .../internal/server/accountapiimp/service.go | 32 +++++++++++------- 14 files changed, 82 insertions(+), 57 deletions(-) delete mode 100644 api/.DS_Store diff --git a/api/.DS_Store b/api/.DS_Store deleted file mode 100644 index eace314625d29ce1c5efb4b192f225927134f25f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMU2GIp6h3EL=v-;(1quv{)zupQx@uWU`4iaw&_a&x@<8N)>F5EvvqjOSxcAj))J7hNJn+BtfH)r#R0U0kbX-w+bWkHx0HT%D zEekzU9w2oRAx(#LTv4e?bE@nCLsbk@3{>hgo)F?h(;*#KRH*|hb-?h=7-lFa-%ff7 zDGr!cG-@LcL>{=(10r{?1{XY;vtORSd*EhE{%F?mvLtglJL=ee(A0DRV-=M%W>#rc z+AQtf%t5b|35=lNw;B0;g6r`-CtH3tvVFEy8sc^Bo*NjpYvrYaZCWIdubOnid)3K)e$FnqnOS(92z*0eNV8*3cD*sk^__Gfu-z9{bvW3%+0-oE}J&I`V^ z$8wCoE?ON%VEIG5)~9XSmh;@*d8Npwu0m~l^?J77q($!VOfj3O@pnj`-jdMk^?hQbJ#SHKh6=aoOIVLj3)Z-y1edc$ z)^mv9$GES0?qE%NuVZ8_hjCNAXBAtm_ZLH}_;%H~maW$Za^A=Ync~w*ld5~8zFEId zy!md!4|a`Ou2|I>HP)tg=!5=2BX8|0Q3PoGL)G6!{c<;GM5t{s?ZC^6HIwPv>vesv zn6T9}r6Ig_c8$JU-=pz!8f~lX7Hx_E|7x=HaM?%73P=g%UgoPr-3`8Qy?X@E)9jkKinP3g_T6_!@qIAK@qX8GeVqkS<-Uz-rWS5yr6r zm*H}3#3Zi9^|%2y;wJ3E9k>&B<2^WtSZ%k?wS3D+J7H?X~D0+BB z$TqGf78S$Rt!ETRyfS1rZDt~bIY>SpZ||U)&T#GDL-bX66HdZu_<$gK0ltE7;3E7% zu)F~|F2IEZ%*9xbOK>ScbA<$E3$DR+1kMiJf}OY(yD@`3*o*zRR|3?O03F4{=wksN z!iRAjAHhfQI6jTf;IsG~LG~4V9p9P+!|sVN#P|P!VKI|;9nT%6o-$}B3QrrZLajs> z`Tv%wzyD90^+pScJP>){+Is*iJ2D+@H1AaBmB_UdRPU!MijW&uRH{%T({Z9Q9VdF} k4?}uRkf_i}hjd&~>Y?(Ve+ak^zlfvzKf3=V*r!qRH}4>U{Qv*} diff --git a/api/fx/oracle/client/client.go b/api/fx/oracle/client/client.go index 82ab2d2..ca8b6fd 100644 --- a/api/fx/oracle/client/client.go +++ b/api/fx/oracle/client/client.go @@ -3,11 +3,11 @@ package client import ( "context" "crypto/tls" - "errors" "fmt" "strings" "time" + "github.com/tech/sendico/pkg/merrors" fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" @@ -88,7 +88,7 @@ type oracleClient struct { func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) { cfg.setDefaults() if strings.TrimSpace(cfg.Address) == "" { - return nil, errors.New("oracle: address is required") + return nil, merrors.InvalidArgument("oracle: address is required") } dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) @@ -105,7 +105,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...) if err != nil { - return nil, fmt.Errorf("oracle: dial %s: %w", cfg.Address, err) + return nil, merrors.InternalWrap(err, fmt.Sprintf("oracle: dial %s", cfg.Address)) } return &oracleClient{ @@ -133,7 +133,7 @@ func (c *oracleClient) Close() error { func (c *oracleClient) LatestRate(ctx context.Context, req LatestRateParams) (*RateSnapshot, error) { if req.Pair == nil { - return nil, errors.New("oracle: pair is required") + return nil, merrors.InvalidArgument("oracle: pair is required") } callCtx, cancel := c.callContext(ctx) @@ -145,26 +145,26 @@ func (c *oracleClient) LatestRate(ctx context.Context, req LatestRateParams) (*R Provider: req.Provider, }) if err != nil { - return nil, fmt.Errorf("oracle: latest rate: %w", err) + return nil, merrors.InternalWrap(err, "oracle: latest rate") } if resp.GetRate() == nil { - return nil, errors.New("oracle: latest rate: empty payload") + return nil, merrors.Internal("oracle: latest rate: empty payload") } return fromProtoRate(resp.GetRate()), nil } func (c *oracleClient) GetQuote(ctx context.Context, req GetQuoteParams) (*Quote, error) { if req.Pair == nil { - return nil, errors.New("oracle: pair is required") + return nil, merrors.InvalidArgument("oracle: pair is required") } if req.Side == fxv1.Side_SIDE_UNSPECIFIED { - return nil, errors.New("oracle: side is required") + return nil, merrors.InvalidArgument("oracle: side is required") } baseSupplied := req.BaseAmount != nil quoteSupplied := req.QuoteAmount != nil if baseSupplied == quoteSupplied { - return nil, errors.New("oracle: exactly one of base_amount or quote_amount must be set") + return nil, merrors.InvalidArgument("oracle: exactly one of base_amount or quote_amount must be set") } callCtx, cancel := c.callContext(ctx) @@ -191,10 +191,10 @@ func (c *oracleClient) GetQuote(ctx context.Context, req GetQuoteParams) (*Quote resp, err := c.client.GetQuote(callCtx, protoReq) if err != nil { - return nil, fmt.Errorf("oracle: get quote: %w", err) + return nil, merrors.InternalWrap(err, "oracle: get quote") } if resp.GetQuote() == nil { - return nil, errors.New("oracle: get quote: empty payload") + return nil, merrors.Internal("oracle: get quote: empty payload") } return fromProtoQuote(resp.GetQuote()), nil } diff --git a/api/ledger/client/client.go b/api/ledger/client/client.go index f71927c..3b0671a 100644 --- a/api/ledger/client/client.go +++ b/api/ledger/client/client.go @@ -3,11 +3,11 @@ package client import ( "context" "crypto/tls" - "errors" "fmt" "strings" "time" + "github.com/tech/sendico/pkg/merrors" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -48,7 +48,7 @@ type ledgerClient struct { func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) { cfg.setDefaults() if strings.TrimSpace(cfg.Address) == "" { - return nil, errors.New("ledger: address is required") + return nil, merrors.InvalidArgument("ledger: address is required") } dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) @@ -65,7 +65,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...) if err != nil { - return nil, fmt.Errorf("ledger: dial %s: %w", cfg.Address, err) + return nil, merrors.InternalWrap(err, fmt.Sprintf("ledger: dial %s", cfg.Address)) } return &ledgerClient{ diff --git a/api/ledger/internal/service/ledger/helpers.go b/api/ledger/internal/service/ledger/helpers.go index 63baf19..de0b9b8 100644 --- a/api/ledger/internal/service/ledger/helpers.go +++ b/api/ledger/internal/service/ledger/helpers.go @@ -146,7 +146,7 @@ func calculateBalance(lines []*model.PostingLine) (decimal.Decimal, error) { for _, line := range lines { amount, err := parseDecimal(line.Amount) if err != nil { - return decimal.Zero, fmt.Errorf("invalid line amount: %w", err) + return decimal.Zero, merrors.InvalidArgumentWrap(err, "invalid line amount") } balance = balance.Add(amount) } diff --git a/api/ledger/internal/service/ledger/outbox_publisher.go b/api/ledger/internal/service/ledger/outbox_publisher.go index 30a3a3d..bad4baf 100644 --- a/api/ledger/internal/service/ledger/outbox_publisher.go +++ b/api/ledger/internal/service/ledger/outbox_publisher.go @@ -8,6 +8,7 @@ import ( "github.com/tech/sendico/ledger/storage" ledgerModel "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/merrors" pmessaging "github.com/tech/sendico/pkg/messaging" me "github.com/tech/sendico/pkg/messaging/envelope" "github.com/tech/sendico/pkg/mlogger" @@ -126,7 +127,7 @@ func (p *outboxPublisher) dispatchPending(ctx context.Context) (int, error) { func (p *outboxPublisher) publishEvent(_ context.Context, event *ledgerModel.OutboxEvent) error { docID := event.GetID() if docID == nil || docID.IsZero() { - return errors.New("outbox event missing identifier") + return merrors.InvalidArgument("outbox event missing identifier") } payload, err := p.wrapPayload(event) @@ -157,7 +158,7 @@ func (p *outboxPublisher) wrapPayload(event *ledgerModel.OutboxEvent) ([]byte, e func (p *outboxPublisher) markSent(ctx context.Context, event *ledgerModel.OutboxEvent) error { eventRef := event.GetID() if eventRef == nil || eventRef.IsZero() { - return errors.New("outbox event missing identifier") + return merrors.InvalidArgument("outbox event missing identifier") } return p.store.MarkSent(ctx, *eventRef, time.Now().UTC()) diff --git a/api/ledger/internal/service/ledger/queries.go b/api/ledger/internal/service/ledger/queries.go index e59e8dd..a61ea9f 100644 --- a/api/ledger/internal/service/ledger/queries.go +++ b/api/ledger/internal/service/ledger/queries.go @@ -249,15 +249,15 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat func parseCursor(cursor string) (int, error) { decoded, err := base64.StdEncoding.DecodeString(cursor) if err != nil { - return 0, fmt.Errorf("invalid base64: %w", err) + return 0, merrors.InvalidArgumentWrap(err, "invalid cursor base64 encoding") } parts := strings.Split(string(decoded), ":") if len(parts) != 2 || parts[0] != "offset" { - return 0, fmt.Errorf("invalid cursor format") + return 0, merrors.InvalidArgument("invalid cursor format") } offset, err := strconv.Atoi(parts[1]) if err != nil { - return 0, fmt.Errorf("invalid offset: %w", err) + return 0, merrors.InvalidArgumentWrap(err, "invalid cursor offset") } return offset, nil } diff --git a/api/ledger/internal/service/ledger/service.go b/api/ledger/internal/service/ledger/service.go index ce9c4cb..3763481 100644 --- a/api/ledger/internal/service/ledger/service.go +++ b/api/ledger/internal/service/ledger/service.go @@ -18,6 +18,7 @@ import ( "github.com/tech/sendico/ledger/storage" "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/merrors" pmessaging "github.com/tech/sendico/pkg/messaging" "github.com/tech/sendico/pkg/mlogger" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" @@ -241,10 +242,10 @@ func (s *Service) quoteFees(ctx context.Context, trigger feesv1.Trigger, organiz return nil, nil } if strings.TrimSpace(organizationRef) == "" { - return nil, fmt.Errorf("organization reference is required to quote fees") + return nil, merrors.InvalidArgument("organization reference is required to quote fees") } if baseAmount == nil { - return nil, fmt.Errorf("base amount is required to quote fees") + return nil, merrors.InvalidArgument("base amount is required to quote fees") } amountCopy := &moneyv1.Money{Amount: baseAmount.GetAmount(), Currency: baseAmount.GetCurrency()} @@ -309,11 +310,11 @@ func convertFeeDerivedLines(lines []*feesv1.DerivedPostingLine) ([]*ledgerv1.Pos continue } if line.GetMoney() == nil { - return nil, fmt.Errorf("fee line %d missing money", idx) + return nil, merrors.Internal(fmt.Sprintf("fee line %d missing money", idx)) } dec, err := decimal.NewFromString(line.GetMoney().GetAmount()) if err != nil { - return nil, fmt.Errorf("fee line %d invalid amount: %w", idx, err) + return nil, merrors.InternalWrap(err, fmt.Sprintf("fee line %d invalid amount", idx)) } dec = ensureAmountForSide(dec, line.GetSide()) posting := &ledgerv1.PostingLine{ diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index 35913b4..c5d32c3 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -2,12 +2,12 @@ package notificationimp import ( "context" - "fmt" "github.com/tech/sendico/notification/interface/api" mmail "github.com/tech/sendico/notification/internal/server/notificationimp/mail" "github.com/tech/sendico/notification/internal/server/notificationimp/telegram" "github.com/tech/sendico/pkg/domainprovider" + "github.com/tech/sendico/pkg/merrors" na "github.com/tech/sendico/pkg/messaging/notifications/account" ni "github.com/tech/sendico/pkg/messaging/notifications/invitation" snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" @@ -39,10 +39,10 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { p.logger = a.Logger().Named(p.Name()) if a.Config().Notification == nil { - return nil, fmt.Errorf("notification configuration is missing") + return nil, merrors.InvalidArgument("notification configuration is missing") } if a.Config().Notification.Telegram == nil { - return nil, fmt.Errorf("telegram configuration is missing") + return nil, merrors.InvalidArgument("telegram configuration is missing") } var err error @@ -90,7 +90,7 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { func (a *NotificationAPI) onDemoRequest(ctx context.Context, request *model.DemoRequest) error { if a.tg == nil { - return fmt.Errorf("telegram client is not configured") + return merrors.Internal("telegram client is not configured") } if err := a.tg.SendDemoRequest(ctx, request); err != nil { a.logger.Warn("Failed to send demo request via telegram", zap.Error(err)) diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go index d91c7b9..030264c 100644 --- a/api/notification/internal/server/notificationimp/telegram/client.go +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -13,6 +13,7 @@ import ( "time" notconfig "github.com/tech/sendico/notification/interface/services/notification/config" + "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" ) @@ -45,15 +46,15 @@ type sendMessagePayload struct { func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, error) { if cfg == nil { - return nil, fmt.Errorf("telegram configuration is not provided") + return nil, merrors.InvalidArgument("telegram configuration is not provided") } token := strings.TrimSpace(os.Getenv(cfg.BotTokenEnv)) if token == "" { - return nil, fmt.Errorf("telegram bot token env %s is empty", cfg.BotTokenEnv) + return nil, merrors.InvalidArgument(fmt.Sprintf("telegram bot token env %s is empty", cfg.BotTokenEnv)) } chatID := strings.TrimSpace(os.Getenv(cfg.ChatIDEnv)) if chatID == "" { - return nil, fmt.Errorf("telegram chat id env %s is empty", cfg.ChatIDEnv) + return nil, merrors.InvalidArgument(fmt.Sprintf("telegram chat id env %s is empty", cfg.ChatIDEnv)) } var threadID *int64 @@ -62,7 +63,7 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er if raw != "" { val, err := strconv.ParseInt(raw, 10, 64) if err != nil { - return nil, fmt.Errorf("telegram thread id env %s is invalid: %w", env, err) + return nil, merrors.InvalidArgumentWrap(err, fmt.Sprintf("telegram thread id env %s is invalid", env)) } threadID = &val } @@ -93,7 +94,7 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er func (c *client) SendDemoRequest(ctx context.Context, request *model.DemoRequest) error { if request == nil { - return fmt.Errorf("demo request payload is nil") + return merrors.InvalidArgument("demo request payload is nil") } message := buildMessage(request) payload := sendMessagePayload{ @@ -127,7 +128,7 @@ func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) er return nil } respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10)) - return fmt.Errorf("telegram sendMessage failed with status %d: %s", resp.StatusCode, string(respBody)) + return merrors.Internal(fmt.Sprintf("telegram sendMessage failed with status %d: %s", resp.StatusCode, string(respBody))) } func (c *client) endpoint() string { diff --git a/api/payments/orchestrator/client/client.go b/api/payments/orchestrator/client/client.go index e98b3ee..c571933 100644 --- a/api/payments/orchestrator/client/client.go +++ b/api/payments/orchestrator/client/client.go @@ -3,11 +3,11 @@ package client import ( "context" "crypto/tls" - "errors" "fmt" "strings" "time" + "github.com/tech/sendico/pkg/merrors" orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -48,7 +48,7 @@ type orchestratorClient struct { func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) { cfg.setDefaults() if strings.TrimSpace(cfg.Address) == "" { - return nil, errors.New("payment-orchestrator: address is required") + return nil, merrors.InvalidArgument("payment-orchestrator: address is required") } dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) @@ -65,7 +65,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...) if err != nil { - return nil, fmt.Errorf("payment-orchestrator: dial %s: %w", cfg.Address, err) + return nil, merrors.InternalWrap(err, fmt.Sprintf("payment-orchestrator: dial %s", cfg.Address)) } return &orchestratorClient{ diff --git a/api/pkg/merrors/errors.go b/api/pkg/merrors/errors.go index cb558c0..3667374 100644 --- a/api/pkg/merrors/errors.go +++ b/api/pkg/merrors/errors.go @@ -49,13 +49,13 @@ func AccessDenied(object, action string, objectRef primitive.ObjectID) error { var ErrInvalidDataType = errors.New("invalidDataType") func InvalidDataType(msg string) error { - return fmt.Errorf("%w: %s", ErrDataConflict, msg) + return fmt.Errorf("%w: %s", ErrInvalidDataType, msg) } var ErrUnauthorized = errors.New("unathorized") func Unauthorized(msg string) error { - return fmt.Errorf("%w: %s", ErrDataConflict, msg) + return fmt.Errorf("%w: %s", ErrUnauthorized, msg) } var ErrNoMessagingTopic = errors.New("messagingTopicError") @@ -63,3 +63,19 @@ var ErrNoMessagingTopic = errors.New("messagingTopicError") func NoMessagingTopic(topic string) error { return fmt.Errorf("%w: messaging topic '%s' not found", ErrNoMessagingTopic, topic) } + +func InvalidArgumentWrap(err error, msg string) error { + return wrapError(ErrInvalidArg, msg, err) +} + +func InternalWrap(err error, msg string) error { + return wrapError(ErrInternal, msg, err) +} + +func wrapError(base error, msg string, err error) error { + baseErr := fmt.Errorf("%w: %s", base, msg) + if err == nil { + return baseErr + } + return errors.Join(baseErr, err) +} diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go index 0225c23..d40f824 100644 --- a/api/pkg/messaging/internal/notifications/site/notification.go +++ b/api/pkg/messaging/internal/notifications/site/notification.go @@ -1,9 +1,8 @@ package notifications import ( - "fmt" - gmessaging "github.com/tech/sendico/pkg/generated/gmessaging" + "github.com/tech/sendico/pkg/merrors" messaging "github.com/tech/sendico/pkg/messaging/envelope" "github.com/tech/sendico/pkg/model" nm "github.com/tech/sendico/pkg/model/notification" @@ -18,7 +17,7 @@ type DemoRequestNotification struct { func (drn *DemoRequestNotification) Serialize() ([]byte, error) { if drn.request == nil { - return nil, fmt.Errorf("demo request payload is empty") + return nil, merrors.InvalidArgument("demo request payload is empty") } msg := gmessaging.DemoRequestEvent{ Name: drn.request.Name, diff --git a/api/server/interface/api/config.go b/api/server/interface/api/config.go index c1f8850..f21e3af 100644 --- a/api/server/interface/api/config.go +++ b/api/server/interface/api/config.go @@ -6,12 +6,13 @@ import ( ) type Config struct { - Mw *mwa.Config `yaml:"middleware"` - Storage *fsc.Config `yaml:"storage"` + Mw *mwa.Config `yaml:"middleware"` + Storage *fsc.Config `yaml:"storage"` ChainGateway *ChainGatewayConfig `yaml:"chain_gateway"` } type ChainGatewayConfig struct { + Address string `yaml:"address"` AddressEnv string `yaml:"address_env"` DialTimeoutSeconds int `yaml:"dial_timeout_seconds"` CallTimeoutSeconds int `yaml:"call_timeout_seconds"` diff --git a/api/server/internal/server/accountapiimp/service.go b/api/server/internal/server/accountapiimp/service.go index 169d464..df3a0e8 100644 --- a/api/server/internal/server/accountapiimp/service.go +++ b/api/server/internal/server/accountapiimp/service.go @@ -16,6 +16,7 @@ import ( "github.com/tech/sendico/pkg/db/refreshtokens" "github.com/tech/sendico/pkg/db/transaction" "github.com/tech/sendico/pkg/domainprovider" + "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/messaging" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mservice" @@ -142,7 +143,12 @@ func CreateAPI(a eapi.API) (*AccountAPI, error) { } p.accountsPermissionRef = accountsPolicy.ID - if err := p.initChainGateway(a.Config()); err != nil { + cfg := a.Config() + if cfg == nil { + p.logger.Error("Failed to fetch service configuration") + return nil, merrors.InvalidArgument("No configuration provided") + } + if err := p.initChainGateway(cfg.ChainGateway); err != nil { p.logger.Error("Failed to initialize chain gateway client", zap.Error(err)) return nil, err } @@ -150,21 +156,21 @@ func CreateAPI(a eapi.API) (*AccountAPI, error) { return p, nil } -func (a *AccountAPI) initChainGateway(cfg *eapi.Config) error { - if cfg == nil || cfg.ChainGateway == nil { - return fmt.Errorf("chain gateway configuration is not provided") +func (a *AccountAPI) initChainGateway(cfg *eapi.ChainGatewayConfig) error { + if cfg == nil { + return merrors.InvalidArgument("chain gateway configuration is not provided") } - address := strings.TrimSpace(os.Getenv(cfg.ChainGateway.AddressEnv)) + address := strings.TrimSpace(os.Getenv(cfg.AddressEnv)) if address == "" { - return fmt.Errorf("chain gateway address env %s is empty", cfg.ChainGateway.AddressEnv) + return merrors.InvalidArgument(fmt.Sprintf("chain gateway address env %s is empty", cfg.AddressEnv)) } clientCfg := chaingatewayclient.Config{ Address: address, - DialTimeout: time.Duration(cfg.ChainGateway.DialTimeoutSeconds) * time.Second, - CallTimeout: time.Duration(cfg.ChainGateway.CallTimeoutSeconds) * time.Second, - Insecure: cfg.ChainGateway.Insecure, + DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second, + CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second, + Insecure: cfg.Insecure, } client, err := chaingatewayclient.New(context.Background(), clientCfg) @@ -172,7 +178,7 @@ func (a *AccountAPI) initChainGateway(cfg *eapi.Config) error { return err } - asset, err := buildGatewayAsset(cfg.ChainGateway.DefaultAsset) + asset, err := buildGatewayAsset(cfg.DefaultAsset) if err != nil { _ = client.Close() return err @@ -190,7 +196,7 @@ func buildGatewayAsset(cfg eapi.ChainGatewayAssetConfig) (*gatewayv1.Asset, erro } tokenSymbol := strings.TrimSpace(cfg.TokenSymbol) if tokenSymbol == "" { - return nil, fmt.Errorf("chain gateway token symbol is required") + return nil, merrors.InvalidArgument("chain gateway token symbol is required") } return &gatewayv1.Asset{ Chain: chain, @@ -208,8 +214,8 @@ func parseChainNetwork(value string) (gatewayv1.ChainNetwork, error) { case "OTHER_EVM", "CHAIN_NETWORK_OTHER_EVM": return gatewayv1.ChainNetwork_CHAIN_NETWORK_OTHER_EVM, nil case "", "CHAIN_NETWORK_UNSPECIFIED": - return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, fmt.Errorf("chain network must be specified") + return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument("chain network must be specified") default: - return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, fmt.Errorf("unsupported chain network %s", value) + return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument("unsupported chain network %s", value) } } From 922dad3dcd3e37f6a130049148aa2108fc31f1e4 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 00:20:28 +0100 Subject: [PATCH 06/32] removed obsolete errors --- api/server/internal/server/accountapiimp/service.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/api/server/internal/server/accountapiimp/service.go b/api/server/internal/server/accountapiimp/service.go index df3a0e8..9db86cc 100644 --- a/api/server/internal/server/accountapiimp/service.go +++ b/api/server/internal/server/accountapiimp/service.go @@ -161,13 +161,16 @@ func (a *AccountAPI) initChainGateway(cfg *eapi.ChainGatewayConfig) error { return merrors.InvalidArgument("chain gateway configuration is not provided") } - address := strings.TrimSpace(os.Getenv(cfg.AddressEnv)) - if address == "" { - return merrors.InvalidArgument(fmt.Sprintf("chain gateway address env %s is empty", cfg.AddressEnv)) + cfg.Address = strings.TrimSpace(cfg.Address) + if cfg.Address == "" { + cfg.Address = strings.TrimSpace(os.Getenv(cfg.AddressEnv)) + } + if cfg.Address == "" { + return merrors.InvalidArgument(fmt.Sprintf("chain gateway address is not specified and address env %s is empty", cfg.AddressEnv)) } clientCfg := chaingatewayclient.Config{ - Address: address, + Address: cfg.Address, DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second, CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second, Insecure: cfg.Insecure, @@ -216,6 +219,6 @@ func parseChainNetwork(value string) (gatewayv1.ChainNetwork, error) { case "", "CHAIN_NETWORK_UNSPECIFIED": return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument("chain network must be specified") default: - return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument("unsupported chain network %s", value) + return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument(fmt.Sprintf("unsupported chain network %s", value)) } } From e1f6f101143d0d974a70ce2cf5c8ce199951d269 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 00:35:55 +0100 Subject: [PATCH 07/32] health check fix --- .DS_Store | Bin 12292 -> 0 bytes api/fx/ingestor/.DS_Store | Bin 6148 -> 0 bytes api/notification/.DS_Store | Bin 6148 -> 0 bytes api/pkg/.DS_Store | Bin 6148 -> 0 bytes ci/prod/compose/bff.yml | 2 +- ci/scripts/common/bump_version.sh | 27 +++++++++++++++------------ 6 files changed, 16 insertions(+), 13 deletions(-) delete mode 100644 .DS_Store delete mode 100644 api/fx/ingestor/.DS_Store delete mode 100644 api/notification/.DS_Store delete mode 100644 api/pkg/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index db2565e8fb2fe6c788a2c2501f92b2c47a8d5f3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12292 zcmeHNU2GIp6uxKL(pkFD0a_@~fei}^*g|djDL=;Twg}~~*lp>y1lk8DKoR% z)`HoX_yVFaK4|=fCx3`Ms3HkDw<%3CK!$KA_=~zJb3QCvu)YJlSC=bB=??s z&%NjVoco>KbLI{q1fps6K|+=iLPSiIN!OFOYw$n7>DL@WjnvMy}5+N2b ziAyxh)d{_QhOYr%19y`K1Y8WY-$G0x=_U4f`_+?~frMctu*>DdfT25XP0bI|3=j$m zi>4HdfvF|IY12!k88c^<%`Bfed-m+IITdr~&7Uv#NV5`3@?f8BrA@WbGAui4q_yro zTUTQPN>Vrb;;uI2#`MRu9%*J?*H%s4H^yok9>6|jV+=OjW?R!tHeO3_ucNu~aple) zsnki@mSOC&9NpC|v$IPXYV+pXu$hz8^s?e&sHAjvF#PaaU z4Xr%~jg)B}*MP5q|E~tP%Xv-T<-8{5`O6*g&aUp@ zZGGsQJ>!m~Wu#N4(<9ADDn>GGD6ZCE7}QrrUcSRVEVm7~ zb{32TB$dxoA6zeYWOxh{khXE=L&0)6p0rXa+@cfUyau^Z-bbh1t~hSXfM(JqUEpnx zU|W~duh?44AUe9(?1@H9LRufVHt0#3qP@D98SXW)JK7%spU@Fjc=SKvqZ34Vs(;7|AqZU{xf z9HBy(D=ZSKglb`huvVxS)(abiokEKc7Zl-uUgfmmakk@|4_py7&&@#fE{lJ4Er}(48uOdMjmc2GHVPj zSeP@vF@xj=+Kxt}4T%A?U8&gX9bPP=(MUyJb?Gv!Zj+|)YGw5b5e-xlST$H#TNgkx zlnT6R^;)a~uLL)244^?ug5@@lK)r61hTtA=fa}2bd1CpaSlP z8pL=Mw!lt?@)!pF`=AH*LjqJtfe98ILcAY_Bk(w6;R!_hQ;7Fx;8}PPUV@hq^RL4j z@Ftvsw-NClz*+bZK7mi+Jbdn9|1u)~DqPP)^Nt*jIfdVEh333ayol)dN|?T2Qtpv} zeB6%iY*_BUDC4-g`(Bjcy2hP)9>V#e{oL1puK`~Jz6N{^OcD(g<2t?jlY7K9^LFFA zW$gO%p1;2w!*8BT=HLJMH}aG6MjjE(u$zbH39pR)>)-48+WM+c?YjDZa<|PsE185B z^5Y`GN%}{FC@aE*&r*8-zv=eh|FKSf=4-&$z`dyfC~S%~MR9N5-TH)G(!2H$ru~@c zL;GbpWg?h)*YT{4UBx5MyUAg^2zIA(Sx%W4*16cf_YuHk_#zKAMr{M9`n zVzh#dDQIgtclmsMu7E4x3b+D)PywFVVv9>f?_B{`z!msbK=y~oCRiFK#k_T}%U1y6 zoat=zr%}X_ypc7RhHi(SHQ8Al|tGzo5V;NbW47 diff --git a/api/notification/.DS_Store b/api/notification/.DS_Store deleted file mode 100644 index c82e4d967d28990c887ea6ba017654903b73915d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKO-sW-5Phpf3SN5jxWB;S$!jR_CL)M;wXId5O-Pe_e!6cy6hm{CDl;(iW@l%1 zHxIHO0MepcT>&!yQ>tJRXGA1C+I8Z|f=S1yu&Uda+iu@BBLf{{NY;La4Juq>OZ#*E z*LcJpYc#ZTYD>Sy8=kRayJYqf9jn%Cx0-X$+zKCzEIqQN*Dzbuhb=@ z;#WH2;Y#kD$-xSbk;Yh4Fc1s`1Hr%_Vt{A1$n@MWY%mZE1Owj;$o`P2f`wyms9Oh( zJ^_dmhE?dxTS98GW8v5vvWJqalxU^NpBTx?8PA?qIQE8Cj-)el9H05?@sf0Q#AV{a&2L!UO}qz`!S6Z84Yt diff --git a/api/pkg/.DS_Store b/api/pkg/.DS_Store deleted file mode 100644 index 1aa90dd38745402468a211e4eb99a4992b4db050..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~K?=e^3`G;|qTr@Wm$UHz-e3?tK`)>n=t3%px}Kx^lL>;=wTS#c@+X-IrEk$` zL_}A&{Zgb8krr+$3kxGt4X|{^>yQ5dhkt?1r_^63}D?Xbvq^QGsbp4;roN zV~Ewg9h%}?4lPw{yJ!p_8c$Z6VqjX^MH3R3W)}t$Ab}BqY0W#k|2ObY^Z%%YDG89k zpApbz-LF@8skmF;UeD^=sM@-~p?)0U_8}^eepgFWuMFqwm0mr~V0$(NY E0L2Ru#Q*>R diff --git a/ci/prod/compose/bff.yml b/ci/prod/compose/bff.yml index a50ad11..605f3da 100644 --- a/ci/prod/compose/bff.yml +++ b/ci/prod/compose/bff.yml @@ -43,7 +43,7 @@ services: ports: - "0.0.0.0:${BFF_HTTP_PORT}:8081" healthcheck: - test: ["CMD-SHELL","wget -qO- http://localhost:8081/health | grep -q '\"status\":\"ok\"'"] + test: ["CMD-SHELL","wget -qO- http://localhost:8081/api/v1/health | grep -q '\"status\":\"ok\"'"] interval: 30s timeout: 10s retries: 3 diff --git a/ci/scripts/common/bump_version.sh b/ci/scripts/common/bump_version.sh index d8c6c7b..b17d240 100755 --- a/ci/scripts/common/bump_version.sh +++ b/ci/scripts/common/bump_version.sh @@ -67,20 +67,23 @@ if [ -z "${BRANCH}" ] || [ "${BRANCH}" = "HEAD" ]; then BRANCH="$(git rev-parse --abbrev-ref HEAD)" fi -NETRC_MACHINE="${CI_NETRC_MACHINE:-${WOODPECKER_NETRC_MACHINE:-}}" -NETRC_USERNAME="${CI_NETRC_USERNAME:-${WOODPECKER_NETRC_USERNAME:-${CI_NETRC_LOGIN:-${WOODPECKER_NETRC_LOGIN:-}}}}" -NETRC_PASSWORD="${CI_NETRC_PASSWORD:-${WOODPECKER_NETRC_PASSWORD:-}}" +NETRC_MACHINE="${CI_NETRC_MACHINE:-${WOODPECKER_NETRC_MACHINE:-${DRONE_NETRC_MACHINE:-}}}" +NETRC_USERNAME="${CI_NETRC_USERNAME:-${WOODPECKER_NETRC_USERNAME:-${DRONE_NETRC_USERNAME:-${CI_NETRC_LOGIN:-${WOODPECKER_NETRC_LOGIN:-${DRONE_NETRC_LOGIN:-}}}}}}" +NETRC_PASSWORD="${CI_NETRC_PASSWORD:-${WOODPECKER_NETRC_PASSWORD:-${DRONE_NETRC_PASSWORD:-}}}" if [ -n "${NETRC_MACHINE}" ] && [ -n "${NETRC_USERNAME}" ] && [ -n "${NETRC_PASSWORD}" ]; then NETRC_FILE="${HOME:-/root}/.netrc" - if [ ! -f "${NETRC_FILE}" ]; then - { - printf 'machine %s\n' "${NETRC_MACHINE}" - printf 'login %s\n' "${NETRC_USERNAME}" - printf 'password %s\n' "${NETRC_PASSWORD}" - } > "${NETRC_FILE}" - chmod 600 "${NETRC_FILE}" - echo "[bump-version] wrote credentials for ${NETRC_MACHINE}" - fi + { + printf 'machine %s\n' "${NETRC_MACHINE}" + printf 'login %s\n' "${NETRC_USERNAME}" + printf 'password %s\n' "${NETRC_PASSWORD}" + } > "${NETRC_FILE}" + chmod 600 "${NETRC_FILE}" + echo "[bump-version] configured credentials for ${NETRC_MACHINE}" +fi + +REMOTE_URL="${CI_REPO_REMOTE:-${WOODPECKER_GIT_REMOTE:-${DRONE_REMOTE_URL:-}}}" +if [ -n "${REMOTE_URL}" ]; then + git remote set-url origin "${REMOTE_URL}" fi git push origin "HEAD:${BRANCH}" From df81a9383847b8b5ed2ae82e6db60cbc73f74741 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 02:25:50 +0100 Subject: [PATCH 08/32] changed CORS permissions --- api/server/config.yml | 4 ++-- ci/scripts/common/bump_version.sh | 25 ++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/api/server/config.yml b/api/server/config.yml index 07f95c0..2378701 100755 --- a/api/server/config.yml +++ b/api/server/config.yml @@ -16,8 +16,8 @@ api: CORS: max_age: 300 allowed_origins: - - "http://*" - - "https://*" + - "https://sendico.io" + - "https://app.sendico.io" allowed_methods: - "GET" - "POST" diff --git a/ci/scripts/common/bump_version.sh b/ci/scripts/common/bump_version.sh index b17d240..c179872 100755 --- a/ci/scripts/common/bump_version.sh +++ b/ci/scripts/common/bump_version.sh @@ -67,9 +67,33 @@ if [ -z "${BRANCH}" ] || [ "${BRANCH}" = "HEAD" ]; then BRANCH="$(git rev-parse --abbrev-ref HEAD)" fi +REMOTE_URL="${CI_REPO_REMOTE:-${WOODPECKER_GIT_REMOTE:-${DRONE_REMOTE_URL:-}}}" +if [ -z "${REMOTE_URL}" ]; then + REMOTE_URL="$(git config --get remote.origin.url 2>/dev/null || true)" +fi + +# Normalize machine to a bare hostname so .netrc matches HTTPS requests. +normalize_machine() { + value="$1" + if [ -z "${value}" ]; then + printf '%s' "" + return + fi + printf '%s' "${value}" | sed -E ' + s#^[[:alpha:]][[:alnum:]+.-]*://##; + s#^[^@]*@##; + s#/.*$##; + ' +} + NETRC_MACHINE="${CI_NETRC_MACHINE:-${WOODPECKER_NETRC_MACHINE:-${DRONE_NETRC_MACHINE:-}}}" NETRC_USERNAME="${CI_NETRC_USERNAME:-${WOODPECKER_NETRC_USERNAME:-${DRONE_NETRC_USERNAME:-${CI_NETRC_LOGIN:-${WOODPECKER_NETRC_LOGIN:-${DRONE_NETRC_LOGIN:-}}}}}}" NETRC_PASSWORD="${CI_NETRC_PASSWORD:-${WOODPECKER_NETRC_PASSWORD:-${DRONE_NETRC_PASSWORD:-}}}" +NETRC_MACHINE="$(normalize_machine "${NETRC_MACHINE}")" +if [ -z "${NETRC_MACHINE}" ] && [ -n "${REMOTE_URL}" ]; then + NETRC_MACHINE="$(normalize_machine "${REMOTE_URL}")" +fi + if [ -n "${NETRC_MACHINE}" ] && [ -n "${NETRC_USERNAME}" ] && [ -n "${NETRC_PASSWORD}" ]; then NETRC_FILE="${HOME:-/root}/.netrc" { @@ -81,7 +105,6 @@ if [ -n "${NETRC_MACHINE}" ] && [ -n "${NETRC_USERNAME}" ] && [ -n "${NETRC_PASS echo "[bump-version] configured credentials for ${NETRC_MACHINE}" fi -REMOTE_URL="${CI_REPO_REMOTE:-${WOODPECKER_GIT_REMOTE:-${DRONE_REMOTE_URL:-}}}" if [ -n "${REMOTE_URL}" ]; then git remote set-url origin "${REMOTE_URL}" fi From 848ff556d2a1ae743dd89ddae7657365e881b3be Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 02:39:17 +0100 Subject: [PATCH 09/32] version bump --- api/server/go.mod | 10 +++++----- api/server/go.sum | 48 ++++++++++++++++++----------------------------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/api/server/go.mod b/api/server/go.mod index b1fcb5a..318e12e 100644 --- a/api/server/go.mod +++ b/api/server/go.mod @@ -117,12 +117,12 @@ require ( github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect @@ -131,6 +131,6 @@ require ( golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/api/server/go.sum b/api/server/go.sum index 5377dae..a9a3bab 100644 --- a/api/server/go.sum +++ b/api/server/go.sum @@ -10,12 +10,8 @@ github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+X github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y= -github.com/aws/aws-sdk-go-v2/config v1.31.18 h1:RouG3AcF2fLFhw+Z0qbnuIl9HZ0Kh4E/U9sKwTMRpMI= -github.com/aws/aws-sdk-go-v2/config v1.31.18/go.mod h1:aXZ13mSQC8S2VEHwGfL1COMuJ1Zty6pX5xU7hyqjvCg= github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc= github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0= -github.com/aws/aws-sdk-go-v2/credentials v1.18.22 h1:hyIVGBHhQPaNP9D4BaVRwpjLMCwMMdAkHqB3gGMiykU= -github.com/aws/aws-sdk-go-v2/credentials v1.18.22/go.mod h1:B9E2qHs3/YGfeQZ4jrIE/nPvqxtyafZrJ5EQiZBG6pk= github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg= github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= @@ -36,20 +32,12 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw= github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2 h1:DhdbtDl4FdNlj31+xiRXANxEE+eC7n8JQz+/ilwQ8Uc= github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ= github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= -github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 h1:ZGDJVmlpPFiNFCb/I42nYVKUanJAdFUiSmUo/32AqPQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.40.0/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0= github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= @@ -212,8 +200,8 @@ github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyA github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -260,24 +248,24 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -367,12 +355,12 @@ 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-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc= -google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 920a7fc90a2b9513ac88230b4c45db0de768a0b0 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 02:39:25 +0100 Subject: [PATCH 10/32] extended logging --- api/server/internal/api/middleware.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/server/internal/api/middleware.go b/api/server/internal/api/middleware.go index 3642cca..800c800 100644 --- a/api/server/internal/api/middleware.go +++ b/api/server/internal/api/middleware.go @@ -97,6 +97,7 @@ func (mw *Middleware) installMiddleware(config *middleware.Config, debug bool) { })) mw.router.Use(cm.Recoverer) mw.router.Handle("/metrics", metrics.Handler()) + mw.logger.Info("Middleware stack installation complete") } func CreateMiddleware(logger mlogger.Logger, db db.Factory, enforcer auth.Enforcer, router *chi.Mux, config *middleware.Config, debug bool) (*Middleware, error) { From e2e77fe409b27be28ab5b7f25b9fe8778ceb119b Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 02:43:09 +0100 Subject: [PATCH 11/32] +www site to cors --- api/server/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/api/server/config.yml b/api/server/config.yml index 2378701..124b6ff 100755 --- a/api/server/config.yml +++ b/api/server/config.yml @@ -18,6 +18,7 @@ api: allowed_origins: - "https://sendico.io" - "https://app.sendico.io" + - "https://www.sendico.io" allowed_methods: - "GET" - "POST" From c8579a14f38bbef036b293778b1bcecb3289f76b Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 02:56:57 +0100 Subject: [PATCH 12/32] Caddy reconfigured --- ci/prod/.env.runtime | 1 + ci/prod/compose/frontend.yml | 1 + frontend/pweb/caddy/Caddyfile | 130 ++++++++++++++++++++++++++++++++-- 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/ci/prod/.env.runtime b/ci/prod/.env.runtime index 13712ad..feb0f92 100644 --- a/ci/prod/.env.runtime +++ b/ci/prod/.env.runtime @@ -16,6 +16,7 @@ AMPLI_ENVIRONMENT=production API_PROTOCOL=https SERVICE_HOST=app.sendico.io API_ENDPOINT=/api/v1 +API_UPSTREAM=sendico-bff:8081 WS_PROTOCOL=wss WS_ENDPOINT=/ws AMPLITUDE_SECRET=c3d75b3e2520d708440acbb16b923e79 diff --git a/ci/prod/compose/frontend.yml b/ci/prod/compose/frontend.yml index 9a1ef16..37ea36b 100644 --- a/ci/prod/compose/frontend.yml +++ b/ci/prod/compose/frontend.yml @@ -23,6 +23,7 @@ services: API_PROTOCOL: ${API_PROTOCOL} SERVICE_HOST: ${SERVICE_HOST} API_ENDPOINT: ${API_ENDPOINT} + API_UPSTREAM: ${API_UPSTREAM} AMPLITUDE_SECRET: ${AMPLITUDE_SECRET} DEFAULT_LOCALE: ${DEFAULT_LOCALE} DEFAULT_CURRENCY: ${DEFAULT_CURRENCY} diff --git a/frontend/pweb/caddy/Caddyfile b/frontend/pweb/caddy/Caddyfile index 09e9a43..7d7e91a 100644 --- a/frontend/pweb/caddy/Caddyfile +++ b/frontend/pweb/caddy/Caddyfile @@ -1,13 +1,135 @@ +######################################## +# Global options +######################################## { email {$CADDY_ACME_EMAIL} + # debug } -{$SERVICE_HOST} { - root * /usr/share/pweb +######################################## +# Sendico site definition +######################################## +(sendico_site) { + vars static_root /usr/share/pweb + # Allow overriding upstream via API_UPSTREAM, default to the BFF container. + vars api_upstream {$API_UPSTREAM:sendico-bff:8081} + encode zstd gzip - try_files {path} /index.html - file_server + header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" } + + route { + ######################################## + # Backend API + ######################################## + handle /api/v1/* { + reverse_proxy {vars.api_upstream} { + health_uri /api/v1/health + health_interval 15s + health_timeout 4s + health_status 2xx + } + header Cache-Control "no-cache, no-store, must-revalidate" + header Pragma "no-cache" + header Expires "0" + } + + ######################################## + # Static assets with tailored caching + ######################################## + handle /version.json { + root * {vars.static_root} + file_server + header Cache-Control "no-cache, no-store, must-revalidate" + header Pragma "no-cache" + header Expires "0" + } + + handle /*.main.dart.js { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=3600" + } + + handle /*.dart.js { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=3600" + } + + handle /*.js { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=3600" + } + + handle /assets/*.js { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=31536000, immutable" + } + + handle /assets/*.css { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=31536000, immutable" + } + + handle /assets/* { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=31536000, immutable" + } + + handle /canvaskit/* { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=86400" + } + + handle /icons/* { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=86400" + } + + handle /*.html { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=3600" + } + + handle /*.css { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=3600" + } + + handle /*.json { + root * {vars.static_root} + file_server + header Cache-Control "public, max-age=3600" + } + + ######################################## + # SPA fallback + ######################################## + handle { + root * {vars.static_root} + try_files {path} /index.html + file_server + header Cache-Control "no-cache, no-store, must-revalidate" + header Pragma "no-cache" + header Expires "0" + } + } +} + +######################################## +# Main site +######################################## +{$SERVICE_HOST} { + import sendico_site } From b12dbf07ea2ed005bdcc06fb7ad0f47fd5412d99 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 10:33:09 +0100 Subject: [PATCH 13/32] proxy app config fix --- ci/prod/.env.runtime | 2 +- ci/prod/compose/bff.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/prod/.env.runtime b/ci/prod/.env.runtime index feb0f92..42f6500 100644 --- a/ci/prod/.env.runtime +++ b/ci/prod/.env.runtime @@ -16,7 +16,7 @@ AMPLI_ENVIRONMENT=production API_PROTOCOL=https SERVICE_HOST=app.sendico.io API_ENDPOINT=/api/v1 -API_UPSTREAM=sendico-bff:8081 +API_UPSTREAM=sendico-bff:8080 WS_PROTOCOL=wss WS_ENDPOINT=/ws AMPLITUDE_SECRET=c3d75b3e2520d708440acbb16b923e79 diff --git a/ci/prod/compose/bff.yml b/ci/prod/compose/bff.yml index 605f3da..400faf7 100644 --- a/ci/prod/compose/bff.yml +++ b/ci/prod/compose/bff.yml @@ -41,7 +41,7 @@ services: PERMISSION_TIMEOUT: ${PERMISSION_TIMEOUT} PERMISSION_IS_FILTERED: ${PERMISSION_IS_FILTERED} ports: - - "0.0.0.0:${BFF_HTTP_PORT}:8081" + - "0.0.0.0:${BFF_HTTP_PORT}:8080" healthcheck: test: ["CMD-SHELL","wget -qO- http://localhost:8081/api/v1/health | grep -q '\"status\":\"ok\"'"] interval: 30s From c5cdb7d2ae97faad1ef49a77b8ecc198615f2c93 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 17:32:23 +0100 Subject: [PATCH 14/32] +caddy proxy fix +version bump fix --- ci/scripts/common/bump_version.sh | 6 +++++- frontend/pshared/lib/.DS_Store | Bin 0 -> 8196 bytes frontend/pshared/lib/api/.DS_Store | Bin 0 -> 8196 bytes frontend/pshared/lib/models/.DS_Store | Bin 0 -> 8196 bytes frontend/pweb/caddy/Caddyfile | 5 ++--- 5 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 frontend/pshared/lib/.DS_Store create mode 100644 frontend/pshared/lib/api/.DS_Store create mode 100644 frontend/pshared/lib/models/.DS_Store diff --git a/ci/scripts/common/bump_version.sh b/ci/scripts/common/bump_version.sh index c179872..f33acc4 100755 --- a/ci/scripts/common/bump_version.sh +++ b/ci/scripts/common/bump_version.sh @@ -83,6 +83,7 @@ normalize_machine() { s#^[[:alpha:]][[:alnum:]+.-]*://##; s#^[^@]*@##; s#/.*$##; + s#:[0-9]+$##; ' } @@ -95,6 +96,8 @@ if [ -z "${NETRC_MACHINE}" ] && [ -n "${REMOTE_URL}" ]; then fi if [ -n "${NETRC_MACHINE}" ] && [ -n "${NETRC_USERNAME}" ] && [ -n "${NETRC_PASSWORD}" ]; then + MASKED_USER="$(printf '%s' "${NETRC_USERNAME}" | cut -c1-2)***" + echo "[bump-version] configuring credentials for ${NETRC_MACHINE} (user ${MASKED_USER})" NETRC_FILE="${HOME:-/root}/.netrc" { printf 'machine %s\n' "${NETRC_MACHINE}" @@ -102,7 +105,8 @@ if [ -n "${NETRC_MACHINE}" ] && [ -n "${NETRC_USERNAME}" ] && [ -n "${NETRC_PASS printf 'password %s\n' "${NETRC_PASSWORD}" } > "${NETRC_FILE}" chmod 600 "${NETRC_FILE}" - echo "[bump-version] configured credentials for ${NETRC_MACHINE}" +else + echo "[bump-version] no netrc credentials available" fi if [ -n "${REMOTE_URL}" ]; then diff --git a/frontend/pshared/lib/.DS_Store b/frontend/pshared/lib/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..bb09868d183b6d15c70b4c0cb051c0c389c75c9e GIT binary patch literal 8196 zcmeHMU2GIp6u#fIv@>*|0~9H^0~;2?j|KVz0s>*XEeN*KhHdE&g=KeVpd+(0WoLE^ zmXgNA7Z8n48vi0s{zV@|l*Ab2K}4f321)Qi6B7+Qm>5koCTcu0cNSy|4;o`c=O%N{ zJ@=k-?>%R}bMKy6#uyR>t%hM2h|=THct9sO^^Xp! z{1kwwmjd{O+LQ;Zu)f)oQ>oyMb{IMH}OCphKm3|yTdSQ$Zv z0)KVVi~8aWaZbZJ^g!r=$sX{+r;6p6%`~Q$suruo8gWl@ z*d9%~X}91c()ryU*JImerqoVnb{YC;N~vqJEjMjgdfrzsG@V3-`V336M-z6z(i~qm z$p=J9lu}Cl*x34}H4V|`rl#?R=-B$^#)jzXmb=HtC2@9a)rOs^gJ#aMk8?8+90hDz znOP^wZ8Du=8~-LzF;ueY^O8-{)Ear3Ixx6K82Mp194shzqq10lUOea?bKOII|n-PpRdqbqr_R+%w# zmONV?ByU=-vER`118FC#Tbf}V=p8jQcet-;I7Y_Qd#rR$mu4U-U97F2JMYejs&awr z3MJA0LdG~$Fx(?bOceb!qUu6;>Oyt5H`12Zsn!C8W$ImW zk3+_bN}3C|S-dI_M0i^Lp1PAyHH=@cvHf_ungxcxqcT z!?p9?p2;QM3srT%o3K^Wd_(5d&#hH^)jm=AR7@#~VHFL+KfPNX24bxAeq!8mz@StVaS}=*CVYF@#|lcmNI#;|RuZ6c6J#PT(XS#S?fI&*6ET!pnFar*Q^v z;$2+8hxizu;1a&ZH@JeU_#VIFH-QNdrV0^373K>|geIX`Xc0CFn}p3mTu2H%!X9D2 zU<xQ$0;x70WD4f1|+(ou+zptb7_KCa8)BQ-Rn|;Te=;Gz8 zRW?0QlG_L+(XCB<1-FN-=mnDtVOlQsn4|MGyW;bs!yE zBvRxkQv!DBGGaTFD$Yh$tQ09q6~SlaNOOxsv8Yr8?25I-f-~}ljS@wrQW>!ER>?ak z6z21h$d;`%(>X=_y8wTgeZ{V@@7ON{`01F1dMrUB!90O&*x>^`iC*kQ3j2_Oh8!%| zI7E;?ig6sn1Rf&5KSGdy43Fa}JdI}v_AlT?yo6WqDuMnjoWpF^o#2#usQlv} V0_bPkU;o`5-v8nK|KGU(e+NtSd!PUS literal 0 HcmV?d00001 diff --git a/frontend/pshared/lib/api/.DS_Store b/frontend/pshared/lib/api/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0b1593613d25a21200693e8464965637089cfd6b GIT binary patch literal 8196 zcmeHMU2GIp6u#fIz|54|TWF!|2w7x}dyl7czB*_KO8}yx z1PB{FQymc6BqN%R=%}QjDXl512SlcbN(@MIk|%{a(R4&dC6(q3(wrgM8Bv9TaCfpx zhUyGyN#ik&KpcUo5fJI4vLaRPCs^su`aSP>d6IpeA9id%Xl=a;rK)CtrEa<@ejxNR~Hrd2W!iU8@)x*rr7)hx%>T^hP?pl56^* zZAJ=IP1Ulz>GbLKZR=9W)$JS3q>`sMtZq#u*RE+lb4F7en;+Y_FMHe>a=o+C6-2KC zRy)bHbJO|d_~kC?S5>0o*R_gYnqFh{4;;|5d`_7Tzk4{aJ=foF6@8*vUgwjO+Y6q% zr)UXzf5CG~L$06Y^@W^MC^@;n+Uz(P`#H-W2&|KVcpCJ*V$c;T4%lQX=CYiZd~2UY z26ox%CNsZb(&3?9oZYx^@sgI-wvJ8R+js39uYIVF*XwijL5h?c*hg%ucp&E&EZ4N% zqkSW`864{`+rFK5tX?-aWNG!tXydi><~J=!7=~21ZBS7iDCO-FB|A99n^iU3W08^6 z51DkPU7jg+F;)I4twXyv8%g7!kaQI-8qLUHsqv`Z>yuk5&q>9UzEbZ!PFJjPU(T%3 z*BE_HE^j#+H|0L<`g)_kEMu$jopN%c{)915@P>xSMV~xK$?3GwX*?r#ek$h&yN4}T zoN7$UdW@~cpnoh^w04gW%2a+u&hMc4@Xkmg%z8{a@QUKh6q2WthH*eFxY;yAN1Eo% zuQf78pUS^e>3VFpY!kNW+9kx*=JaxTLt$*2)2))zD03FDmGp($#tu>L46{@0JbRm6 zU>~v1*ckhUeao)1AJ~uV2K$Nq%6?~mqZ+lS14lg)Sc+v>juxy%8k^CHo!EoD$ePE40Mi7i{V-5;5A zeRh~DbLKWQE^1!3Vs-n*^es$D5LR$^Cz?YDPXK-h;qbmEVQ)N4D*fSwl`OiQS2EA~ z2>aGaRREaJinJeDtP=M5jEG&bl-QkoreqT>DV6e!DC2`>|DU&HHo18?F2 z-XQ?L9|8Cid^rh_J0=1Wm!<<`Ia72U&pk@A3Xmt7#oRq3P$e~*ME&1$_uv2TxlXJQ zM<9;C|2G1t?#^^~()sW9CKa`IlKQjMMH70Xl7=QU2o*6Omg7Wk{lk!+laVYP(NRfB YLhV2QA;5{h|I_`+8h`&oI6g4LKc_`5`Tzg` literal 0 HcmV?d00001 diff --git a/frontend/pshared/lib/models/.DS_Store b/frontend/pshared/lib/models/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f9c58c74ecb848849d72d478deb76bdf7bdec629 GIT binary patch literal 8196 zcmeHMU2GIp6u#fIz|54&6j~@dLPnYZ(gl~lT8hfjAAUuCwxy+j>+H?|6Q(m|XLbt` zV%7Kp{w2ovB>qGnB{4>0j2QYx^hr|*yl8@nFZy7N`ecIV&YcDNLx?X1!`x)FGj4^bUt<{V*Gsa}PxYX*Xo2GF)zos-P9;qb>if7DZ1?DolFcJ(;@lFO3 z1QG-i1QG-i1QG=91qA4u&6|9O@4m1meS$!OzahZ+fsF2aCgSzMlK$0H; zqDJ?04v1omSY~56Dr6K&&lINz#G#0f7!cuPH@G{=Y%E8G6yXdaoFSeW@d*Xd>=b82 z?hIKWlRiNpL0~EZynJL9u#owzbo=?e;Q9r!eO?%LogmC)u0X1(oiTHkJWH;V59f~g zBe~EF%R#4EI>@zN-**eud$X|Lu}AV+W0&uRrsLVAD8R97ayiuRc$PoX>6bk#h+=cX zAS<$x*P2hCZf|c(8*A2O&ZLdg8`fmf#=5m_XU-^cQ_CY8_vMefL!N(Dq=NW$!0IQZ zc5b@7+@R7W@~TNy^SWBiOHu3e{(%E(UYlE?#qS;t9p4M~+ogbHUYiq8lG}^Ex2I%t zdwOIb5DT3xI6 zxn{w370nX!w5jcSe??%cXgkH=M)h%hpy&?`QHlX&kQT#Py;Fah&-|nrgu91rkFRP> z*n0G>`e1O(EZMt9h-I>NS&Z+X@o3M;!ozwjC-h5v%>>C)hOQsr58P~7Q6kOr7u4%H zy-(J@lWBV#ui_B5Y1;+G<(BLUu|pBIE!m9F6snwsY!!W>wy{IZX2a|hdzrn>&a;o$ zXKajp!@gzL*bnSScAfpier3P2zfp^N%z=gmq_7Oju>!4Fhb%Uu6Fad7dyzvg4x@k~ zY#f1)GAcNUQ+NSqaSpHGRlJ3F@g6?Fhq!=?_zIUWhOhBGuHY)J;X3}npZE(mBt=rC zI;l}=k`_qKQmd4f)=M2ymsq@1t(=1s$tQJ_KIM%-l`||VM>;WG<)pT3-F9zO&b8T5 zt<0U**tEE1`N}nI8?!e#B}iE1-R*3SC_I7q5rw1up2WTJsHoHj7gek1R$YlY>m%;l zCuL4xZC334&=Q%rug!?nrOQa&sm&B>sx>WBooP~}s;SIsg{n!biPhHiq(Ug_h7N`5 zN~?|4CpIa()HN3AsZ>`tJ!wpn|2<_tW?!&N>@piC%KprLVZX5(kcp@%7+4xnwG}JT zhDXtk4m^e)Y{6D+C(`z!5Bo8IL87chq&*4;B?JhGxX)n}&*MeBM6`Vkuj388iSu}e z2>gCb;7{=7Btq_(NJw0qj*yjH$#Z@0DA}q+o*0&BcSWE^cr^3#f6twN|G(>V5{CqV z1cCoA0;uiIb$8PG@AM|+XKjP7XXxUM`;7`2g;2*`_;_?2CwcQ9hIDVlwrng%g(M5L X|NMslH~Icg_a|%e{g3E)-*o=~BIYh8 literal 0 HcmV?d00001 diff --git a/frontend/pweb/caddy/Caddyfile b/frontend/pweb/caddy/Caddyfile index 7d7e91a..84a79f7 100644 --- a/frontend/pweb/caddy/Caddyfile +++ b/frontend/pweb/caddy/Caddyfile @@ -11,8 +11,6 @@ ######################################## (sendico_site) { vars static_root /usr/share/pweb - # Allow overriding upstream via API_UPSTREAM, default to the BFF container. - vars api_upstream {$API_UPSTREAM:sendico-bff:8081} encode zstd gzip @@ -25,7 +23,8 @@ # Backend API ######################################## handle /api/v1/* { - reverse_proxy {vars.api_upstream} { + # Allow overriding upstream via API_UPSTREAM, default to the BFF container. + reverse_proxy {$API_UPSTREAM:sendico-bff:8081} { health_uri /api/v1/health health_interval 15s health_timeout 4s From 71a67e1f6dfca89d6dbe1753e140722f63e37a71 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 17:33:01 +0100 Subject: [PATCH 15/32] removed trash --- ci/scripts/common/bump_version.sh | 1 + frontend/pshared/lib/.DS_Store | Bin 8196 -> 0 bytes frontend/pshared/lib/api/.DS_Store | Bin 8196 -> 0 bytes frontend/pshared/lib/models/.DS_Store | Bin 8196 -> 0 bytes 4 files changed, 1 insertion(+) delete mode 100644 frontend/pshared/lib/.DS_Store delete mode 100644 frontend/pshared/lib/api/.DS_Store delete mode 100644 frontend/pshared/lib/models/.DS_Store diff --git a/ci/scripts/common/bump_version.sh b/ci/scripts/common/bump_version.sh index f33acc4..a330aca 100755 --- a/ci/scripts/common/bump_version.sh +++ b/ci/scripts/common/bump_version.sh @@ -110,6 +110,7 @@ else fi if [ -n "${REMOTE_URL}" ]; then + echo "[bump-version] using remote ${REMOTE_URL}" git remote set-url origin "${REMOTE_URL}" fi diff --git a/frontend/pshared/lib/.DS_Store b/frontend/pshared/lib/.DS_Store deleted file mode 100644 index bb09868d183b6d15c70b4c0cb051c0c389c75c9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMU2GIp6u#fIv@>*|0~9H^0~;2?j|KVz0s>*XEeN*KhHdE&g=KeVpd+(0WoLE^ zmXgNA7Z8n48vi0s{zV@|l*Ab2K}4f321)Qi6B7+Qm>5koCTcu0cNSy|4;o`c=O%N{ zJ@=k-?>%R}bMKy6#uyR>t%hM2h|=THct9sO^^Xp! z{1kwwmjd{O+LQ;Zu)f)oQ>oyMb{IMH}OCphKm3|yTdSQ$Zv z0)KVVi~8aWaZbZJ^g!r=$sX{+r;6p6%`~Q$suruo8gWl@ z*d9%~X}91c()ryU*JImerqoVnb{YC;N~vqJEjMjgdfrzsG@V3-`V336M-z6z(i~qm z$p=J9lu}Cl*x34}H4V|`rl#?R=-B$^#)jzXmb=HtC2@9a)rOs^gJ#aMk8?8+90hDz znOP^wZ8Du=8~-LzF;ueY^O8-{)Ear3Ixx6K82Mp194shzqq10lUOea?bKOII|n-PpRdqbqr_R+%w# zmONV?ByU=-vER`118FC#Tbf}V=p8jQcet-;I7Y_Qd#rR$mu4U-U97F2JMYejs&awr z3MJA0LdG~$Fx(?bOceb!qUu6;>Oyt5H`12Zsn!C8W$ImW zk3+_bN}3C|S-dI_M0i^Lp1PAyHH=@cvHf_ungxcxqcT z!?p9?p2;QM3srT%o3K^Wd_(5d&#hH^)jm=AR7@#~VHFL+KfPNX24bxAeq!8mz@StVaS}=*CVYF@#|lcmNI#;|RuZ6c6J#PT(XS#S?fI&*6ET!pnFar*Q^v z;$2+8hxizu;1a&ZH@JeU_#VIFH-QNdrV0^373K>|geIX`Xc0CFn}p3mTu2H%!X9D2 zU<xQ$0;x70WD4f1|+(ou+zptb7_KCa8)BQ-Rn|;Te=;Gz8 zRW?0QlG_L+(XCB<1-FN-=mnDtVOlQsn4|MGyW;bs!yE zBvRxkQv!DBGGaTFD$Yh$tQ09q6~SlaNOOxsv8Yr8?25I-f-~}ljS@wrQW>!ER>?ak z6z21h$d;`%(>X=_y8wTgeZ{V@@7ON{`01F1dMrUB!90O&*x>^`iC*kQ3j2_Oh8!%| zI7E;?ig6sn1Rf&5KSGdy43Fa}JdI}v_AlT?yo6WqDuMnjoWpF^o#2#usQlv} V0_bPkU;o`5-v8nK|KGU(e+NtSd!PUS diff --git a/frontend/pshared/lib/api/.DS_Store b/frontend/pshared/lib/api/.DS_Store deleted file mode 100644 index 0b1593613d25a21200693e8464965637089cfd6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMU2GIp6u#fIz|54|TWF!|2w7x}dyl7czB*_KO8}yx z1PB{FQymc6BqN%R=%}QjDXl512SlcbN(@MIk|%{a(R4&dC6(q3(wrgM8Bv9TaCfpx zhUyGyN#ik&KpcUo5fJI4vLaRPCs^su`aSP>d6IpeA9id%Xl=a;rK)CtrEa<@ejxNR~Hrd2W!iU8@)x*rr7)hx%>T^hP?pl56^* zZAJ=IP1Ulz>GbLKZR=9W)$JS3q>`sMtZq#u*RE+lb4F7en;+Y_FMHe>a=o+C6-2KC zRy)bHbJO|d_~kC?S5>0o*R_gYnqFh{4;;|5d`_7Tzk4{aJ=foF6@8*vUgwjO+Y6q% zr)UXzf5CG~L$06Y^@W^MC^@;n+Uz(P`#H-W2&|KVcpCJ*V$c;T4%lQX=CYiZd~2UY z26ox%CNsZb(&3?9oZYx^@sgI-wvJ8R+js39uYIVF*XwijL5h?c*hg%ucp&E&EZ4N% zqkSW`864{`+rFK5tX?-aWNG!tXydi><~J=!7=~21ZBS7iDCO-FB|A99n^iU3W08^6 z51DkPU7jg+F;)I4twXyv8%g7!kaQI-8qLUHsqv`Z>yuk5&q>9UzEbZ!PFJjPU(T%3 z*BE_HE^j#+H|0L<`g)_kEMu$jopN%c{)915@P>xSMV~xK$?3GwX*?r#ek$h&yN4}T zoN7$UdW@~cpnoh^w04gW%2a+u&hMc4@Xkmg%z8{a@QUKh6q2WthH*eFxY;yAN1Eo% zuQf78pUS^e>3VFpY!kNW+9kx*=JaxTLt$*2)2))zD03FDmGp($#tu>L46{@0JbRm6 zU>~v1*ckhUeao)1AJ~uV2K$Nq%6?~mqZ+lS14lg)Sc+v>juxy%8k^CHo!EoD$ePE40Mi7i{V-5;5A zeRh~DbLKWQE^1!3Vs-n*^es$D5LR$^Cz?YDPXK-h;qbmEVQ)N4D*fSwl`OiQS2EA~ z2>aGaRREaJinJeDtP=M5jEG&bl-QkoreqT>DV6e!DC2`>|DU&HHo18?F2 z-XQ?L9|8Cid^rh_J0=1Wm!<<`Ia72U&pk@A3Xmt7#oRq3P$e~*ME&1$_uv2TxlXJQ zM<9;C|2G1t?#^^~()sW9CKa`IlKQjMMH70Xl7=QU2o*6Omg7Wk{lk!+laVYP(NRfB YLhV2QA;5{h|I_`+8h`&oI6g4LKc_`5`Tzg` diff --git a/frontend/pshared/lib/models/.DS_Store b/frontend/pshared/lib/models/.DS_Store deleted file mode 100644 index f9c58c74ecb848849d72d478deb76bdf7bdec629..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMU2GIp6u#fIz|54&6j~@dLPnYZ(gl~lT8hfjAAUuCwxy+j>+H?|6Q(m|XLbt` zV%7Kp{w2ovB>qGnB{4>0j2QYx^hr|*yl8@nFZy7N`ecIV&YcDNLx?X1!`x)FGj4^bUt<{V*Gsa}PxYX*Xo2GF)zos-P9;qb>if7DZ1?DolFcJ(;@lFO3 z1QG-i1QG-i1QG=91qA4u&6|9O@4m1meS$!OzahZ+fsF2aCgSzMlK$0H; zqDJ?04v1omSY~56Dr6K&&lINz#G#0f7!cuPH@G{=Y%E8G6yXdaoFSeW@d*Xd>=b82 z?hIKWlRiNpL0~EZynJL9u#owzbo=?e;Q9r!eO?%LogmC)u0X1(oiTHkJWH;V59f~g zBe~EF%R#4EI>@zN-**eud$X|Lu}AV+W0&uRrsLVAD8R97ayiuRc$PoX>6bk#h+=cX zAS<$x*P2hCZf|c(8*A2O&ZLdg8`fmf#=5m_XU-^cQ_CY8_vMefL!N(Dq=NW$!0IQZ zc5b@7+@R7W@~TNy^SWBiOHu3e{(%E(UYlE?#qS;t9p4M~+ogbHUYiq8lG}^Ex2I%t zdwOIb5DT3xI6 zxn{w370nX!w5jcSe??%cXgkH=M)h%hpy&?`QHlX&kQT#Py;Fah&-|nrgu91rkFRP> z*n0G>`e1O(EZMt9h-I>NS&Z+X@o3M;!ozwjC-h5v%>>C)hOQsr58P~7Q6kOr7u4%H zy-(J@lWBV#ui_B5Y1;+G<(BLUu|pBIE!m9F6snwsY!!W>wy{IZX2a|hdzrn>&a;o$ zXKajp!@gzL*bnSScAfpier3P2zfp^N%z=gmq_7Oju>!4Fhb%Uu6Fad7dyzvg4x@k~ zY#f1)GAcNUQ+NSqaSpHGRlJ3F@g6?Fhq!=?_zIUWhOhBGuHY)J;X3}npZE(mBt=rC zI;l}=k`_qKQmd4f)=M2ymsq@1t(=1s$tQJ_KIM%-l`||VM>;WG<)pT3-F9zO&b8T5 zt<0U**tEE1`N}nI8?!e#B}iE1-R*3SC_I7q5rw1up2WTJsHoHj7gek1R$YlY>m%;l zCuL4xZC334&=Q%rug!?nrOQa&sm&B>sx>WBooP~}s;SIsg{n!biPhHiq(Ug_h7N`5 zN~?|4CpIa()HN3AsZ>`tJ!wpn|2<_tW?!&N>@piC%KprLVZX5(kcp@%7+4xnwG}JT zhDXtk4m^e)Y{6D+C(`z!5Bo8IL87chq&*4;B?JhGxX)n}&*MeBM6`Vkuj388iSu}e z2>gCb;7{=7Btq_(NJw0qj*yjH$#Z@0DA}q+o*0&BcSWE^cr^3#f6twN|G(>V5{CqV z1cCoA0;uiIb$8PG@AM|+XKjP7XXxUM`;7`2g;2*`_;_?2CwcQ9hIDVlwrng%g(M5L X|NMslH~Icg_a|%e{g3E)-*o=~BIYh8 From 515887d7f880b5fb4c7dd9f55eac1fd7170c1ae5 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 18:40:41 +0100 Subject: [PATCH 16/32] config fix --- ci/prod/.env.runtime | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/prod/.env.runtime b/ci/prod/.env.runtime index 42f6500..4b3c058 100644 --- a/ci/prod/.env.runtime +++ b/ci/prod/.env.runtime @@ -122,7 +122,7 @@ CADDY_ACME_EMAIL=infra@sendico.io BFF_DIR=bff BFF_COMPOSE_PROJECT=sendico-bff BFF_SERVICE_NAME=sendico_bff -BFF_HTTP_PORT=8080 +BFF_HTTP_PORT=8081 # Chain gateway stack CHAIN_GATEWAY_DIR=chain_gateway From 020a40b90caaf933fced5d9c6b48e5b756fd539f Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 18:52:23 +0100 Subject: [PATCH 17/32] Config fix --- ci/prod/.env.runtime | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/prod/.env.runtime b/ci/prod/.env.runtime index 4b3c058..42f6500 100644 --- a/ci/prod/.env.runtime +++ b/ci/prod/.env.runtime @@ -122,7 +122,7 @@ CADDY_ACME_EMAIL=infra@sendico.io BFF_DIR=bff BFF_COMPOSE_PROJECT=sendico-bff BFF_SERVICE_NAME=sendico_bff -BFF_HTTP_PORT=8081 +BFF_HTTP_PORT=8080 # Chain gateway stack CHAIN_GATEWAY_DIR=chain_gateway From c3a4a3cc1b5df31cedab87f7df328839adacc989 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 19:19:05 +0100 Subject: [PATCH 18/32] Config fix --- ci/prod/.env.runtime | 3 +-- ci/prod/compose/bff.yml | 2 +- ci/prod/compose/frontend.yml | 1 - frontend/pweb/caddy/Caddyfile | 4 ++-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ci/prod/.env.runtime b/ci/prod/.env.runtime index 42f6500..d6c368c 100644 --- a/ci/prod/.env.runtime +++ b/ci/prod/.env.runtime @@ -16,7 +16,6 @@ AMPLI_ENVIRONMENT=production API_PROTOCOL=https SERVICE_HOST=app.sendico.io API_ENDPOINT=/api/v1 -API_UPSTREAM=sendico-bff:8080 WS_PROTOCOL=wss WS_ENDPOINT=/ws AMPLITUDE_SECRET=c3d75b3e2520d708440acbb16b923e79 @@ -122,7 +121,7 @@ CADDY_ACME_EMAIL=infra@sendico.io BFF_DIR=bff BFF_COMPOSE_PROJECT=sendico-bff BFF_SERVICE_NAME=sendico_bff -BFF_HTTP_PORT=8080 +BFF_HTTP_PORT=8081 # Chain gateway stack CHAIN_GATEWAY_DIR=chain_gateway diff --git a/ci/prod/compose/bff.yml b/ci/prod/compose/bff.yml index 400faf7..605f3da 100644 --- a/ci/prod/compose/bff.yml +++ b/ci/prod/compose/bff.yml @@ -41,7 +41,7 @@ services: PERMISSION_TIMEOUT: ${PERMISSION_TIMEOUT} PERMISSION_IS_FILTERED: ${PERMISSION_IS_FILTERED} ports: - - "0.0.0.0:${BFF_HTTP_PORT}:8080" + - "0.0.0.0:${BFF_HTTP_PORT}:8081" healthcheck: test: ["CMD-SHELL","wget -qO- http://localhost:8081/api/v1/health | grep -q '\"status\":\"ok\"'"] interval: 30s diff --git a/ci/prod/compose/frontend.yml b/ci/prod/compose/frontend.yml index 37ea36b..9a1ef16 100644 --- a/ci/prod/compose/frontend.yml +++ b/ci/prod/compose/frontend.yml @@ -23,7 +23,6 @@ services: API_PROTOCOL: ${API_PROTOCOL} SERVICE_HOST: ${SERVICE_HOST} API_ENDPOINT: ${API_ENDPOINT} - API_UPSTREAM: ${API_UPSTREAM} AMPLITUDE_SECRET: ${AMPLITUDE_SECRET} DEFAULT_LOCALE: ${DEFAULT_LOCALE} DEFAULT_CURRENCY: ${DEFAULT_CURRENCY} diff --git a/frontend/pweb/caddy/Caddyfile b/frontend/pweb/caddy/Caddyfile index 84a79f7..1df6dbd 100644 --- a/frontend/pweb/caddy/Caddyfile +++ b/frontend/pweb/caddy/Caddyfile @@ -23,8 +23,8 @@ # Backend API ######################################## handle /api/v1/* { - # Allow overriding upstream via API_UPSTREAM, default to the BFF container. - reverse_proxy {$API_UPSTREAM:sendico-bff:8081} { + # Proxy directly to the BFF container on the Docker network. + reverse_proxy sendico-bff:8081 { health_uri /api/v1/health health_interval 15s health_timeout 4s From 796e7b63b9cb0a77cd69327756ce8baacf93febf Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 18 Nov 2025 19:31:08 +0100 Subject: [PATCH 19/32] Config fix --- ci/prod/.env.runtime | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/prod/.env.runtime b/ci/prod/.env.runtime index d6c368c..13712ad 100644 --- a/ci/prod/.env.runtime +++ b/ci/prod/.env.runtime @@ -121,7 +121,7 @@ CADDY_ACME_EMAIL=infra@sendico.io BFF_DIR=bff BFF_COMPOSE_PROJECT=sendico-bff BFF_SERVICE_NAME=sendico_bff -BFF_HTTP_PORT=8081 +BFF_HTTP_PORT=8080 # Chain gateway stack CHAIN_GATEWAY_DIR=chain_gateway From 26a849e582bad1de30b2ddc74076208c74330bd0 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 19 Nov 2025 12:34:44 +0100 Subject: [PATCH 20/32] version bump --- .gitignore | 2 + api/billing/fees/go.mod | 4 +- api/billing/fees/go.sum | 4 + api/chain/gateway/go.mod | 6 +- api/chain/gateway/go.sum | 6 + api/fx/ingestor/go.mod | 4 +- api/fx/ingestor/go.sum | 4 + api/fx/oracle/go.mod | 4 +- api/fx/oracle/go.sum | 4 + api/ledger/go.mod | 4 +- api/ledger/go.sum | 4 + api/notification/go.mod | 4 +- api/notification/go.sum | 4 + api/payments/orchestrator/go.mod | 4 +- api/payments/orchestrator/go.sum | 4 + api/pkg/go.mod | 12 +- api/pkg/go.sum | 8 + ci/scripts/common/bump_version.sh | 239 ++- frontend/.DS_Store | Bin 8196 -> 0 bytes frontend/pshared/lib/api/requests/.DS_Store | Bin 6148 -> 0 bytes frontend/pshared/lib/data/mapper/.DS_Store | Bin 6148 -> 0 bytes .../pshared/lib/models/storable/.DS_Store | Bin 6148 -> 0 bytes frontend/pweb/pubspec.lock | 1334 ----------------- 23 files changed, 254 insertions(+), 1401 deletions(-) delete mode 100644 frontend/.DS_Store delete mode 100644 frontend/pshared/lib/api/requests/.DS_Store delete mode 100644 frontend/pshared/lib/data/mapper/.DS_Store delete mode 100644 frontend/pshared/lib/models/storable/.DS_Store delete mode 100644 frontend/pweb/pubspec.lock diff --git a/.gitignore b/.gitignore index 7ee1fdd..d73c05b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .env.version *.pb.go *.pb.gw.go +pubspec.lock +.DS_Store diff --git a/api/billing/fees/go.mod b/api/billing/fees/go.mod index 1c5aea6..10bae4f 100644 --- a/api/billing/fees/go.mod +++ b/api/billing/fees/go.mod @@ -11,7 +11,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver v1.17.6 go.uber.org/zap v1.27.0 - google.golang.org/grpc v1.76.0 + google.golang.org/grpc v1.77.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -36,7 +36,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect diff --git a/api/billing/fees/go.sum b/api/billing/fees/go.sum index b00a4ba..13a4a5d 100644 --- a/api/billing/fees/go.sum +++ b/api/billing/fees/go.sum @@ -119,6 +119,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -218,6 +220,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/chain/gateway/go.mod b/api/chain/gateway/go.mod index 91cf4f5..3f79cec 100644 --- a/api/chain/gateway/go.mod +++ b/api/chain/gateway/go.mod @@ -15,14 +15,14 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver v1.17.6 go.uber.org/zap v1.27.0 - google.golang.org/grpc v1.76.0 + google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251117160429-c598d23eddcf // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect @@ -65,7 +65,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect diff --git a/api/chain/gateway/go.sum b/api/chain/gateway/go.sum index 9fdd6f2..704ef47 100644 --- a/api/chain/gateway/go.sum +++ b/api/chain/gateway/go.sum @@ -10,6 +10,8 @@ github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254 github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251117160429-c598d23eddcf h1:aZI2VRIP0LAI6Rw934WEAxxL0SNYSVt9vR9h/cP5Pbo= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251117160429-c598d23eddcf/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7 h1:uups37roJCTtR/BrJa0WoMrxt3rzgV+Qrj+TxYyJoAo= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -247,6 +249,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= @@ -376,6 +380,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/fx/ingestor/go.mod b/api/fx/ingestor/go.mod index 21a9959..c856265 100644 --- a/api/fx/ingestor/go.mod +++ b/api/fx/ingestor/go.mod @@ -35,7 +35,7 @@ require ( github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect @@ -50,6 +50,6 @@ require ( golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/api/fx/ingestor/go.sum b/api/fx/ingestor/go.sum index b1ed329..53148e5 100644 --- a/api/fx/ingestor/go.sum +++ b/api/fx/ingestor/go.sum @@ -119,6 +119,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -230,6 +232,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/fx/oracle/go.mod b/api/fx/oracle/go.mod index 5b6e650..1659166 100644 --- a/api/fx/oracle/go.mod +++ b/api/fx/oracle/go.mod @@ -13,7 +13,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver v1.17.6 go.uber.org/zap v1.27.0 - google.golang.org/grpc v1.76.0 + google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 ) @@ -37,7 +37,7 @@ require ( github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect diff --git a/api/fx/oracle/go.sum b/api/fx/oracle/go.sum index b00a4ba..13a4a5d 100644 --- a/api/fx/oracle/go.sum +++ b/api/fx/oracle/go.sum @@ -119,6 +119,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -218,6 +220,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/ledger/go.mod b/api/ledger/go.mod index c015888..12767dd 100644 --- a/api/ledger/go.mod +++ b/api/ledger/go.mod @@ -11,7 +11,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver v1.17.6 go.uber.org/zap v1.27.0 - google.golang.org/grpc v1.76.0 + google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 ) @@ -38,7 +38,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect diff --git a/api/ledger/go.sum b/api/ledger/go.sum index 7fa6056..4ae6767 100644 --- a/api/ledger/go.sum +++ b/api/ledger/go.sum @@ -119,6 +119,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -220,6 +222,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/notification/go.mod b/api/notification/go.mod index 89a3e9f..4f18fef 100644 --- a/api/notification/go.mod +++ b/api/notification/go.mod @@ -38,7 +38,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect @@ -53,6 +53,6 @@ require ( golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/api/notification/go.sum b/api/notification/go.sum index ff04c50..acaab3e 100644 --- a/api/notification/go.sum +++ b/api/notification/go.sum @@ -125,6 +125,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -233,6 +235,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/payments/orchestrator/go.mod b/api/payments/orchestrator/go.mod index 6ac7d6d..5973b59 100644 --- a/api/payments/orchestrator/go.mod +++ b/api/payments/orchestrator/go.mod @@ -21,7 +21,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver v1.17.6 go.uber.org/zap v1.27.0 - google.golang.org/grpc v1.76.0 + google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 ) @@ -46,7 +46,7 @@ require ( github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect diff --git a/api/payments/orchestrator/go.sum b/api/payments/orchestrator/go.sum index 0468119..4babef9 100644 --- a/api/payments/orchestrator/go.sum +++ b/api/payments/orchestrator/go.sum @@ -119,6 +119,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -221,6 +223,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/pkg/go.mod b/api/pkg/go.mod index 0147599..e96d7fa 100644 --- a/api/pkg/go.mod +++ b/api/pkg/go.mod @@ -17,7 +17,7 @@ require ( go.mongodb.org/mongo-driver v1.17.6 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.44.0 - google.golang.org/grpc v1.76.0 + google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 ) @@ -67,7 +67,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -80,12 +80,12 @@ require ( github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + 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.47.0 // indirect diff --git a/api/pkg/go.sum b/api/pkg/go.sum index daeed6a..d3150a6 100644 --- a/api/pkg/go.sum +++ b/api/pkg/go.sum @@ -130,6 +130,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -174,22 +176,26 @@ go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUps go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -275,6 +281,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/ci/scripts/common/bump_version.sh b/ci/scripts/common/bump_version.sh index a330aca..eba26d1 100755 --- a/ci/scripts/common/bump_version.sh +++ b/ci/scripts/common/bump_version.sh @@ -1,8 +1,167 @@ #!/bin/sh set -eu +log() { + printf '[bump-version] %s\n' "$*" +} + +error() { + printf '[bump-version] %s\n' "$*" >&2 +} + +die() { + error "${1:-fatal error}" + exit "${2:-1}" +} + +var_value() { + # shellcheck disable=SC2039 + eval "printf '%s' \"\${$1:-}\"" +} + +normalize_host() { + value="$1" + if [ -z "${value}" ]; then + printf '%s' "" + return + fi + printf '%s' "${value}" | sed -E ' + s#^[[:alpha:]][[:alnum:]+.-]*://##; + s#^[^@]*@##; + s#[:/].*$##; + ' +} + +extract_proto() { + url="$1" + if printf '%s' "${url}" | grep -Eq '^[[:alpha:]][[:alnum:]+.-]*://'; then + printf '%s' "${url}" | sed -E 's#^([[:alpha:]][[:alnum:]+.-]*://).*#\1#' | tr -d ':/' + return + fi + if printf '%s' "${url}" | grep -Eq '^[^@]+@[^:]+:'; then + printf 'ssh' + return + fi + printf 'https' +} + +discover_env_credentials() { + DEFAULT_GIT_USER="${GIT_AUTH_DEFAULT_USER:-woodpecker}" + while IFS=: read -r user_var pass_var; do + [ -n "${pass_var}" ] || continue + pass="$(var_value "${pass_var}")" + [ -n "${pass}" ] || continue + if [ "${user_var}" = "-" ] || [ -z "${user_var}" ]; then + user="" + else + user="$(var_value "${user_var}")" + fi + if [ -z "${user}" ]; then + user="${DEFAULT_GIT_USER}" + fi + GIT_AUTH_USER="${user}" + GIT_AUTH_PASS="${pass}" + GIT_AUTH_SOURCE="env:${user_var:--}/${pass_var}" + return 0 + done <<'EOF' +CI_GIT_USERNAME:CI_GIT_PASSWORD +CI_GIT_USERNAME:CI_GIT_TOKEN +CI_GIT_USERNAME:CI_GIT_PASS +CI_GIT_USER:CI_GIT_PASSWORD +CI_GIT_USER:CI_GIT_TOKEN +CI_NETRC_USERNAME:CI_NETRC_PASSWORD +CI_NETRC_LOGIN:CI_NETRC_PASSWORD +WOODPECKER_GIT_USERNAME:WOODPECKER_GIT_PASSWORD +WOODPECKER_GIT_USERNAME:WOODPECKER_GIT_TOKEN +WOODPECKER_NETRC_USERNAME:WOODPECKER_NETRC_PASSWORD +WOODPECKER_NETRC_LOGIN:WOODPECKER_NETRC_PASSWORD +DRONE_GIT_USERNAME:DRONE_GIT_PASSWORD +DRONE_GIT_USERNAME:DRONE_GIT_TOKEN +DRONE_NETRC_USERNAME:DRONE_NETRC_PASSWORD +DRONE_NETRC_LOGIN:DRONE_NETRC_PASSWORD +GIT_USERNAME:GIT_PASSWORD +GIT_USER:GIT_PASS +GIT_AUTH_USERNAME:GIT_AUTH_PASSWORD +-:GIT_PASSWORD +-:GIT_PASS +EOF + + for token_var in CI_GIT_TOKEN WOODPECKER_GIT_TOKEN DRONE_GIT_TOKEN GIT_TOKEN GITEA_TOKEN; do + token="$(var_value "${token_var}")" + if [ -n "${token}" ]; then + user="$(var_value CI_GIT_USERNAME)" + [ -n "${user}" ] || user="$(var_value WOODPECKER_GIT_USERNAME)" + [ -n "${user}" ] || user="$(var_value DRONE_GIT_USERNAME)" + [ -n "${user}" ] || user="${DEFAULT_GIT_USER}" + GIT_AUTH_USER="${user}" + GIT_AUTH_PASS="${token}" + GIT_AUTH_SOURCE="env:${token_var}" + return 0 + fi + done + return 1 +} + +discover_vault_credentials() { + VAULT_MOUNT="${GIT_CREDENTIALS_VAULT_MOUNT:-kv}" + VAULT_PATH="${GIT_CREDENTIALS_VAULT_PATH:-${CI_GIT_VAULT_PATH:-}}" + [ -n "${VAULT_PATH}" ] || return 1 + VLT_BIN="${GIT_VAULT_HELPER:-./ci/vlt}" + if [ ! -x "${VLT_BIN}" ]; then + return 1 + fi + user_field="${GIT_CREDENTIALS_VAULT_USER_FIELD:-username}" + pass_field="${GIT_CREDENTIALS_VAULT_PASSWORD_FIELD:-password}" + if ! GIT_AUTH_USER="$("${VLT_BIN}" kv_get "${VAULT_MOUNT}" "${VAULT_PATH}" "${user_field}" 2>/dev/null)"; then + return 1 + fi + if ! GIT_AUTH_PASS="$("${VLT_BIN}" kv_get "${VAULT_MOUNT}" "${VAULT_PATH}" "${pass_field}" 2>/dev/null)"; then + return 1 + fi + GIT_AUTH_SOURCE="vault:${VAULT_MOUNT}/${VAULT_PATH}" + return 0 +} + +write_netrc() { + host="$1" + user="$2" + pass="$3" + file="${HOME:-/root}/.netrc" + { + printf 'machine %s\n' "${host}" + printf 'login %s\n' "${user}" + printf 'password %s\n' "${pass}" + } > "${file}" + chmod 600 "${file}" +} + +setup_https_credentials() { + host="$1" + netrc="${HOME:-/root}/.netrc" + if [ -s "${netrc}" ] && awk -v host="${host}" ' + $1 == "machine" && $2 == host { found=1; exit } + END { exit found ? 0 : 1 } + ' "${netrc}"; then + log "reusing credentials already present in ${netrc}" + return 0 + fi + if discover_env_credentials; then + masked="$(printf '%s' "${GIT_AUTH_USER}" | cut -c1-2)***" + log "using ${GIT_AUTH_SOURCE} for ${host} (user ${masked})" + write_netrc "${host}" "${GIT_AUTH_USER}" "${GIT_AUTH_PASS}" + return 0 + fi + if discover_vault_credentials; then + masked="$(printf '%s' "${GIT_AUTH_USER}" | cut -c1-2)***" + log "using ${GIT_AUTH_SOURCE} for ${host} (user ${masked})" + write_netrc "${host}" "${GIT_AUTH_USER}" "${GIT_AUTH_PASS}" + return 0 + fi + return 1 +} + START_DIR="$(pwd)" -echo "[bump-version] invoked from ${START_DIR}" +log "invoked from ${START_DIR}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="" @@ -12,17 +171,16 @@ fi if [ -z "${REPO_ROOT}" ]; then REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" fi -echo "[bump-version] repo root resolved to ${REPO_ROOT}" +log "repo root resolved to ${REPO_ROOT}" cd "${REPO_ROOT}" VERSION_FILE="./version" if [ ! -f "${VERSION_FILE}" ]; then if git cat-file -e "HEAD:version" 2>/dev/null; then - echo "[bump-version] version file missing in workspace, restoring from HEAD" >&2 + error "version file missing in workspace, restoring from HEAD" git show "HEAD:version" > "${VERSION_FILE}" else - echo "[bump-version] version file not found: ${VERSION_FILE}" >&2 - exit 1 + die "version file not found: ${VERSION_FILE}" fi fi @@ -47,11 +205,11 @@ NEXT_VERSION="$(printf '%s' "${CURRENT_VERSION}" | awk -F. -v OFS=. ' }')" printf '%s\n' "${NEXT_VERSION}" > "${VERSION_FILE}" -echo "[bump-version] ${CURRENT_VERSION} -> ${NEXT_VERSION}" +log "${CURRENT_VERSION} -> ${NEXT_VERSION}" git add "${VERSION_FILE}" if git diff --cached --quiet; then - echo "[bump-version] no changes staged, skipping commit" + log "no changes staged, skipping commit" exit 0 fi @@ -71,47 +229,36 @@ REMOTE_URL="${CI_REPO_REMOTE:-${WOODPECKER_GIT_REMOTE:-${DRONE_REMOTE_URL:-}}}" if [ -z "${REMOTE_URL}" ]; then REMOTE_URL="$(git config --get remote.origin.url 2>/dev/null || true)" fi +[ -n "${REMOTE_URL}" ] || die "unable to determine git remote url" -# Normalize machine to a bare hostname so .netrc matches HTTPS requests. -normalize_machine() { - value="$1" - if [ -z "${value}" ]; then - printf '%s' "" - return - fi - printf '%s' "${value}" | sed -E ' - s#^[[:alpha:]][[:alnum:]+.-]*://##; - s#^[^@]*@##; - s#/.*$##; - s#:[0-9]+$##; - ' -} +REMOTE_PROTO="$(extract_proto "${REMOTE_URL}")" +REMOTE_HOST="$(normalize_host "${REMOTE_URL}")" +[ -n "${REMOTE_HOST}" ] || die "unable to determine remote host from ${REMOTE_URL}" -NETRC_MACHINE="${CI_NETRC_MACHINE:-${WOODPECKER_NETRC_MACHINE:-${DRONE_NETRC_MACHINE:-}}}" -NETRC_USERNAME="${CI_NETRC_USERNAME:-${WOODPECKER_NETRC_USERNAME:-${DRONE_NETRC_USERNAME:-${CI_NETRC_LOGIN:-${WOODPECKER_NETRC_LOGIN:-${DRONE_NETRC_LOGIN:-}}}}}}" -NETRC_PASSWORD="${CI_NETRC_PASSWORD:-${WOODPECKER_NETRC_PASSWORD:-${DRONE_NETRC_PASSWORD:-}}}" -NETRC_MACHINE="$(normalize_machine "${NETRC_MACHINE}")" -if [ -z "${NETRC_MACHINE}" ] && [ -n "${REMOTE_URL}" ]; then - NETRC_MACHINE="$(normalize_machine "${REMOTE_URL}")" -fi +log "using remote ${REMOTE_URL}" +git remote set-url origin "${REMOTE_URL}" -if [ -n "${NETRC_MACHINE}" ] && [ -n "${NETRC_USERNAME}" ] && [ -n "${NETRC_PASSWORD}" ]; then - MASKED_USER="$(printf '%s' "${NETRC_USERNAME}" | cut -c1-2)***" - echo "[bump-version] configuring credentials for ${NETRC_MACHINE} (user ${MASKED_USER})" - NETRC_FILE="${HOME:-/root}/.netrc" - { - printf 'machine %s\n' "${NETRC_MACHINE}" - printf 'login %s\n' "${NETRC_USERNAME}" - printf 'password %s\n' "${NETRC_PASSWORD}" - } > "${NETRC_FILE}" - chmod 600 "${NETRC_FILE}" -else - echo "[bump-version] no netrc credentials available" -fi - -if [ -n "${REMOTE_URL}" ]; then - echo "[bump-version] using remote ${REMOTE_URL}" - git remote set-url origin "${REMOTE_URL}" -fi +case "${REMOTE_PROTO}" in + ssh) + if [ -n "${GIT_SSH_KEY_B64:-}" ]; then + SSH_KEY_FILE="${HOME:-/root}/.ssh/id_ci" + mkdir -p "$(dirname "${SSH_KEY_FILE}")" + printf '%s' "${GIT_SSH_KEY_B64}" | base64 -d > "${SSH_KEY_FILE}" + chmod 600 "${SSH_KEY_FILE}" + export GIT_SSH_COMMAND="ssh -i ${SSH_KEY_FILE} -o StrictHostKeyChecking=no" + log "configured SSH key for git push" + else + log "no SSH key provided, relying on existing agent configuration" + fi + ;; + http|https) + if ! setup_https_credentials "${REMOTE_HOST}"; then + die "no git credentials detected (set CI_GIT_USERNAME/CI_GIT_PASSWORD, WOODPECKER_GIT_USERNAME/WOODPECKER_GIT_PASSWORD, or configure GIT_CREDENTIALS_VAULT_PATH)" + fi + ;; + *) + log "unknown git protocol ${REMOTE_PROTO}, attempting push without extra credentials" + ;; +esac git push origin "HEAD:${BRANCH}" diff --git a/frontend/.DS_Store b/frontend/.DS_Store deleted file mode 100644 index 5f303ac654ba7a4dd59af0b734ea6d0542dae5f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMTWl0n7(U;$&>1?=0a{tG1Dh^Hz!qu&yWEoPO}Q7@mTt>s+1(lGz;veU%x;UN zW@F+Dh{h+4x5$&X$b*U!Uo=q^eN;5T7#}bi^+gkWQF-v6IkV6UBt96UI47C&pZ|8w z{QrFanLV?NF|-%7dl-u_#zeY2s#H^VljiBYYg!WmNhMK`Kg*6d!&%eL5`TW0btnik z5N06EK$w9r17QaK4H=+!w%5d4y!W{_?86L%8Temjz}p|9ba^xp&=RNq)$$+Vzqc6IcSe0-Hcms z+B5lm9@k^rX0|-eWcM2SNLs1wuq`)ZSbE-fFf^S^_V*c z$kN?JU32gFxFpuaR&U&sK57nG_DLQBf^C4!o)XfjiSd|D zv4ek)s7RLM`Kla`G^<*kt@fq*2lmNnWnPhPynEO+Y|GiJ=N+PHrN*HEx8`hXcV74O zeL34K3|UTEnVZX)xq_K-^)}N?8V~6tt8sF9+cbCEj^RzypK|q4*V9c?r3M^3?{@f; zr3{*%TPX!c-=ou{Mp5sish!#>!9hAZXTg%C%bQwSHzm5Z?tI` z@t~pS`!h~Xw=}~#+&f}u?qFZhaEz>}_gI-BU7CwV=~7K(;i5b1Rh1jua44Im3R&Y= z!Elc&F;Vo_h^h_pevR&Fr>%LbXrgkC=AqS>sSWBrPt%dtsn-IB73$q`k3+$WN`@P* zlAGn8qXYp-akylSyiV;kGg;k~6pgPFmp7<=MV^02+0KQVe39?_LY>4T@9+*?@iaH zX?_fgA`5HOq}nSgpNVNjF|49Nu&2k$(J#izk0zE_$%nD`b0vS|CpwmB=3coXOFHr~TUe1cE$Ij-Pae24Gx zGk(V(xF*aH76=hxq0k^S3QfW)p;d?r8-$I*E}>gU2^rytU<;!>0HIuJ2RL8uh2u23 zzI9Nk1)u(evo|lb`un$R-M0O|Q)-vz_{CLQcgKS0vX!gX#y7WJ2O&po8K@KC?_+*a zrTCclF);!ab!@3f#iGmz*ySsT-LA~!Y<<%z zkqT20d{(Y+StC*TDHQ?R)JiNkBX8U!Q6Vao0lPRsEc2QA`pzzz=At70T`0fIuCO22 zFYFqjd=BO#g1gX6*lx!b?DEl_L@y2^jYG&n!w@XkI7T=>fpI*H5*{IxKSnrz0#D)@ zJd5WD>o4JDyn@&8IwAcXoX5NP03YH4J`OPdH6i{dT%CgAon`!TieD$9xR}garfnT2 zSsC?{&2KBNf=3ha&i`Gv{{DYk#T*6`W+2SKwg&1I7+6TPa>ctPDw)TAO8^G{~-ShKefa6fB63Q5qf)ee*y^?Y7YPa diff --git a/frontend/pshared/lib/api/requests/.DS_Store b/frontend/pshared/lib/api/requests/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0J5S@WYtb~M8Qu+pv8<=Q0K`s!INKhnfilpDufke+)DB(TN1hI+>8W2J= zk^RP=kG)Td?GX{3?pG6$v4}KiM5R&@PFGDQ?mPl&$+4G5*{$y1KB#Pc8xS4!9Xw&3hM3JDbv+8%7@t1OvgqCj&em5*jf(_J-x?KvzltAfM4iptF`>OmfVQy&)_RwNRji zs+SmQ;h0bEmmPaU3m5g`Lw)5#@uFpQF|hyu diff --git a/frontend/pshared/lib/models/storable/.DS_Store b/frontend/pshared/lib/models/storable/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0=3.9.0 <4.0.0" - flutter: ">=3.35.1" From 8710d7da530525cd0bd08007f799881bdd4842f7 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 19 Nov 2025 12:47:45 +0100 Subject: [PATCH 21/32] cleanup --- api/server/go.mod | 2 +- api/server/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/server/go.mod b/api/server/go.mod index 318e12e..4618b51 100644 --- a/api/server/go.mod +++ b/api/server/go.mod @@ -104,7 +104,7 @@ require ( github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/shirou/gopsutil/v3 v3.24.5 // indirect diff --git a/api/server/go.sum b/api/server/go.sum index a9a3bab..6c9922f 100644 --- a/api/server/go.sum +++ b/api/server/go.sum @@ -196,8 +196,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= From 62956b06ca5a019524ada97bf29e5f3abfd61ecb Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 19 Nov 2025 13:32:40 +0100 Subject: [PATCH 22/32] better logging --- .../server/notificationimp/telegram/client.go | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go index 030264c..07356bc 100644 --- a/api/notification/internal/server/notificationimp/telegram/client.go +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -16,6 +16,7 @@ import ( "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" ) const defaultAPIURL = "https://api.telegram.org" @@ -110,24 +111,45 @@ func (c *client) SendDemoRequest(ctx context.Context, request *model.DemoRequest func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) error { body, err := json.Marshal(&payload) if err != nil { + c.logger.Warn("Failed to marshal telegram payload", zap.Error(err)) return err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint(), bytes.NewReader(body)) if err != nil { + c.logger.Warn("Failed to create telegram request", zap.Error(err)) return err } req.Header.Set("Content-Type", "application/json") + fields := []zap.Field{ + zap.String("chat_id", payload.ChatID), + zap.Int("payload_size_bytes", len(body)), + zap.Bool("disable_preview", payload.DisablePreview), + zap.Bool("disable_notification", payload.DisableNotify), + zap.Bool("protect_content", payload.ProtectContent), + } + if payload.ThreadID != nil { + fields = append(fields, zap.Int64("thread_id", *payload.ThreadID)) + } + c.logger.Debug("Sending Telegram message", fields...) + start := time.Now() + resp, err := c.httpClient.Do(req) if err != nil { + c.logger.Warn("Telegram request failed", zap.Error(err)) return err } defer resp.Body.Close() if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { + c.logger.Debug("Telegram message sent", zap.Int("status_code", resp.StatusCode), zap.Duration("latency", time.Since(start))) return nil } respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10)) + c.logger.Warn("Telegram API returned non-success status", + zap.Int("status_code", resp.StatusCode), + zap.ByteString("response_body", respBody), + zap.String("chat_id", c.chatID)) return merrors.Internal(fmt.Sprintf("telegram sendMessage failed with status %d: %s", resp.StatusCode, string(respBody))) } From 717dafc6732933990097015119e9d7caa5a8ca34 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 19 Nov 2025 13:54:25 +0100 Subject: [PATCH 23/32] better message formatting --- api/notification/config.yml | 2 +- .../mail/internal/builder/builder.go | 2 +- .../notificationimp/mail/internal/mailimp.go | 2 +- .../server/notificationimp/mail/mail.go | 6 +- .../server/notificationimp/notification.go | 4 +- .../server/notificationimp/telegram/client.go | 108 +++++++++++++++--- api/pkg/auth/factory.go | 2 +- api/pkg/auth/internal/casbin/action.go | 2 +- api/pkg/auth/internal/casbin/config/config.go | 2 +- api/pkg/auth/internal/casbin/role.go | 2 +- api/pkg/auth/internal/native/role.go | 2 +- api/pkg/db/connection.go | 4 +- api/pkg/db/factory.go | 2 +- .../internal/mongo/organizationdb/create.go | 2 +- .../db/internal/mongo/refreshtokensdb/crud.go | 2 +- .../db/internal/mongo/repositoryimp/index.go | 2 +- .../mongo/repositoryimp/repository.go | 4 +- .../db/internal/mongo/tseriesimp/tseries.go | 2 +- api/pkg/merrors/errors.go | 29 ++++- api/pkg/merrors/errors_test.go | 47 ++++++++ .../messaging/internal/inprocess/broker.go | 2 +- api/pkg/messaging/internal/natsb/broker.go | 4 +- .../notifications/site/notification.go | 2 +- api/pkg/model/demorequest.go | 12 +- api/pkg/mutil/reorder/reorder.go | 4 +- api/pkg/server/grpcapp/app.go | 6 +- 26 files changed, 202 insertions(+), 56 deletions(-) create mode 100644 api/pkg/merrors/errors_test.go diff --git a/api/notification/config.yml b/api/notification/config.yml index 7e7c8d7..deae90f 100755 --- a/api/notification/config.yml +++ b/api/notification/config.yml @@ -61,7 +61,7 @@ api: thread_id_env: TELEGRAM_THREAD_ID api_url: "https://api.telegram.org" timeout_seconds: 10 - parse_mode: "" + parse_mode: markdown localizer: path: "./i18n" diff --git a/api/notification/internal/server/notificationimp/mail/internal/builder/builder.go b/api/notification/internal/server/notificationimp/mail/internal/builder/builder.go index a729130..b486c22 100644 --- a/api/notification/internal/server/notificationimp/mail/internal/builder/builder.go +++ b/api/notification/internal/server/notificationimp/mail/internal/builder/builder.go @@ -42,7 +42,7 @@ func (mb *MessageBuilderImp) AddData(key, value string) mmail.MailBuilder { func (mb *MessageBuilderImp) Build() (mmail.Message, error) { if len(mb.message.recipients) == 0 { - return nil, merrors.InvalidArgument("Recipient not set") + return nil, merrors.InvalidArgument("Recipient not set", "recipients") } return mb.message, nil } diff --git a/api/notification/internal/server/notificationimp/mail/internal/mailimp.go b/api/notification/internal/server/notificationimp/mail/internal/mailimp.go index 87f2d32..65a6df4 100755 --- a/api/notification/internal/server/notificationimp/mail/internal/mailimp.go +++ b/api/notification/internal/server/notificationimp/mail/internal/mailimp.go @@ -69,7 +69,7 @@ func (c *Client) Send(r mmail.MailBuilder) error { c.logger.Warn("Malformed messge", zap.String("template_id", m.TemplateID()), zap.String("locale", m.Locale()), zap.Strings("recipients", m.Recipients()), zap.Int("body_size", len(body))) - return merrors.InvalidArgument("malformed message") + return merrors.InvalidArgument("malformed message", "message.body", "message.recipients") } subj, err := mailkey.Subject(c.l, m.Parameters(), m.TemplateID(), m.Locale()) if err != nil { diff --git a/api/notification/internal/server/notificationimp/mail/mail.go b/api/notification/internal/server/notificationimp/mail/mail.go index 6cacb60..12f616d 100644 --- a/api/notification/internal/server/notificationimp/mail/mail.go +++ b/api/notification/internal/server/notificationimp/mail/mail.go @@ -1,6 +1,7 @@ package mail import ( + "github.com/mitchellh/mapstructure" "github.com/tech/sendico/notification/interface/api/localizer" notification "github.com/tech/sendico/notification/interface/services/notification/config" mi "github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal" @@ -9,7 +10,6 @@ import ( "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/messaging" "github.com/tech/sendico/pkg/mlogger" - "github.com/mitchellh/mapstructure" "go.uber.org/zap" ) @@ -22,7 +22,7 @@ type Config = notification.Config func createMailClient(logger mlogger.Logger, producer messaging.Producer, l localizer.Localizer, dp domainprovider.DomainProvider, config *Config) (Client, error) { if len(config.Driver) == 0 { - return nil, merrors.InvalidArgument("Mail driver name must be provided") + return nil, merrors.InvalidArgument("Mail driver name must be provided", "config.driver") } logger.Info("Connecting mail client...", zap.String("driver", config.Driver)) if config.Driver == "dummy" { @@ -45,7 +45,7 @@ func createMailClient(logger mlogger.Logger, producer messaging.Producer, l loca return mi.NewClient(logger, l, dp, &gsmconfing), nil } - return nil, merrors.InvalidArgument("Unkwnown mail driver: " + config.Driver) + return nil, merrors.InvalidArgument("Unkwnown mail driver: "+config.Driver, "config.driver") } func CreateMailClient(logger mlogger.Logger, sender string, producer messaging.Producer, l localizer.Localizer, dp domainprovider.DomainProvider, config *Config) (Client, error) { diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index c5d32c3..b6a752e 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -39,10 +39,10 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { p.logger = a.Logger().Named(p.Name()) if a.Config().Notification == nil { - return nil, merrors.InvalidArgument("notification configuration is missing") + return nil, merrors.InvalidArgument("notification configuration is missing", "config.notification") } if a.Config().Notification.Telegram == nil { - return nil, merrors.InvalidArgument("telegram configuration is missing") + return nil, merrors.InvalidArgument("telegram configuration is missing", "config.notification.telegram") } var err error diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go index 07356bc..fda0c4c 100644 --- a/api/notification/internal/server/notificationimp/telegram/client.go +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "html" "io" "net/http" "os" @@ -47,15 +48,15 @@ type sendMessagePayload struct { func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, error) { if cfg == nil { - return nil, merrors.InvalidArgument("telegram configuration is not provided") + return nil, merrors.InvalidArgument("telegram configuration is not provided", "config.notification.telegram") } token := strings.TrimSpace(os.Getenv(cfg.BotTokenEnv)) if token == "" { - return nil, merrors.InvalidArgument(fmt.Sprintf("telegram bot token env %s is empty", cfg.BotTokenEnv)) + return nil, merrors.InvalidArgument(fmt.Sprintf("telegram bot token env %s is empty", cfg.BotTokenEnv), cfg.BotTokenEnv) } chatID := strings.TrimSpace(os.Getenv(cfg.ChatIDEnv)) if chatID == "" { - return nil, merrors.InvalidArgument(fmt.Sprintf("telegram chat id env %s is empty", cfg.ChatIDEnv)) + return nil, merrors.InvalidArgument(fmt.Sprintf("telegram chat id env %s is empty", cfg.ChatIDEnv), cfg.ChatIDEnv) } var threadID *int64 @@ -64,7 +65,7 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er if raw != "" { val, err := strconv.ParseInt(raw, 10, 64) if err != nil { - return nil, merrors.InvalidArgumentWrap(err, fmt.Sprintf("telegram thread id env %s is invalid", env)) + return nil, merrors.InvalidArgumentWrap(err, fmt.Sprintf("telegram thread id env %s is invalid", env), env) } threadID = &val } @@ -79,6 +80,10 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er if apiURL == "" { apiURL = defaultAPIURL } + parseMode := strings.TrimSpace(cfg.ParseMode) + if parseMode == "" { + parseMode = "Markdown" + } return &client{ logger: logger.Named("telegram"), @@ -89,15 +94,15 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er botToken: token, chatID: chatID, threadID: threadID, - parseMode: strings.TrimSpace(cfg.ParseMode), + parseMode: parseMode, }, nil } func (c *client) SendDemoRequest(ctx context.Context, request *model.DemoRequest) error { if request == nil { - return merrors.InvalidArgument("demo request payload is nil") + return merrors.InvalidArgument("demo request payload is nil", "request") } - message := buildMessage(request) + message := buildMessage(request, c.parseMode) payload := sendMessagePayload{ ChatID: c.chatID, Text: message, @@ -157,16 +162,89 @@ func (c *client) endpoint() string { return fmt.Sprintf("%s/bot%s/sendMessage", c.apiURL, c.botToken) } -func buildMessage(req *model.DemoRequest) string { +func buildMessage(req *model.DemoRequest, parseMode string) string { var builder strings.Builder builder.WriteString("New demo request received\n") - builder.WriteString(fmt.Sprintf("Name: %s\n", req.Name)) - builder.WriteString(fmt.Sprintf("Organization: %s\n", req.OrganizationName)) - builder.WriteString(fmt.Sprintf("Phone: %s\n", req.Phone)) - builder.WriteString(fmt.Sprintf("Work email: %s\n", req.WorkEmail)) - builder.WriteString(fmt.Sprintf("Payout volume: %s\n", req.PayoutVolume)) - if req.Comment != "" { - builder.WriteString(fmt.Sprintf("Comment: %s\n", req.Comment)) + builder.WriteString("-----------------------------\n") + + formatter := selectValueFormatter(parseMode) + appendMessageField(&builder, "Name", req.Name, formatter) + appendMessageField(&builder, "Organization", req.OrganizationName, formatter) + appendMessageField(&builder, "Phone", req.Phone, formatter) + appendMessageField(&builder, "Work email", req.WorkEmail, formatter) + appendMessageField(&builder, "Payout volume", req.PayoutVolume, formatter) + if strings.TrimSpace(req.Comment) != "" { + appendMessageField(&builder, "Comment", req.Comment, formatter) } return builder.String() } + +type valueFormatter func(string) string + +func appendMessageField(builder *strings.Builder, label, value string, formatter valueFormatter) { + value = strings.TrimSpace(value) + if value == "" { + value = "—" + } else if formatter != nil { + value = formatter(value) + } + fmt.Fprintf(builder, "• %s: %s\n", label, value) +} + +func selectValueFormatter(parseMode string) valueFormatter { + switch strings.ToLower(parseMode) { + case "markdown": + return func(value string) string { + return fmt.Sprintf("*%s*", escapeMarkdown(value)) + } + case "markdownv2": + return func(value string) string { + return fmt.Sprintf("*%s*", escapeMarkdownV2(value)) + } + case "html": + return func(value string) string { + return fmt.Sprintf("%s", html.EscapeString(value)) + } + default: + return nil + } +} + +var markdownEscaper = strings.NewReplacer( + "*", "\\*", + "_", "\\_", + "[", "\\[", + "]", "\\]", + "(", "\\(", + ")", "\\)", + "`", "\\`", +) + +var markdownV2Escaper = strings.NewReplacer( + "_", "\\_", + "*", "\\*", + "[", "\\[", + "]", "\\]", + "(", "\\(", + ")", "\\)", + "~", "\\~", + "`", "\\`", + ">", "\\>", + "#", "\\#", + "+", "\\+", + "-", "\\-", + "=", "\\=", + "|", "\\|", + "{", "\\{", + "}", "\\}", + ".", "\\.", + "!", "\\!", +) + +func escapeMarkdown(value string) string { + return markdownEscaper.Replace(value) +} + +func escapeMarkdownV2(value string) string { + return markdownV2Escaper.Replace(value) +} diff --git a/api/pkg/auth/factory.go b/api/pkg/auth/factory.go index b4b6a55..c054e56 100644 --- a/api/pkg/auth/factory.go +++ b/api/pkg/auth/factory.go @@ -48,5 +48,5 @@ func CreateAuth( } return enforcer, manager, nil } - return nil, nil, merrors.InvalidArgument("Unknown enforcer type: " + string(config.Driver)) + return nil, nil, merrors.InvalidArgument("Unknown enforcer type: "+string(config.Driver), "config.driver") } diff --git a/api/pkg/auth/internal/casbin/action.go b/api/pkg/auth/internal/casbin/action.go index 8e25dad..e19efb1 100644 --- a/api/pkg/auth/internal/casbin/action.go +++ b/api/pkg/auth/internal/casbin/action.go @@ -18,6 +18,6 @@ func stringToAction(actionStr string) (model.Action, error) { case string(model.ActionDelete): return model.ActionDelete, nil default: - return "", merrors.InvalidArgument(fmt.Sprintf("invalid action: %s", actionStr)) + return "", merrors.InvalidArgument(fmt.Sprintf("invalid action: %s", actionStr), "action") } } diff --git a/api/pkg/auth/internal/casbin/config/config.go b/api/pkg/auth/internal/casbin/config/config.go index 17d1f47..4984fef 100644 --- a/api/pkg/auth/internal/casbin/config/config.go +++ b/api/pkg/auth/internal/casbin/config/config.go @@ -109,7 +109,7 @@ func PrepareConfig(logger mlogger.Logger, config *Config) (*EnforcerConfig, erro if len(adapter.DatabaseName) == 0 { logger.Error("Database name is not set") - return nil, merrors.InvalidArgument("database name must be provided") + return nil, merrors.InvalidArgument("database name must be provided", "adapter.databaseName") } path := getEnvValue(logger, "model_path", "model_path_env", config.ModelPath, config.ModelPathEnv) diff --git a/api/pkg/auth/internal/casbin/role.go b/api/pkg/auth/internal/casbin/role.go index cc42979..7c0811e 100644 --- a/api/pkg/auth/internal/casbin/role.go +++ b/api/pkg/auth/internal/casbin/role.go @@ -35,7 +35,7 @@ func NewRoleManager(logger mlogger.Logger, enforcer *CasbinEnforcer, rolePermiss func (rm *RoleManager) validateObjectIDs(ids ...primitive.ObjectID) error { for _, id := range ids { if id.IsZero() { - return merrors.InvalidArgument("Object references cannot be zero") + return merrors.InvalidArgument("Object references cannot be zero", "objectRef") } } return nil diff --git a/api/pkg/auth/internal/native/role.go b/api/pkg/auth/internal/native/role.go index 2515b2c..f50eaa6 100644 --- a/api/pkg/auth/internal/native/role.go +++ b/api/pkg/auth/internal/native/role.go @@ -36,7 +36,7 @@ func NewRoleManager(logger mlogger.Logger, enforcer *Enforcer, rolePermissionRef func (rm *RoleManager) validateObjectIDs(ids ...primitive.ObjectID) error { for _, id := range ids { if id.IsZero() { - return merrors.InvalidArgument("Object references cannot be zero") + return merrors.InvalidArgument("Object references cannot be zero", "objectRef") } } return nil diff --git a/api/pkg/db/connection.go b/api/pkg/db/connection.go index da45fca..de00fbf 100644 --- a/api/pkg/db/connection.go +++ b/api/pkg/db/connection.go @@ -47,10 +47,10 @@ func (c *MongoConnection) Ping(ctx context.Context) error { // ConnectMongo returns a low-level MongoDB connection without constructing repositories. func ConnectMongo(logger mlogger.Logger, config *Config) (*MongoConnection, error) { if config == nil { - return nil, merrors.InvalidArgument("database configuration is nil") + return nil, merrors.InvalidArgument("database configuration is nil", "config") } if config.Driver != Mongo { - return nil, merrors.InvalidArgument("unsupported database driver: " + string(config.Driver)) + return nil, merrors.InvalidArgument("unsupported database driver: "+string(config.Driver), "config.driver") } client, _, settings, err := mongoimpl.ConnectClient(logger, config.Settings) diff --git a/api/pkg/db/factory.go b/api/pkg/db/factory.go index f43697d..dc8c7e8 100644 --- a/api/pkg/db/factory.go +++ b/api/pkg/db/factory.go @@ -37,5 +37,5 @@ func NewConnection(logger mlogger.Logger, config *Config) (Factory, error) { if config.Driver == Mongo { return mongoimpl.NewConnection(logger, config.Settings) } - return nil, merrors.InvalidArgument("unknown database driver: " + string(config.Driver)) + return nil, merrors.InvalidArgument("unknown database driver: "+string(config.Driver), "config.driver") } diff --git a/api/pkg/db/internal/mongo/organizationdb/create.go b/api/pkg/db/internal/mongo/organizationdb/create.go index 9f69ae4..2be8cd2 100644 --- a/api/pkg/db/internal/mongo/organizationdb/create.go +++ b/api/pkg/db/internal/mongo/organizationdb/create.go @@ -10,7 +10,7 @@ import ( func (db *OrganizationDB) Create(ctx context.Context, _, _ primitive.ObjectID, org *model.Organization) error { if org == nil { - return merrors.InvalidArgument("Organization object is nil") + return merrors.InvalidArgument("Organization object is nil", "organization") } org.SetID(primitive.NewObjectID()) // Organizaiton reference must be set to the same value as own organization reference diff --git a/api/pkg/db/internal/mongo/refreshtokensdb/crud.go b/api/pkg/db/internal/mongo/refreshtokensdb/crud.go index 7365b88..92699f3 100644 --- a/api/pkg/db/internal/mongo/refreshtokensdb/crud.go +++ b/api/pkg/db/internal/mongo/refreshtokensdb/crud.go @@ -18,7 +18,7 @@ func (db *RefreshTokenDB) Create(ctx context.Context, rt *model.RefreshToken) er // First, try to find an existing token for this account/client/device combination var existing model.RefreshToken if rt.AccountRef == nil { - return merrors.InvalidArgument("Account reference must have a vaild value") + return merrors.InvalidArgument("Account reference must have a vaild value", "refreshToken.accountRef") } if err := db.FindOne(ctx, filterByAccount(*rt.AccountRef, &rt.SessionIdentifier), &existing); err != nil { if errors.Is(err, merrors.ErrNoData) { diff --git a/api/pkg/db/internal/mongo/repositoryimp/index.go b/api/pkg/db/internal/mongo/repositoryimp/index.go index 9a3a31b..7313aa7 100644 --- a/api/pkg/db/internal/mongo/repositoryimp/index.go +++ b/api/pkg/db/internal/mongo/repositoryimp/index.go @@ -15,7 +15,7 @@ func (r *MongoRepository) CreateIndex(def *ri.Definition) error { return merrors.NoData("data collection is not set") } if len(def.Keys) == 0 { - return merrors.InvalidArgument("Index definition has no keys") + return merrors.InvalidArgument("Index definition has no keys", "index.keys") } // ----- build BSON keys -------------------------------------------------- diff --git a/api/pkg/db/internal/mongo/repositoryimp/repository.go b/api/pkg/db/internal/mongo/repositoryimp/repository.go index adac397..81e2cc4 100644 --- a/api/pkg/db/internal/mongo/repositoryimp/repository.go +++ b/api/pkg/db/internal/mongo/repositoryimp/repository.go @@ -83,7 +83,7 @@ func (r *MongoRepository) findOneByFilterImp(ctx context.Context, filter bson.D, func (r *MongoRepository) Get(ctx context.Context, id primitive.ObjectID, result storable.Storable) error { if id.IsZero() { - return merrors.InvalidArgument("zero id provided while fetching " + result.Collection()) + return merrors.InvalidArgument("zero id provided while fetching "+result.Collection(), "id") } return r.findOneByFilterImp(ctx, idFilter(id), fmt.Sprintf("%s with ID = %s not found", result.Collection(), id.Hex()), result) } @@ -134,7 +134,7 @@ func (r *MongoRepository) Update(ctx context.Context, obj storable.Storable) err func (r *MongoRepository) Patch(ctx context.Context, id primitive.ObjectID, patch builder.Patch) error { if id.IsZero() { - return merrors.InvalidArgument("zero id provided while patching") + return merrors.InvalidArgument("zero id provided while patching", "id") } _, err := r.collection.UpdateByID(ctx, id, patch.Build()) return err diff --git a/api/pkg/db/internal/mongo/tseriesimp/tseries.go b/api/pkg/db/internal/mongo/tseriesimp/tseries.go index f7713c0..ed1a5ba 100644 --- a/api/pkg/db/internal/mongo/tseriesimp/tseries.go +++ b/api/pkg/db/internal/mongo/tseriesimp/tseries.go @@ -22,7 +22,7 @@ type TimeSeries struct { func NewMongoTimeSeriesCollection(ctx context.Context, db *mongo.Database, tsOpts *tsoptions.Options) (*TimeSeries, error) { if tsOpts == nil { - return nil, merrors.InvalidArgument("nil time-series options provided") + return nil, merrors.InvalidArgument("nil time-series options provided", "options") } // Configure time-series options granularity := tsOpts.Granularity.String() diff --git a/api/pkg/merrors/errors.go b/api/pkg/merrors/errors.go index 3667374..754db56 100644 --- a/api/pkg/merrors/errors.go +++ b/api/pkg/merrors/errors.go @@ -3,6 +3,7 @@ package merrors import ( "errors" "fmt" + "strings" "go.mongodb.org/mongo-driver/bson/primitive" ) @@ -27,8 +28,8 @@ func Internal(msg string) error { var ErrInvalidArg = errors.New("invalidArgError") -func InvalidArgument(msg string) error { - return fmt.Errorf("%w: %s", ErrInvalidArg, msg) +func InvalidArgument(msg string, argumentNames ...string) error { + return fmt.Errorf("%w: %s", ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...)) } var ErrDataConflict = errors.New("DataConflict") @@ -64,8 +65,8 @@ func NoMessagingTopic(topic string) error { return fmt.Errorf("%w: messaging topic '%s' not found", ErrNoMessagingTopic, topic) } -func InvalidArgumentWrap(err error, msg string) error { - return wrapError(ErrInvalidArg, msg, err) +func InvalidArgumentWrap(err error, msg string, argumentNames ...string) error { + return wrapError(ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...), err) } func InternalWrap(err error, msg string) error { @@ -79,3 +80,23 @@ func wrapError(base error, msg string, err error) error { } return errors.Join(baseErr, err) } + +func invalidArgumentMessage(msg string, argumentNames ...string) string { + names := make([]string, 0, len(argumentNames)) + for _, name := range argumentNames { + name = strings.TrimSpace(name) + if name == "" { + continue + } + names = append(names, fmt.Sprintf("%q", name)) + } + if len(names) == 0 { + return msg + } + + prefix := "broken argument" + if len(names) > 1 { + prefix = "broken arguments" + } + return fmt.Sprintf("%s %s: %s", prefix, strings.Join(names, ", "), msg) +} diff --git a/api/pkg/merrors/errors_test.go b/api/pkg/merrors/errors_test.go new file mode 100644 index 0000000..bb6cedd --- /dev/null +++ b/api/pkg/merrors/errors_test.go @@ -0,0 +1,47 @@ +package merrors + +import ( + "errors" + "strings" + "testing" +) + +func TestInvalidArgumentSupportsBrokenArgumentName(t *testing.T) { + t.Run("without argument name keeps old behavior", func(t *testing.T) { + err := InvalidArgument("value is missing") + expected := "invalidArgError: value is missing" + if err.Error() != expected { + t.Fatalf("unexpected error message: %s", err) + } + if !errors.Is(err, ErrInvalidArg) { + t.Fatalf("error should wrap ErrInvalidArg") + } + }) + + t.Run("single argument name", func(t *testing.T) { + err := InvalidArgument("value is missing", "bot_token_env") + expected := `invalidArgError: broken argument "bot_token_env": value is missing` + if err.Error() != expected { + t.Fatalf("unexpected error message: %s", err) + } + }) + + t.Run("multiple argument names", func(t *testing.T) { + err := InvalidArgument("value mismatch", "bot_token_env", "chat_id_env", " ") + expected := `invalidArgError: broken arguments "bot_token_env", "chat_id_env": value mismatch` + if err.Error() != expected { + t.Fatalf("unexpected error message: %s", err) + } + }) +} + +func TestInvalidArgumentWrapSupportsBrokenArgumentName(t *testing.T) { + base := errors.New("root cause") + err := InvalidArgumentWrap(base, "value is missing", "bot_token_env") + if !strings.Contains(err.Error(), `invalidArgError: broken argument "bot_token_env": value is missing`) { + t.Fatalf("wrapped error should include broken argument name: %s", err) + } + if !errors.Is(err, ErrInvalidArg) || !errors.Is(err, base) { + t.Fatalf("wrapped error should preserve all error layers") + } +} diff --git a/api/pkg/messaging/internal/inprocess/broker.go b/api/pkg/messaging/internal/inprocess/broker.go index dc54b20..3be97aa 100644 --- a/api/pkg/messaging/internal/inprocess/broker.go +++ b/api/pkg/messaging/internal/inprocess/broker.go @@ -76,7 +76,7 @@ func (b *MessageBroker) Unsubscribe(event model.NotificationEvent, subChan <-cha func NewInProcessBroker(logger mlogger.Logger, bufferSize int) (*MessageBroker, error) { if bufferSize < 1 { - return nil, merrors.InvalidArgument(fmt.Sprintf("Invelid buffer size %d. It must be greater than 1", bufferSize)) + return nil, merrors.InvalidArgument(fmt.Sprintf("Invelid buffer size %d. It must be greater than 1", bufferSize), "bufferSize") } logger.Info("Created in-process logger", zap.Int("buffer_size", bufferSize)) return &MessageBroker{ diff --git a/api/pkg/messaging/internal/natsb/broker.go b/api/pkg/messaging/internal/natsb/broker.go index 14cb51c..b2e2b97 100644 --- a/api/pkg/messaging/internal/natsb/broker.go +++ b/api/pkg/messaging/internal/natsb/broker.go @@ -39,7 +39,7 @@ func loadEnv(settings *nc.Settings, l *zap.Logger) (*envConfig, error) { return v, nil } l.Error(fmt.Sprintf("NATS %s not found in environment", label), zap.String("env_var", key)) - return "", merrors.InvalidArgument(fmt.Sprintf("NATS %s not found in environment variable: %s", label, key)) + return "", merrors.InvalidArgument(fmt.Sprintf("NATS %s not found in environment variable: %s", label, key), key) } user, err := get(settings.UsernameEnv, "user name") @@ -65,7 +65,7 @@ func loadEnv(settings *nc.Settings, l *zap.Logger) (*envConfig, error) { port, err := strconv.Atoi(portStr) if err != nil || port <= 0 || port > 65535 { l.Error("Invalid NATS port value", zap.String("port", portStr)) - return nil, merrors.InvalidArgument("Invalid NATS port: " + portStr) + return nil, merrors.InvalidArgument("Invalid NATS port: "+portStr, settings.PortEnv) } return &envConfig{ diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go index d40f824..c0b92ec 100644 --- a/api/pkg/messaging/internal/notifications/site/notification.go +++ b/api/pkg/messaging/internal/notifications/site/notification.go @@ -17,7 +17,7 @@ type DemoRequestNotification struct { func (drn *DemoRequestNotification) Serialize() ([]byte, error) { if drn.request == nil { - return nil, merrors.InvalidArgument("demo request payload is empty") + return nil, merrors.InvalidArgument("demo request payload is empty", "request") } msg := gmessaging.DemoRequestEvent{ Name: drn.request.Name, diff --git a/api/pkg/model/demorequest.go b/api/pkg/model/demorequest.go index 007b357..c7c7e54 100644 --- a/api/pkg/model/demorequest.go +++ b/api/pkg/model/demorequest.go @@ -32,22 +32,22 @@ func (dr *DemoRequest) Normalize() { // Validate ensures that all required fields are present. func (dr *DemoRequest) Validate() error { if dr == nil { - return merrors.InvalidArgument("request payload is empty") + return merrors.InvalidArgument("request payload is empty", "request") } if dr.Name == "" { - return merrors.InvalidArgument("name must not be empty") + return merrors.InvalidArgument("name must not be empty", "request.name") } if dr.OrganizationName == "" { - return merrors.InvalidArgument("organization name must not be empty") + return merrors.InvalidArgument("organization name must not be empty", "request.organizationName") } if dr.Phone == "" { - return merrors.InvalidArgument("phone must not be empty") + return merrors.InvalidArgument("phone must not be empty", "request.phone") } if dr.WorkEmail == "" { - return merrors.InvalidArgument("work email must not be empty") + return merrors.InvalidArgument("work email must not be empty", "request.workEmail") } if dr.PayoutVolume == "" { - return merrors.InvalidArgument("payout volume must not be empty") + return merrors.InvalidArgument("payout volume must not be empty", "request.payoutVolume") } return nil } diff --git a/api/pkg/mutil/reorder/reorder.go b/api/pkg/mutil/reorder/reorder.go index 891e797..14f02d6 100644 --- a/api/pkg/mutil/reorder/reorder.go +++ b/api/pkg/mutil/reorder/reorder.go @@ -17,12 +17,12 @@ func IndexableRefs(items []model.IndexableRef, oldIndex, newIndex int) ([]model. } } if targetIndex == -1 { - return nil, merrors.InvalidArgument("Item not found at specified index") + return nil, merrors.InvalidArgument("Item not found at specified index", "oldIndex") } // Validate new index bounds if newIndex < 0 || newIndex >= len(items) { - return nil, merrors.InvalidArgument("Invalid new index for reorder") + return nil, merrors.InvalidArgument("Invalid new index for reorder", "newIndex") } // Remove the item from its current position diff --git a/api/pkg/server/grpcapp/app.go b/api/pkg/server/grpcapp/app.go index d2d0b6d..8a35d17 100644 --- a/api/pkg/server/grpcapp/app.go +++ b/api/pkg/server/grpcapp/app.go @@ -53,13 +53,13 @@ type App[T any] struct { func NewApp[T any](logger mlogger.Logger, name string, config *Config, debug bool, repoFactory RepositoryFactory[T], serviceFactory ServiceFactory[T], opts ...Option[T]) (*App[T], error) { if logger == nil { - return nil, merrors.InvalidArgument("nil logger supplied") + return nil, merrors.InvalidArgument("nil logger supplied", "logger") } if config == nil { - return nil, merrors.InvalidArgument("nil config supplied") + return nil, merrors.InvalidArgument("nil config supplied", "config") } if serviceFactory == nil { - return nil, merrors.InvalidArgument("nil service factory supplied") + return nil, merrors.InvalidArgument("nil service factory supplied", "serviceFactory") } app := &App[T]{ From e08eb742e4f96e6624568577cc3075a206d4e7ae Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 19 Nov 2025 14:42:38 +0100 Subject: [PATCH 24/32] + contact requests --- .../server/notificationimp/notification.go | 17 +++ .../server/notificationimp/telegram/client.go | 106 +++--------------- .../notificationimp/telegram/contact.go | 17 +++ .../server/notificationimp/telegram/demo.go | 24 ++++ .../notificationimp/telegram/message.go | 100 +++++++++++++++++ .../notifications/site/notification.go | 35 ++++++ .../notifications/site/contact_request.go | 11 ++ .../notifications/site/handler/handler.go | 1 + .../messaging/notifications/site/processor.go | 35 ++++++ api/pkg/model/contactrequest.go | 41 +++++++ api/pkg/model/contactrequest_test.go | 31 +++++ api/pkg/model/demorequest.go | 16 +-- api/proto/contact_request.proto | 12 ++ api/server/internal/server/siteimp/contact.go | 29 +++++ api/server/internal/server/siteimp/demo.go | 31 +++++ api/server/internal/server/siteimp/service.go | 29 +---- 16 files changed, 403 insertions(+), 132 deletions(-) create mode 100644 api/notification/internal/server/notificationimp/telegram/contact.go create mode 100644 api/notification/internal/server/notificationimp/telegram/demo.go create mode 100644 api/notification/internal/server/notificationimp/telegram/message.go create mode 100644 api/pkg/messaging/notifications/site/contact_request.go create mode 100644 api/pkg/model/contactrequest.go create mode 100644 api/pkg/model/contactrequest_test.go create mode 100644 api/proto/contact_request.proto create mode 100644 api/server/internal/server/siteimp/contact.go create mode 100644 api/server/internal/server/siteimp/demo.go diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index b6a752e..41b3e01 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -85,6 +85,11 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { return nil, err } + if err := a.Register().Consumer(snotifications.NewContactRequestProcessor(p.logger, p.onContactRequest)); err != nil { + p.logger.Error("Failed to register contact request handler", zap.Error(err)) + return nil, err + } + return p, nil } @@ -99,3 +104,15 @@ func (a *NotificationAPI) onDemoRequest(ctx context.Context, request *model.Demo a.logger.Info("Demo request sent via Telegram", zap.String("name", request.Name), zap.String("organization", request.OrganizationName)) return nil } + +func (a *NotificationAPI) onContactRequest(ctx context.Context, request *model.ContactRequest) error { + if a.tg == nil { + return merrors.Internal("telegram client is not configured") + } + if err := a.tg.SendContactRequest(ctx, request); err != nil { + a.logger.Warn("Failed to send contact request via telegram", zap.Error(err)) + return err + } + a.logger.Info("Contact request sent via Telegram", zap.String("name", request.Name), zap.String("topic", request.Topic)) + return nil +} diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go index fda0c4c..8b8e68c 100644 --- a/api/notification/internal/server/notificationimp/telegram/client.go +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "html" "io" "net/http" "os" @@ -24,6 +23,7 @@ const defaultAPIURL = "https://api.telegram.org" type Client interface { SendDemoRequest(ctx context.Context, request *model.DemoRequest) error + SendContactRequest(ctx context.Context, request *model.ContactRequest) error } type client struct { @@ -102,15 +102,7 @@ func (c *client) SendDemoRequest(ctx context.Context, request *model.DemoRequest if request == nil { return merrors.InvalidArgument("demo request payload is nil", "request") } - message := buildMessage(request, c.parseMode) - payload := sendMessagePayload{ - ChatID: c.chatID, - Text: message, - ParseMode: c.parseMode, - ThreadID: c.threadID, - DisablePreview: true, - } - return c.sendMessage(ctx, payload) + return c.sendForm(ctx, newDemoRequestTemplate(request)) } func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) error { @@ -162,89 +154,21 @@ func (c *client) endpoint() string { return fmt.Sprintf("%s/bot%s/sendMessage", c.apiURL, c.botToken) } -func buildMessage(req *model.DemoRequest, parseMode string) string { - var builder strings.Builder - builder.WriteString("New demo request received\n") - builder.WriteString("-----------------------------\n") - - formatter := selectValueFormatter(parseMode) - appendMessageField(&builder, "Name", req.Name, formatter) - appendMessageField(&builder, "Organization", req.OrganizationName, formatter) - appendMessageField(&builder, "Phone", req.Phone, formatter) - appendMessageField(&builder, "Work email", req.WorkEmail, formatter) - appendMessageField(&builder, "Payout volume", req.PayoutVolume, formatter) - if strings.TrimSpace(req.Comment) != "" { - appendMessageField(&builder, "Comment", req.Comment, formatter) +func (c *client) SendContactRequest(ctx context.Context, request *model.ContactRequest) error { + if request == nil { + return merrors.InvalidArgument("contact request payload is nil", "request") } - return builder.String() + return c.sendForm(ctx, newContactRequestTemplate(request)) } -type valueFormatter func(string) string - -func appendMessageField(builder *strings.Builder, label, value string, formatter valueFormatter) { - value = strings.TrimSpace(value) - if value == "" { - value = "—" - } else if formatter != nil { - value = formatter(value) +func (c *client) sendForm(ctx context.Context, template messageTemplate) error { + message := template.Format(c.parseMode) + payload := sendMessagePayload{ + ChatID: c.chatID, + Text: message, + ParseMode: c.parseMode, + ThreadID: c.threadID, + DisablePreview: true, } - fmt.Fprintf(builder, "• %s: %s\n", label, value) -} - -func selectValueFormatter(parseMode string) valueFormatter { - switch strings.ToLower(parseMode) { - case "markdown": - return func(value string) string { - return fmt.Sprintf("*%s*", escapeMarkdown(value)) - } - case "markdownv2": - return func(value string) string { - return fmt.Sprintf("*%s*", escapeMarkdownV2(value)) - } - case "html": - return func(value string) string { - return fmt.Sprintf("%s", html.EscapeString(value)) - } - default: - return nil - } -} - -var markdownEscaper = strings.NewReplacer( - "*", "\\*", - "_", "\\_", - "[", "\\[", - "]", "\\]", - "(", "\\(", - ")", "\\)", - "`", "\\`", -) - -var markdownV2Escaper = strings.NewReplacer( - "_", "\\_", - "*", "\\*", - "[", "\\[", - "]", "\\]", - "(", "\\(", - ")", "\\)", - "~", "\\~", - "`", "\\`", - ">", "\\>", - "#", "\\#", - "+", "\\+", - "-", "\\-", - "=", "\\=", - "|", "\\|", - "{", "\\{", - "}", "\\}", - ".", "\\.", - "!", "\\!", -) - -func escapeMarkdown(value string) string { - return markdownEscaper.Replace(value) -} - -func escapeMarkdownV2(value string) string { - return markdownV2Escaper.Replace(value) + return c.sendMessage(ctx, payload) } diff --git a/api/notification/internal/server/notificationimp/telegram/contact.go b/api/notification/internal/server/notificationimp/telegram/contact.go new file mode 100644 index 0000000..7f94750 --- /dev/null +++ b/api/notification/internal/server/notificationimp/telegram/contact.go @@ -0,0 +1,17 @@ +package telegram + +import "github.com/tech/sendico/pkg/model" + +func newContactRequestTemplate(request *model.ContactRequest) messageTemplate { + return messageTemplate{ + title: "New contact request received", + fields: []messageField{ + {label: "Name", value: request.Name}, + {label: "Email", value: request.Email}, + {label: "Phone", value: request.Phone}, + {label: "Company", value: request.Company}, + {label: "Topic", value: request.Topic}, + {label: "Message", value: request.Message}, + }, + } +} diff --git a/api/notification/internal/server/notificationimp/telegram/demo.go b/api/notification/internal/server/notificationimp/telegram/demo.go new file mode 100644 index 0000000..d2afc20 --- /dev/null +++ b/api/notification/internal/server/notificationimp/telegram/demo.go @@ -0,0 +1,24 @@ +package telegram + +import ( + "strings" + + "github.com/tech/sendico/pkg/model" +) + +func newDemoRequestTemplate(request *model.DemoRequest) messageTemplate { + fields := []messageField{ + {label: "Name", value: request.Name}, + {label: "Organization", value: request.OrganizationName}, + {label: "Phone", value: request.Phone}, + {label: "Work email", value: request.WorkEmail}, + {label: "Payout volume", value: request.PayoutVolume}, + } + if strings.TrimSpace(request.Comment) != "" { + fields = append(fields, messageField{label: "Comment", value: request.Comment}) + } + return messageTemplate{ + title: "New demo request received", + fields: fields, + } +} diff --git a/api/notification/internal/server/notificationimp/telegram/message.go b/api/notification/internal/server/notificationimp/telegram/message.go new file mode 100644 index 0000000..db2c515 --- /dev/null +++ b/api/notification/internal/server/notificationimp/telegram/message.go @@ -0,0 +1,100 @@ +package telegram + +import ( + "fmt" + "html" + "strings" +) + +type messageField struct { + label string + value string +} + +type messageTemplate struct { + title string + fields []messageField +} + +func (mt messageTemplate) Format(parseMode string) string { + var builder strings.Builder + builder.WriteString(mt.title) + builder.WriteString("\n") + builder.WriteString("-----------------------------\n") + + formatter := selectValueFormatter(parseMode) + for _, field := range mt.fields { + appendMessageField(&builder, field.label, field.value, formatter) + } + return builder.String() +} + +type valueFormatter func(string) string + +func appendMessageField(builder *strings.Builder, label, value string, formatter valueFormatter) { + value = strings.TrimSpace(value) + if value == "" { + value = "—" + } else if formatter != nil { + value = formatter(value) + } + fmt.Fprintf(builder, "• %s: %s\n", label, value) +} + +func selectValueFormatter(parseMode string) valueFormatter { + switch strings.ToLower(parseMode) { + case "markdown": + return func(value string) string { + return fmt.Sprintf("*%s*", escapeMarkdown(value)) + } + case "markdownv2": + return func(value string) string { + return fmt.Sprintf("*%s*", escapeMarkdownV2(value)) + } + case "html": + return func(value string) string { + return fmt.Sprintf("%s", html.EscapeString(value)) + } + default: + return nil + } +} + +var markdownEscaper = strings.NewReplacer( + "*", "\\*", + "_", "\\_", + "[", "\\[", + "]", "\\]", + "(", "\\(", + ")", "\\)", + "`", "\\`", +) + +var markdownV2Escaper = strings.NewReplacer( + "_", "\\_", + "*", "\\*", + "[", "\\[", + "]", "\\]", + "(", "\\(", + ")", "\\)", + "~", "\\~", + "`", "\\`", + ">", "\\>", + "#", "\\#", + "+", "\\+", + "-", "\\-", + "=", "\\=", + "|", "\\|", + "{", "\\{", + "}", "\\}", + ".", "\\.", + "!", "\\!", +) + +func escapeMarkdown(value string) string { + return markdownEscaper.Replace(value) +} + +func escapeMarkdownV2(value string) string { + return markdownV2Escaper.Replace(value) +} diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go index c0b92ec..9387fc6 100644 --- a/api/pkg/messaging/internal/notifications/site/notification.go +++ b/api/pkg/messaging/internal/notifications/site/notification.go @@ -44,3 +44,38 @@ func NewDemoRequestEnvelope(sender string, request *model.DemoRequest) messaging request: request, } } + +type ContactRequestNotification struct { + messaging.Envelope + request *model.ContactRequest +} + +func (crn *ContactRequestNotification) Serialize() ([]byte, error) { + if crn.request == nil { + return nil, merrors.InvalidArgument("contact request payload is empty", "request") + } + msg := gmessaging.ContactRequestEvent{ + Name: crn.request.Name, + Email: crn.request.Email, + Phone: crn.request.Phone, + Company: crn.request.Company, + Topic: crn.request.Topic, + Message: crn.request.Message, + } + data, err := proto.Marshal(&msg) + if err != nil { + return nil, err + } + return crn.Envelope.Wrap(data) +} + +func NewContactRequestEvent() model.NotificationEvent { + return model.NewNotification(mservice.Site, nm.NACreated) +} + +func NewContactRequestEnvelope(sender string, request *model.ContactRequest) messaging.Envelope { + return &ContactRequestNotification{ + Envelope: messaging.CreateEnvelope(sender, NewContactRequestEvent()), + request: request, + } +} diff --git a/api/pkg/messaging/notifications/site/contact_request.go b/api/pkg/messaging/notifications/site/contact_request.go new file mode 100644 index 0000000..fbf8c6d --- /dev/null +++ b/api/pkg/messaging/notifications/site/contact_request.go @@ -0,0 +1,11 @@ +package notifications + +import ( + messaging "github.com/tech/sendico/pkg/messaging/envelope" + internalsite "github.com/tech/sendico/pkg/messaging/internal/notifications/site" + "github.com/tech/sendico/pkg/model" +) + +func ContactRequest(sender string, request *model.ContactRequest) messaging.Envelope { + return internalsite.NewContactRequestEnvelope(sender, request) +} diff --git a/api/pkg/messaging/notifications/site/handler/handler.go b/api/pkg/messaging/notifications/site/handler/handler.go index ef1b3ef..4d525a8 100644 --- a/api/pkg/messaging/notifications/site/handler/handler.go +++ b/api/pkg/messaging/notifications/site/handler/handler.go @@ -7,3 +7,4 @@ import ( ) type DemoRequestHandler = func(context.Context, *model.DemoRequest) error +type ContactRequestHandler = func(context.Context, *model.ContactRequest) error diff --git a/api/pkg/messaging/notifications/site/processor.go b/api/pkg/messaging/notifications/site/processor.go index 33e5eab..a58ea29 100644 --- a/api/pkg/messaging/notifications/site/processor.go +++ b/api/pkg/messaging/notifications/site/processor.go @@ -48,3 +48,38 @@ func NewDemoRequestProcessor(logger mlogger.Logger, handler handler.DemoRequestH event: internalsite.NewDemoRequestEvent(), } } + +type ContactRequestProcessor struct { + logger mlogger.Logger + handler handler.ContactRequestHandler + event model.NotificationEvent +} + +func (crp *ContactRequestProcessor) Process(ctx context.Context, envelope me.Envelope) error { + var msg gmessaging.ContactRequestEvent + if err := proto.Unmarshal(envelope.GetData(), &msg); err != nil { + crp.logger.Warn("Failed to decode contact request envelope", zap.Error(err), zap.String("topic", crp.event.ToString())) + return err + } + request := &model.ContactRequest{ + Name: msg.GetName(), + Email: msg.GetEmail(), + Phone: msg.GetPhone(), + Company: msg.GetCompany(), + Topic: msg.GetTopic(), + Message: msg.GetMessage(), + } + return crp.handler(ctx, request) +} + +func (crp *ContactRequestProcessor) GetSubject() model.NotificationEvent { + return crp.event +} + +func NewContactRequestProcessor(logger mlogger.Logger, handler handler.ContactRequestHandler) np.EnvelopeProcessor { + return &ContactRequestProcessor{ + logger: logger.Named("contact_request_processor"), + handler: handler, + event: internalsite.NewContactRequestEvent(), + } +} diff --git a/api/pkg/model/contactrequest.go b/api/pkg/model/contactrequest.go new file mode 100644 index 0000000..30252ab --- /dev/null +++ b/api/pkg/model/contactrequest.go @@ -0,0 +1,41 @@ +package model + +import ( + "strings" + + "github.com/tech/sendico/pkg/merrors" +) + +// ContactRequest represents a contact form submission from the marketing site. +type ContactRequest struct { + Name string `json:"name"` + Email string `json:"email"` + Phone string `json:"phone"` + Company string `json:"company"` + Topic string `json:"topic"` + Message string `json:"message"` +} + +// Normalize trims whitespace from all string fields. +func (cr *ContactRequest) Normalize() { + if cr == nil { + return + } + cr.Name = strings.TrimSpace(cr.Name) + cr.Email = strings.TrimSpace(cr.Email) + cr.Phone = strings.TrimSpace(cr.Phone) + cr.Company = strings.TrimSpace(cr.Company) + cr.Topic = strings.TrimSpace(cr.Topic) + cr.Message = strings.TrimSpace(cr.Message) +} + +// Validate ensures required contact request fields are present. +func (cr *ContactRequest) Validate() error { + if cr == nil { + return merrors.InvalidArgument("request payload is empty", "request") + } + if (cr.Email == "") || (cr.Phone == "") { + return merrors.InvalidArgument("email or phone must not be empty", "request.email", "request.phone") + } + return nil +} diff --git a/api/pkg/model/contactrequest_test.go b/api/pkg/model/contactrequest_test.go new file mode 100644 index 0000000..0dc49cc --- /dev/null +++ b/api/pkg/model/contactrequest_test.go @@ -0,0 +1,31 @@ +package model + +import "testing" + +func TestContactRequestNormalizeAndValidate(t *testing.T) { + req := &ContactRequest{ + Name: " Alice ", + Email: " alice@example.com ", + Phone: " +1 234 ", + Company: " Sendico ", + Topic: " General question ", + Message: " Hello team ", + } + + req.Normalize() + if err := req.Validate(); err != nil { + t.Fatalf("expected request to be valid, got error: %v", err) + } + + if req.Name != "Alice" || req.Email != "alice@example.com" || req.Phone != "+1 234" || req.Company != "Sendico" || req.Topic != "General question" || req.Message != "Hello team" { + t.Fatalf("normalize failed: %+v", req) + } +} + +func TestContactRequestValidateMissing(t *testing.T) { + req := &ContactRequest{} + req.Normalize() + if err := req.Validate(); err == nil { + t.Fatalf("expected validation error for empty request") + } +} diff --git a/api/pkg/model/demorequest.go b/api/pkg/model/demorequest.go index c7c7e54..709447c 100644 --- a/api/pkg/model/demorequest.go +++ b/api/pkg/model/demorequest.go @@ -34,20 +34,8 @@ func (dr *DemoRequest) Validate() error { if dr == nil { return merrors.InvalidArgument("request payload is empty", "request") } - if dr.Name == "" { - return merrors.InvalidArgument("name must not be empty", "request.name") - } - if dr.OrganizationName == "" { - return merrors.InvalidArgument("organization name must not be empty", "request.organizationName") - } - if dr.Phone == "" { - return merrors.InvalidArgument("phone must not be empty", "request.phone") - } - if dr.WorkEmail == "" { - return merrors.InvalidArgument("work email must not be empty", "request.workEmail") - } - if dr.PayoutVolume == "" { - return merrors.InvalidArgument("payout volume must not be empty", "request.payoutVolume") + if (dr.WorkEmail == "") || (dr.Phone == "") { + return merrors.InvalidArgument("work email or phone must not be empty", "request.workEmail", "request.phone") } return nil } diff --git a/api/proto/contact_request.proto b/api/proto/contact_request.proto new file mode 100644 index 0000000..08f219a --- /dev/null +++ b/api/proto/contact_request.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +option go_package = "github.com/tech/sendico/pkg/generated/gmessaging"; + +message ContactRequestEvent { + string Name = 1; + string Email = 2; + string Phone = 3; + string Company = 4; + string Topic = 5; + string Message = 6; +} diff --git a/api/server/internal/server/siteimp/contact.go b/api/server/internal/server/siteimp/contact.go new file mode 100644 index 0000000..13641bc --- /dev/null +++ b/api/server/internal/server/siteimp/contact.go @@ -0,0 +1,29 @@ +package siteimp + +import ( + "encoding/json" + "net/http" + + "github.com/tech/sendico/pkg/api/http/response" + snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" + "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" +) + +func (a *SiteAPI) contactRequest(r *http.Request) http.HandlerFunc { + var request model.ContactRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + a.logger.Warn("Failed to decode contact request payload", zap.Error(err)) + return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode contact request payload") + } + request.Normalize() + if err := request.Validate(); err != nil { + a.logger.Warn("Contact request validation failed", zap.Error(err)) + return response.BadPayload(a.logger, a.Name(), err) + } + if err := a.producer.SendMessage(snotifications.ContactRequest(a.Name(), &request)); err != nil { + a.logger.Warn("Failed to enqueue contact request notification", zap.Error(err)) + return response.Internal(a.logger, a.Name(), err) + } + return response.Accepted(a.logger, map[string]string{"status": "queued"}) +} diff --git a/api/server/internal/server/siteimp/demo.go b/api/server/internal/server/siteimp/demo.go new file mode 100644 index 0000000..65e54c8 --- /dev/null +++ b/api/server/internal/server/siteimp/demo.go @@ -0,0 +1,31 @@ +package siteimp + +import ( + "encoding/json" + "net/http" + + "github.com/tech/sendico/pkg/api/http/response" + snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" + "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" +) + +func (a *SiteAPI) demoRequest(r *http.Request) http.HandlerFunc { + var request model.DemoRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + a.logger.Warn("Failed to decode demo request payload", zap.Error(err)) + return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode demo request payload") + } + request.Normalize() + if err := request.Validate(); err != nil { + a.logger.Warn("Demo request validation failed", zap.Error(err)) + return response.BadPayload(a.logger, a.Name(), err) + } + + if err := a.producer.SendMessage(snotifications.DemoRequest(a.Name(), &request)); err != nil { + a.logger.Warn("Failed to enqueue demo request notification", zap.Error(err)) + return response.Internal(a.logger, a.Name(), err) + } + + return response.Accepted(a.logger, map[string]string{"status": "queued"}) +} diff --git a/api/server/internal/server/siteimp/service.go b/api/server/internal/server/siteimp/service.go index fce8434..7a59c5a 100644 --- a/api/server/internal/server/siteimp/service.go +++ b/api/server/internal/server/siteimp/service.go @@ -2,18 +2,12 @@ package siteimp import ( "context" - "encoding/json" - "net/http" api "github.com/tech/sendico/pkg/api/http" - "github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/messaging" - snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" "github.com/tech/sendico/pkg/mlogger" - "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" eapi "github.com/tech/sendico/server/interface/api" - "go.uber.org/zap" ) type SiteAPI struct { @@ -29,32 +23,13 @@ func (a *SiteAPI) Finish(_ context.Context) error { return nil } -func (a *SiteAPI) demoRequest(r *http.Request) http.HandlerFunc { - var request model.DemoRequest - if err := json.NewDecoder(r.Body).Decode(&request); err != nil { - a.logger.Warn("Failed to decode demo request payload", zap.Error(err)) - return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode demo request payload") - } - request.Normalize() - if err := request.Validate(); err != nil { - a.logger.Warn("Demo request validation failed", zap.Error(err)) - return response.BadPayload(a.logger, a.Name(), err) - } - - if err := a.producer.SendMessage(snotifications.DemoRequest(a.Name(), &request)); err != nil { - a.logger.Warn("Failed to enqueue demo request notification", zap.Error(err)) - return response.Internal(a.logger, a.Name(), err) - } - - return response.Accepted(a.logger, map[string]string{"status": "queued"}) -} - func CreateAPI(a eapi.API) (*SiteAPI, error) { p := &SiteAPI{ logger: a.Logger().Named(mservice.Site), producer: a.Register().Messaging().Producer(), } - a.Register().Handler(mservice.Site, "/demo/request", api.Post, p.demoRequest) + a.Register().Handler(mservice.Site, "/request/demo", api.Post, p.demoRequest) + a.Register().Handler(mservice.Site, "/request/contact", api.Post, p.contactRequest) return p, nil } From 29f5a56f21e838d96ddcf793c2ca93c9d2e2152b Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 19 Nov 2025 20:15:36 +0100 Subject: [PATCH 25/32] fixed notifications dispatch --- .../server/notificationimp/notification.go | 9 +- .../server/notificationimp/telegram/client.go | 12 +- .../notificationimp/telegram/contact.go | 3 +- .../server/notificationimp/telegram/demo.go | 5 +- .../notificationimp/telegram/message.go | 94 +++++++++++++-- .../notifications/site/notification.go | 110 +++++++++-------- .../messaging/notifications/site/processor.go | 114 +++++++++--------- api/pkg/model/notificationevent.go | 7 +- api/proto/contact_request.proto | 12 -- api/proto/demo_request.proto | 12 -- api/proto/site_request.proto | 36 ++++++ 11 files changed, 258 insertions(+), 156 deletions(-) delete mode 100644 api/proto/contact_request.proto delete mode 100644 api/proto/demo_request.proto create mode 100644 api/proto/site_request.proto diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index 41b3e01..8fb57b8 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -80,13 +80,8 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { return nil, err } - if err := a.Register().Consumer(snotifications.NewDemoRequestProcessor(p.logger, p.onDemoRequest)); err != nil { - p.logger.Error("Failed to register demo request handler", zap.Error(err)) - return nil, err - } - - if err := a.Register().Consumer(snotifications.NewContactRequestProcessor(p.logger, p.onContactRequest)); err != nil { - p.logger.Error("Failed to register contact request handler", zap.Error(err)) + if err := a.Register().Consumer(snotifications.NewSiteRequestProcessor(p.logger, p.onDemoRequest, p.onContactRequest)); err != nil { + p.logger.Error("Failed to register site request handler", zap.Error(err)) return nil, err } diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go index 8b8e68c..1fed9b3 100644 --- a/api/notification/internal/server/notificationimp/telegram/client.go +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -33,7 +33,7 @@ type client struct { botToken string chatID string threadID *int64 - parseMode string + parseMode parseMode } type sendMessagePayload struct { @@ -80,9 +80,9 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er if apiURL == "" { apiURL = defaultAPIURL } - parseMode := strings.TrimSpace(cfg.ParseMode) - if parseMode == "" { - parseMode = "Markdown" + mode := normalizeParseMode(cfg.ParseMode) + if mode == parseModeUnset { + mode = parseModeMarkdown } return &client{ @@ -94,7 +94,7 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er botToken: token, chatID: chatID, threadID: threadID, - parseMode: parseMode, + parseMode: mode, }, nil } @@ -166,7 +166,7 @@ func (c *client) sendForm(ctx context.Context, template messageTemplate) error { payload := sendMessagePayload{ ChatID: c.chatID, Text: message, - ParseMode: c.parseMode, + ParseMode: c.parseMode.String(), ThreadID: c.threadID, DisablePreview: true, } diff --git a/api/notification/internal/server/notificationimp/telegram/contact.go b/api/notification/internal/server/notificationimp/telegram/contact.go index 7f94750..fc73d22 100644 --- a/api/notification/internal/server/notificationimp/telegram/contact.go +++ b/api/notification/internal/server/notificationimp/telegram/contact.go @@ -4,7 +4,8 @@ import "github.com/tech/sendico/pkg/model" func newContactRequestTemplate(request *model.ContactRequest) messageTemplate { return messageTemplate{ - title: "New contact request received", + title: "New site request received", + emphasize: []string{"site request"}, fields: []messageField{ {label: "Name", value: request.Name}, {label: "Email", value: request.Email}, diff --git a/api/notification/internal/server/notificationimp/telegram/demo.go b/api/notification/internal/server/notificationimp/telegram/demo.go index d2afc20..3965f98 100644 --- a/api/notification/internal/server/notificationimp/telegram/demo.go +++ b/api/notification/internal/server/notificationimp/telegram/demo.go @@ -18,7 +18,8 @@ func newDemoRequestTemplate(request *model.DemoRequest) messageTemplate { fields = append(fields, messageField{label: "Comment", value: request.Comment}) } return messageTemplate{ - title: "New demo request received", - fields: fields, + title: "New demo request received", + fields: fields, + emphasize: []string{"demo request"}, } } diff --git a/api/notification/internal/server/notificationimp/telegram/message.go b/api/notification/internal/server/notificationimp/telegram/message.go index db2c515..e8468dc 100644 --- a/api/notification/internal/server/notificationimp/telegram/message.go +++ b/api/notification/internal/server/notificationimp/telegram/message.go @@ -6,23 +6,50 @@ import ( "strings" ) +type parseMode string + +const ( + parseModeUnset parseMode = "" + parseModeMarkdown parseMode = "markdown" + parseModeMarkdownV2 parseMode = "markdownV2" + parseModeHTML parseMode = "HTML" +) + +func normalizeParseMode(value string) parseMode { + switch strings.ToLower(strings.TrimSpace(value)) { + case "markdown": + return parseModeMarkdown + case "markdownv2": + return parseModeMarkdownV2 + case "html": + return parseModeHTML + default: + return parseModeUnset + } +} + +func (pm parseMode) String() string { + return string(pm) +} + type messageField struct { label string value string } type messageTemplate struct { - title string - fields []messageField + title string + fields []messageField + emphasize []string } -func (mt messageTemplate) Format(parseMode string) string { +func (mt messageTemplate) Format(mode parseMode) string { var builder strings.Builder - builder.WriteString(mt.title) + builder.WriteString(formatTitle(mode, mt.title, mt.emphasize)) builder.WriteString("\n") builder.WriteString("-----------------------------\n") - formatter := selectValueFormatter(parseMode) + formatter := selectValueFormatter(mode) for _, field := range mt.fields { appendMessageField(&builder, field.label, field.value, formatter) } @@ -31,6 +58,53 @@ func (mt messageTemplate) Format(parseMode string) string { type valueFormatter func(string) string +func formatTitle(mode parseMode, title string, emphasize []string) string { + switch mode { + case parseModeMarkdown: + return highlightMarkdown(title, emphasize, escapeMarkdown) + case parseModeMarkdownV2: + return highlightMarkdown(title, emphasize, escapeMarkdownV2) + case parseModeHTML: + return highlightHTML(title, emphasize) + default: + return title + } +} + +func highlightMarkdown(title string, emphasize []string, esc func(string) string) string { + if len(emphasize) == 0 { + return title + } + result := title + for _, word := range emphasize { + word = strings.TrimSpace(word) + if word == "" { + continue + } + escaped := esc(word) + replacement := fmt.Sprintf("*%s*", escaped) + result = strings.ReplaceAll(result, word, replacement) + } + return result +} + +func highlightHTML(title string, emphasize []string) string { + if len(emphasize) == 0 { + return html.EscapeString(title) + } + result := html.EscapeString(title) + for _, word := range emphasize { + word = strings.TrimSpace(word) + if word == "" { + continue + } + escaped := html.EscapeString(word) + replacement := fmt.Sprintf("%s", escaped) + result = strings.ReplaceAll(result, escaped, replacement) + } + return result +} + func appendMessageField(builder *strings.Builder, label, value string, formatter valueFormatter) { value = strings.TrimSpace(value) if value == "" { @@ -41,17 +115,17 @@ func appendMessageField(builder *strings.Builder, label, value string, formatter fmt.Fprintf(builder, "• %s: %s\n", label, value) } -func selectValueFormatter(parseMode string) valueFormatter { - switch strings.ToLower(parseMode) { - case "markdown": +func selectValueFormatter(mode parseMode) valueFormatter { + switch mode { + case parseModeMarkdown: return func(value string) string { return fmt.Sprintf("*%s*", escapeMarkdown(value)) } - case "markdownv2": + case parseModeMarkdownV2: return func(value string) string { return fmt.Sprintf("*%s*", escapeMarkdownV2(value)) } - case "html": + case parseModeHTML: return func(value string) string { return fmt.Sprintf("%s", html.EscapeString(value)) } diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go index 9387fc6..47e9ed1 100644 --- a/api/pkg/messaging/internal/notifications/site/notification.go +++ b/api/pkg/messaging/internal/notifications/site/notification.go @@ -10,72 +10,84 @@ import ( "google.golang.org/protobuf/proto" ) -type DemoRequestNotification struct { +type SiteRequestNotification struct { messaging.Envelope - request *model.DemoRequest + requestType gmessaging.SiteRequestEvent_RequestType + demoRequest *model.DemoRequest + contactRequest *model.ContactRequest } -func (drn *DemoRequestNotification) Serialize() ([]byte, error) { - if drn.request == nil { - return nil, merrors.InvalidArgument("demo request payload is empty", "request") +func (srn *SiteRequestNotification) Serialize() ([]byte, error) { + msg := gmessaging.SiteRequestEvent{ + Type: srn.requestType, } - msg := gmessaging.DemoRequestEvent{ - Name: drn.request.Name, - OrganizationName: drn.request.OrganizationName, - Phone: drn.request.Phone, - WorkEmail: drn.request.WorkEmail, - PayoutVolume: drn.request.PayoutVolume, - Comment: drn.request.Comment, + + switch srn.requestType { + case gmessaging.SiteRequestEvent_REQUEST_TYPE_DEMO: + if srn.demoRequest == nil { + return nil, merrors.InvalidArgument("demo request payload is empty", "request") + } + msg.Payload = &gmessaging.SiteRequestEvent_Demo{ + Demo: &gmessaging.SiteDemoRequest{ + Name: srn.demoRequest.Name, + OrganizationName: srn.demoRequest.OrganizationName, + Phone: srn.demoRequest.Phone, + WorkEmail: srn.demoRequest.WorkEmail, + PayoutVolume: srn.demoRequest.PayoutVolume, + Comment: srn.demoRequest.Comment, + }, + } + case gmessaging.SiteRequestEvent_REQUEST_TYPE_CONTACT: + if srn.contactRequest == nil { + return nil, merrors.InvalidArgument("contact request payload is empty", "request") + } + msg.Payload = &gmessaging.SiteRequestEvent_Contact{ + Contact: &gmessaging.SiteContactRequest{ + Name: srn.contactRequest.Name, + Email: srn.contactRequest.Email, + Phone: srn.contactRequest.Phone, + Company: srn.contactRequest.Company, + Topic: srn.contactRequest.Topic, + Message: srn.contactRequest.Message, + }, + } + default: + return nil, merrors.InvalidArgument("unsupported site request type", "type") } + data, err := proto.Marshal(&msg) if err != nil { return nil, err } - return drn.Envelope.Wrap(data) + return srn.Envelope.Wrap(data) +} + +func newSiteRequestEvent() model.NotificationEvent { + return model.NewNotification(mservice.Site, nm.NACreated) } func NewDemoRequestEvent() model.NotificationEvent { - return model.NewNotification(mservice.Site, nm.NACreated) -} - -func NewDemoRequestEnvelope(sender string, request *model.DemoRequest) messaging.Envelope { - return &DemoRequestNotification{ - Envelope: messaging.CreateEnvelope(sender, NewDemoRequestEvent()), - request: request, - } -} - -type ContactRequestNotification struct { - messaging.Envelope - request *model.ContactRequest -} - -func (crn *ContactRequestNotification) Serialize() ([]byte, error) { - if crn.request == nil { - return nil, merrors.InvalidArgument("contact request payload is empty", "request") - } - msg := gmessaging.ContactRequestEvent{ - Name: crn.request.Name, - Email: crn.request.Email, - Phone: crn.request.Phone, - Company: crn.request.Company, - Topic: crn.request.Topic, - Message: crn.request.Message, - } - data, err := proto.Marshal(&msg) - if err != nil { - return nil, err - } - return crn.Envelope.Wrap(data) + return newSiteRequestEvent() } func NewContactRequestEvent() model.NotificationEvent { - return model.NewNotification(mservice.Site, nm.NACreated) + return newSiteRequestEvent() +} + +func NewDemoRequestEnvelope(sender string, request *model.DemoRequest) messaging.Envelope { + return &SiteRequestNotification{ + Envelope: messaging.CreateEnvelope(sender, newSiteRequestEvent()), + requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_DEMO, + demoRequest: request, + contactRequest: nil, + } } func NewContactRequestEnvelope(sender string, request *model.ContactRequest) messaging.Envelope { - return &ContactRequestNotification{ - Envelope: messaging.CreateEnvelope(sender, NewContactRequestEvent()), - request: request, + return &SiteRequestNotification{ + Envelope: messaging.CreateEnvelope(sender, newSiteRequestEvent()), + requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_CONTACT, + contactRequest: request, + demoRequest: nil, } } diff --git a/api/pkg/messaging/notifications/site/processor.go b/api/pkg/messaging/notifications/site/processor.go index a58ea29..1cc70f4 100644 --- a/api/pkg/messaging/notifications/site/processor.go +++ b/api/pkg/messaging/notifications/site/processor.go @@ -14,72 +14,74 @@ import ( "google.golang.org/protobuf/proto" ) -type DemoRequestProcessor struct { - logger mlogger.Logger - handler handler.DemoRequestHandler - event model.NotificationEvent +type SiteRequestProcessor struct { + logger mlogger.Logger + demoHandler handler.DemoRequestHandler + contactHandler handler.ContactRequestHandler + event model.NotificationEvent } -func (drp *DemoRequestProcessor) Process(ctx context.Context, envelope me.Envelope) error { - var msg gmessaging.DemoRequestEvent +func (srp *SiteRequestProcessor) Process(ctx context.Context, envelope me.Envelope) error { + var msg gmessaging.SiteRequestEvent if err := proto.Unmarshal(envelope.GetData(), &msg); err != nil { - drp.logger.Warn("Failed to decode demo request envelope", zap.Error(err), zap.String("topic", drp.event.ToString())) + srp.logger.Warn("Failed to decode site request envelope", zap.Error(err), zap.String("topic", srp.event.ToString())) return err } - request := &model.DemoRequest{ - Name: msg.GetName(), - OrganizationName: msg.GetOrganizationName(), - Phone: msg.GetPhone(), - WorkEmail: msg.GetWorkEmail(), - PayoutVolume: msg.GetPayoutVolume(), - Comment: msg.GetComment(), - } - return drp.handler(ctx, request) -} -func (drp *DemoRequestProcessor) GetSubject() model.NotificationEvent { - return drp.event -} - -func NewDemoRequestProcessor(logger mlogger.Logger, handler handler.DemoRequestHandler) np.EnvelopeProcessor { - return &DemoRequestProcessor{ - logger: logger.Named("demo_request_processor"), - handler: handler, - event: internalsite.NewDemoRequestEvent(), + switch msg.GetType() { + case gmessaging.SiteRequestEvent_REQUEST_TYPE_DEMO: + if srp.demoHandler == nil { + srp.logger.Warn("Demo request handler is not configured") + return nil + } + demo := msg.GetDemo() + if demo == nil { + srp.logger.Warn("Demo request payload is empty") + return nil + } + request := &model.DemoRequest{ + Name: demo.GetName(), + OrganizationName: demo.GetOrganizationName(), + Phone: demo.GetPhone(), + WorkEmail: demo.GetWorkEmail(), + PayoutVolume: demo.GetPayoutVolume(), + Comment: demo.GetComment(), + } + return srp.demoHandler(ctx, request) + case gmessaging.SiteRequestEvent_REQUEST_TYPE_CONTACT: + if srp.contactHandler == nil { + srp.logger.Warn("Contact request handler is not configured") + return nil + } + contact := msg.GetContact() + if contact == nil { + srp.logger.Warn("Contact request payload is empty") + return nil + } + request := &model.ContactRequest{ + Name: contact.GetName(), + Email: contact.GetEmail(), + Phone: contact.GetPhone(), + Company: contact.GetCompany(), + Topic: contact.GetTopic(), + Message: contact.GetMessage(), + } + return srp.contactHandler(ctx, request) + default: + srp.logger.Warn("Received site request with unsupported type", zap.Any("type", msg.GetType())) + return nil } } -type ContactRequestProcessor struct { - logger mlogger.Logger - handler handler.ContactRequestHandler - event model.NotificationEvent +func (srp *SiteRequestProcessor) GetSubject() model.NotificationEvent { + return srp.event } -func (crp *ContactRequestProcessor) Process(ctx context.Context, envelope me.Envelope) error { - var msg gmessaging.ContactRequestEvent - if err := proto.Unmarshal(envelope.GetData(), &msg); err != nil { - crp.logger.Warn("Failed to decode contact request envelope", zap.Error(err), zap.String("topic", crp.event.ToString())) - return err - } - request := &model.ContactRequest{ - Name: msg.GetName(), - Email: msg.GetEmail(), - Phone: msg.GetPhone(), - Company: msg.GetCompany(), - Topic: msg.GetTopic(), - Message: msg.GetMessage(), - } - return crp.handler(ctx, request) -} - -func (crp *ContactRequestProcessor) GetSubject() model.NotificationEvent { - return crp.event -} - -func NewContactRequestProcessor(logger mlogger.Logger, handler handler.ContactRequestHandler) np.EnvelopeProcessor { - return &ContactRequestProcessor{ - logger: logger.Named("contact_request_processor"), - handler: handler, - event: internalsite.NewContactRequestEvent(), +func NewSiteRequestProcessor(logger mlogger.Logger, demo handler.DemoRequestHandler, contact handler.ContactRequestHandler) np.EnvelopeProcessor { + return &SiteRequestProcessor{ + logger: logger.Named("site_request_processor"), + demoHandler: demo, + contactHandler: contact, + event: internalsite.NewDemoRequestEvent(), } } diff --git a/api/pkg/model/notificationevent.go b/api/pkg/model/notificationevent.go index de52d20..d6d0024 100644 --- a/api/pkg/model/notificationevent.go +++ b/api/pkg/model/notificationevent.go @@ -71,7 +71,12 @@ func FromString(s string) (*NotificationEventImp, error) { func StringToNotificationAction(s string) (nm.NotificationAction, error) { switch nm.NotificationAction(s) { - case nm.NACreated, nm.NAPending, nm.NAUpdated, nm.NADeleted, nm.NAAssigned, nm.NAPasswordReset: + case nm.NACreated, + nm.NAPending, + nm.NAUpdated, + nm.NADeleted, + nm.NAAssigned, + nm.NAPasswordReset: return nm.NotificationAction(s), nil default: return "", merrors.DataConflict("invalid Notification action: " + s) diff --git a/api/proto/contact_request.proto b/api/proto/contact_request.proto deleted file mode 100644 index 08f219a..0000000 --- a/api/proto/contact_request.proto +++ /dev/null @@ -1,12 +0,0 @@ -syntax = "proto3"; - -option go_package = "github.com/tech/sendico/pkg/generated/gmessaging"; - -message ContactRequestEvent { - string Name = 1; - string Email = 2; - string Phone = 3; - string Company = 4; - string Topic = 5; - string Message = 6; -} diff --git a/api/proto/demo_request.proto b/api/proto/demo_request.proto deleted file mode 100644 index 68aeea5..0000000 --- a/api/proto/demo_request.proto +++ /dev/null @@ -1,12 +0,0 @@ -syntax = "proto3"; - -option go_package = "github.com/tech/sendico/pkg/generated/gmessaging"; - -message DemoRequestEvent { - string Name = 1; - string OrganizationName = 2; - string Phone = 3; - string WorkEmail = 4; - string PayoutVolume = 5; - string Comment = 6; -} diff --git a/api/proto/site_request.proto b/api/proto/site_request.proto new file mode 100644 index 0000000..9187d17 --- /dev/null +++ b/api/proto/site_request.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +option go_package = "github.com/tech/sendico/pkg/generated/gmessaging"; + +message SiteRequestEvent { + enum RequestType { + REQUEST_TYPE_UNSPECIFIED = 0; + REQUEST_TYPE_DEMO = 1; + REQUEST_TYPE_CONTACT = 2; + } + + RequestType type = 1; + + oneof payload { + SiteDemoRequest demo = 2; + SiteContactRequest contact = 3; + } +} + +message SiteDemoRequest { + string name = 1; + string organization_name = 2; + string phone = 3; + string work_email = 4; + string payout_volume = 5; + string comment = 6; +} + +message SiteContactRequest { + string name = 1; + string email = 2; + string phone = 3; + string company = 4; + string topic = 5; + string message = 6; +} From 56d6c8caa62284078cff2dcfb3a9e6820dbf9c59 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 19 Nov 2025 22:04:27 +0100 Subject: [PATCH 26/32] fixed requests filtration --- api/pkg/model/contactrequest.go | 2 +- api/pkg/model/demorequest.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/pkg/model/contactrequest.go b/api/pkg/model/contactrequest.go index 30252ab..3bdc513 100644 --- a/api/pkg/model/contactrequest.go +++ b/api/pkg/model/contactrequest.go @@ -34,7 +34,7 @@ func (cr *ContactRequest) Validate() error { if cr == nil { return merrors.InvalidArgument("request payload is empty", "request") } - if (cr.Email == "") || (cr.Phone == "") { + if (cr.Email == "") && (cr.Phone == "") { return merrors.InvalidArgument("email or phone must not be empty", "request.email", "request.phone") } return nil diff --git a/api/pkg/model/demorequest.go b/api/pkg/model/demorequest.go index 709447c..6bbe605 100644 --- a/api/pkg/model/demorequest.go +++ b/api/pkg/model/demorequest.go @@ -34,7 +34,7 @@ func (dr *DemoRequest) Validate() error { if dr == nil { return merrors.InvalidArgument("request payload is empty", "request") } - if (dr.WorkEmail == "") || (dr.Phone == "") { + if (dr.WorkEmail == "") && (dr.Phone == "") { return merrors.InvalidArgument("work email or phone must not be empty", "request.workEmail", "request.phone") } return nil From 36d1a94cf6b71c9ef89a48ed4bcb0ab8a6fe437e Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 19 Nov 2025 22:19:27 +0100 Subject: [PATCH 27/32] + call request --- .../server/notificationimp/notification.go | 14 ++++++- .../server/notificationimp/telegram/call.go | 18 ++++++++ .../server/notificationimp/telegram/client.go | 8 ++++ .../notificationimp/telegram/message.go | 4 +- .../notifications/site/notification.go | 31 ++++++++++++++ .../notifications/site/call_request.go | 11 +++++ .../notifications/site/handler/handler.go | 1 + .../messaging/notifications/site/processor.go | 23 ++++++++++- api/pkg/model/callrequest.go | 41 +++++++++++++++++++ api/proto/site_request.proto | 11 +++++ api/server/internal/server/siteimp/call.go | 29 +++++++++++++ api/server/internal/server/siteimp/contact.go | 2 +- api/server/internal/server/siteimp/demo.go | 2 +- .../internal/server/siteimp/response.go | 19 +++++++++ api/server/internal/server/siteimp/service.go | 1 + 15 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 api/notification/internal/server/notificationimp/telegram/call.go create mode 100644 api/pkg/messaging/notifications/site/call_request.go create mode 100644 api/pkg/model/callrequest.go create mode 100644 api/server/internal/server/siteimp/call.go create mode 100644 api/server/internal/server/siteimp/response.go diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index 8fb57b8..2805652 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -80,7 +80,7 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { return nil, err } - if err := a.Register().Consumer(snotifications.NewSiteRequestProcessor(p.logger, p.onDemoRequest, p.onContactRequest)); err != nil { + if err := a.Register().Consumer(snotifications.NewSiteRequestProcessor(p.logger, p.onDemoRequest, p.onContactRequest, p.onCallRequest)); err != nil { p.logger.Error("Failed to register site request handler", zap.Error(err)) return nil, err } @@ -111,3 +111,15 @@ func (a *NotificationAPI) onContactRequest(ctx context.Context, request *model.C a.logger.Info("Contact request sent via Telegram", zap.String("name", request.Name), zap.String("topic", request.Topic)) return nil } + +func (a *NotificationAPI) onCallRequest(ctx context.Context, request *model.CallRequest) error { + if a.tg == nil { + return merrors.Internal("telegram client is not configured") + } + if err := a.tg.SendCallRequest(ctx, request); err != nil { + a.logger.Warn("Failed to send call request via telegram", zap.Error(err)) + return err + } + a.logger.Info("Call request sent via Telegram", zap.String("phone", request.Phone)) + return nil +} diff --git a/api/notification/internal/server/notificationimp/telegram/call.go b/api/notification/internal/server/notificationimp/telegram/call.go new file mode 100644 index 0000000..7a445c4 --- /dev/null +++ b/api/notification/internal/server/notificationimp/telegram/call.go @@ -0,0 +1,18 @@ +package telegram + +import "github.com/tech/sendico/pkg/model" + +func newCallRequestTemplate(request *model.CallRequest) messageTemplate { + return messageTemplate{ + title: "New call request received", + emphasize: []string{"call request"}, + fields: []messageField{ + {label: "Name", value: request.Name}, + {label: "Phone", value: request.Phone}, + {label: "Email", value: request.Email}, + {label: "Company", value: request.Company}, + {label: "Preferred time", value: request.PreferredTime}, + {label: "Message", value: request.Message}, + }, + } +} diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go index 1fed9b3..aabc0eb 100644 --- a/api/notification/internal/server/notificationimp/telegram/client.go +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -24,6 +24,7 @@ const defaultAPIURL = "https://api.telegram.org" type Client interface { SendDemoRequest(ctx context.Context, request *model.DemoRequest) error SendContactRequest(ctx context.Context, request *model.ContactRequest) error + SendCallRequest(ctx context.Context, request *model.CallRequest) error } type client struct { @@ -161,6 +162,13 @@ func (c *client) SendContactRequest(ctx context.Context, request *model.ContactR return c.sendForm(ctx, newContactRequestTemplate(request)) } +func (c *client) SendCallRequest(ctx context.Context, request *model.CallRequest) error { + if request == nil { + return merrors.InvalidArgument("call request payload is nil", "request") + } + return c.sendForm(ctx, newCallRequestTemplate(request)) +} + func (c *client) sendForm(ctx context.Context, template messageTemplate) error { message := template.Format(c.parseMode) payload := sendMessagePayload{ diff --git a/api/notification/internal/server/notificationimp/telegram/message.go b/api/notification/internal/server/notificationimp/telegram/message.go index e8468dc..8ec7ae9 100644 --- a/api/notification/internal/server/notificationimp/telegram/message.go +++ b/api/notification/internal/server/notificationimp/telegram/message.go @@ -10,8 +10,8 @@ type parseMode string const ( parseModeUnset parseMode = "" - parseModeMarkdown parseMode = "markdown" - parseModeMarkdownV2 parseMode = "markdownV2" + parseModeMarkdown parseMode = "Markdown" + parseModeMarkdownV2 parseMode = "MarkdownV2" parseModeHTML parseMode = "HTML" ) diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go index 47e9ed1..d496272 100644 --- a/api/pkg/messaging/internal/notifications/site/notification.go +++ b/api/pkg/messaging/internal/notifications/site/notification.go @@ -15,6 +15,7 @@ type SiteRequestNotification struct { requestType gmessaging.SiteRequestEvent_RequestType demoRequest *model.DemoRequest contactRequest *model.ContactRequest + callRequest *model.CallRequest } func (srn *SiteRequestNotification) Serialize() ([]byte, error) { @@ -51,6 +52,20 @@ func (srn *SiteRequestNotification) Serialize() ([]byte, error) { Message: srn.contactRequest.Message, }, } + case gmessaging.SiteRequestEvent_REQUEST_TYPE_CALL: + if srn.callRequest == nil { + return nil, merrors.InvalidArgument("call request payload is empty", "request") + } + msg.Payload = &gmessaging.SiteRequestEvent_Call{ + Call: &gmessaging.SiteCallRequest{ + Name: srn.callRequest.Name, + Phone: srn.callRequest.Phone, + Email: srn.callRequest.Email, + Company: srn.callRequest.Company, + PreferredTime: srn.callRequest.PreferredTime, + Message: srn.callRequest.Message, + }, + } default: return nil, merrors.InvalidArgument("unsupported site request type", "type") } @@ -74,12 +89,17 @@ func NewContactRequestEvent() model.NotificationEvent { return newSiteRequestEvent() } +func NewCallRequestEvent() model.NotificationEvent { + return newSiteRequestEvent() +} + func NewDemoRequestEnvelope(sender string, request *model.DemoRequest) messaging.Envelope { return &SiteRequestNotification{ Envelope: messaging.CreateEnvelope(sender, newSiteRequestEvent()), requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_DEMO, demoRequest: request, contactRequest: nil, + callRequest: nil, } } @@ -89,5 +109,16 @@ func NewContactRequestEnvelope(sender string, request *model.ContactRequest) mes requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_CONTACT, contactRequest: request, demoRequest: nil, + callRequest: nil, + } +} + +func NewCallRequestEnvelope(sender string, request *model.CallRequest) messaging.Envelope { + return &SiteRequestNotification{ + Envelope: messaging.CreateEnvelope(sender, newSiteRequestEvent()), + requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_CALL, + callRequest: request, + demoRequest: nil, + contactRequest: nil, } } diff --git a/api/pkg/messaging/notifications/site/call_request.go b/api/pkg/messaging/notifications/site/call_request.go new file mode 100644 index 0000000..b1d92ad --- /dev/null +++ b/api/pkg/messaging/notifications/site/call_request.go @@ -0,0 +1,11 @@ +package notifications + +import ( + messaging "github.com/tech/sendico/pkg/messaging/envelope" + internalsite "github.com/tech/sendico/pkg/messaging/internal/notifications/site" + "github.com/tech/sendico/pkg/model" +) + +func CallRequest(sender string, request *model.CallRequest) messaging.Envelope { + return internalsite.NewCallRequestEnvelope(sender, request) +} diff --git a/api/pkg/messaging/notifications/site/handler/handler.go b/api/pkg/messaging/notifications/site/handler/handler.go index 4d525a8..54a369c 100644 --- a/api/pkg/messaging/notifications/site/handler/handler.go +++ b/api/pkg/messaging/notifications/site/handler/handler.go @@ -8,3 +8,4 @@ import ( type DemoRequestHandler = func(context.Context, *model.DemoRequest) error type ContactRequestHandler = func(context.Context, *model.ContactRequest) error +type CallRequestHandler = func(context.Context, *model.CallRequest) error diff --git a/api/pkg/messaging/notifications/site/processor.go b/api/pkg/messaging/notifications/site/processor.go index 1cc70f4..5952340 100644 --- a/api/pkg/messaging/notifications/site/processor.go +++ b/api/pkg/messaging/notifications/site/processor.go @@ -18,6 +18,7 @@ type SiteRequestProcessor struct { logger mlogger.Logger demoHandler handler.DemoRequestHandler contactHandler handler.ContactRequestHandler + callHandler handler.CallRequestHandler event model.NotificationEvent } @@ -67,6 +68,25 @@ func (srp *SiteRequestProcessor) Process(ctx context.Context, envelope me.Envelo Message: contact.GetMessage(), } return srp.contactHandler(ctx, request) + case gmessaging.SiteRequestEvent_REQUEST_TYPE_CALL: + if srp.callHandler == nil { + srp.logger.Warn("Call request handler is not configured") + return nil + } + call := msg.GetCall() + if call == nil { + srp.logger.Warn("Call request payload is empty") + return nil + } + request := &model.CallRequest{ + Name: call.GetName(), + Phone: call.GetPhone(), + Email: call.GetEmail(), + Company: call.GetCompany(), + PreferredTime: call.GetPreferredTime(), + Message: call.GetMessage(), + } + return srp.callHandler(ctx, request) default: srp.logger.Warn("Received site request with unsupported type", zap.Any("type", msg.GetType())) return nil @@ -77,11 +97,12 @@ func (srp *SiteRequestProcessor) GetSubject() model.NotificationEvent { return srp.event } -func NewSiteRequestProcessor(logger mlogger.Logger, demo handler.DemoRequestHandler, contact handler.ContactRequestHandler) np.EnvelopeProcessor { +func NewSiteRequestProcessor(logger mlogger.Logger, demo handler.DemoRequestHandler, contact handler.ContactRequestHandler, call handler.CallRequestHandler) np.EnvelopeProcessor { return &SiteRequestProcessor{ logger: logger.Named("site_request_processor"), demoHandler: demo, contactHandler: contact, + callHandler: call, event: internalsite.NewDemoRequestEvent(), } } diff --git a/api/pkg/model/callrequest.go b/api/pkg/model/callrequest.go new file mode 100644 index 0000000..68fee09 --- /dev/null +++ b/api/pkg/model/callrequest.go @@ -0,0 +1,41 @@ +package model + +import ( + "strings" + + "github.com/tech/sendico/pkg/merrors" +) + +// CallRequest represents a request to schedule a call from the marketing site. +type CallRequest struct { + Name string `json:"name"` + Phone string `json:"phone"` + Email string `json:"email"` + Company string `json:"company"` + PreferredTime string `json:"preferredTime"` + Message string `json:"message"` +} + +// Normalize trims whitespace from all string fields. +func (cr *CallRequest) Normalize() { + if cr == nil { + return + } + cr.Name = strings.TrimSpace(cr.Name) + cr.Phone = strings.TrimSpace(cr.Phone) + cr.Email = strings.TrimSpace(cr.Email) + cr.Company = strings.TrimSpace(cr.Company) + cr.PreferredTime = strings.TrimSpace(cr.PreferredTime) + cr.Message = strings.TrimSpace(cr.Message) +} + +// Validate ensures required call request fields are present. +func (cr *CallRequest) Validate() error { + if cr == nil { + return merrors.InvalidArgument("request payload is empty", "request") + } + if cr.Phone == "" { + return merrors.InvalidArgument("phone must not be empty", "request.phone") + } + return nil +} diff --git a/api/proto/site_request.proto b/api/proto/site_request.proto index 9187d17..98db4e6 100644 --- a/api/proto/site_request.proto +++ b/api/proto/site_request.proto @@ -7,6 +7,7 @@ message SiteRequestEvent { REQUEST_TYPE_UNSPECIFIED = 0; REQUEST_TYPE_DEMO = 1; REQUEST_TYPE_CONTACT = 2; + REQUEST_TYPE_CALL = 3; } RequestType type = 1; @@ -14,6 +15,7 @@ message SiteRequestEvent { oneof payload { SiteDemoRequest demo = 2; SiteContactRequest contact = 3; + SiteCallRequest call = 4; } } @@ -34,3 +36,12 @@ message SiteContactRequest { string topic = 5; string message = 6; } + +message SiteCallRequest { + string name = 1; + string phone = 2; + string email = 3; + string company = 4; + string preferred_time = 5; + string message = 6; +} diff --git a/api/server/internal/server/siteimp/call.go b/api/server/internal/server/siteimp/call.go new file mode 100644 index 0000000..2e7a5fd --- /dev/null +++ b/api/server/internal/server/siteimp/call.go @@ -0,0 +1,29 @@ +package siteimp + +import ( + "encoding/json" + "net/http" + + "github.com/tech/sendico/pkg/api/http/response" + snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" + "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" +) + +func (a *SiteAPI) callRequest(r *http.Request) http.HandlerFunc { + var request model.CallRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + a.logger.Warn("Failed to decode call request payload", zap.Error(err)) + return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode call request payload") + } + request.Normalize() + if err := request.Validate(); err != nil { + a.logger.Warn("Call request validation failed", zap.Error(err)) + return response.BadPayload(a.logger, a.Name(), err) + } + if err := a.producer.SendMessage(snotifications.CallRequest(a.Name(), &request)); err != nil { + a.logger.Warn("Failed to enqueue call request notification", zap.Error(err)) + return response.Internal(a.logger, a.Name(), err) + } + return a.acceptedQueued() +} diff --git a/api/server/internal/server/siteimp/contact.go b/api/server/internal/server/siteimp/contact.go index 13641bc..ab0ceb9 100644 --- a/api/server/internal/server/siteimp/contact.go +++ b/api/server/internal/server/siteimp/contact.go @@ -25,5 +25,5 @@ func (a *SiteAPI) contactRequest(r *http.Request) http.HandlerFunc { a.logger.Warn("Failed to enqueue contact request notification", zap.Error(err)) return response.Internal(a.logger, a.Name(), err) } - return response.Accepted(a.logger, map[string]string{"status": "queued"}) + return a.acceptedQueued() } diff --git a/api/server/internal/server/siteimp/demo.go b/api/server/internal/server/siteimp/demo.go index 65e54c8..99cb590 100644 --- a/api/server/internal/server/siteimp/demo.go +++ b/api/server/internal/server/siteimp/demo.go @@ -27,5 +27,5 @@ func (a *SiteAPI) demoRequest(r *http.Request) http.HandlerFunc { return response.Internal(a.logger, a.Name(), err) } - return response.Accepted(a.logger, map[string]string{"status": "queued"}) + return a.acceptedQueued() } diff --git a/api/server/internal/server/siteimp/response.go b/api/server/internal/server/siteimp/response.go new file mode 100644 index 0000000..739d9b5 --- /dev/null +++ b/api/server/internal/server/siteimp/response.go @@ -0,0 +1,19 @@ +package siteimp + +import ( + "net/http" + + "github.com/tech/sendico/pkg/api/http/response" +) + +type enqueueResponse struct { + Status string `json:"status"` +} + +func newEnqueueResponse() enqueueResponse { + return enqueueResponse{Status: "queued"} +} + +func (a *SiteAPI) acceptedQueued() http.HandlerFunc { + return response.Accepted(a.logger, newEnqueueResponse()) +} diff --git a/api/server/internal/server/siteimp/service.go b/api/server/internal/server/siteimp/service.go index 7a59c5a..0fd68ef 100644 --- a/api/server/internal/server/siteimp/service.go +++ b/api/server/internal/server/siteimp/service.go @@ -31,5 +31,6 @@ func CreateAPI(a eapi.API) (*SiteAPI, error) { a.Register().Handler(mservice.Site, "/request/demo", api.Post, p.demoRequest) a.Register().Handler(mservice.Site, "/request/contact", api.Post, p.contactRequest) + a.Register().Handler(mservice.Site, "/request/call", api.Post, p.callRequest) return p, nil } From ef5b3dc1a76c98addb501ad714de70741297f0bf Mon Sep 17 00:00:00 2001 From: Stephan D Date: Thu, 20 Nov 2025 00:47:14 +0100 Subject: [PATCH 28/32] extended message set --- api/pkg/model/callrequest.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/pkg/model/callrequest.go b/api/pkg/model/callrequest.go index 68fee09..d237c42 100644 --- a/api/pkg/model/callrequest.go +++ b/api/pkg/model/callrequest.go @@ -34,8 +34,8 @@ func (cr *CallRequest) Validate() error { if cr == nil { return merrors.InvalidArgument("request payload is empty", "request") } - if cr.Phone == "" { - return merrors.InvalidArgument("phone must not be empty", "request.phone") + if cr.Phone == "" && cr.Email == "" { + return merrors.InvalidArgument("phone or email must not be empty", "request.phone", "request.email") } return nil } From e1e4c580e8a0ab420e0c745dab1f98c4b35d1608 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 21 Nov 2025 16:41:41 +0100 Subject: [PATCH 29/32] New code verification service --- api/billing/fees/go.mod | 6 +- api/billing/fees/go.sum | 42 ++-- api/chain/gateway/go.mod | 6 +- api/chain/gateway/go.sum | 52 ++--- api/fx/ingestor/go.mod | 6 +- api/fx/ingestor/go.sum | 54 ++---- api/fx/oracle/go.mod | 6 +- api/fx/oracle/go.sum | 42 ++-- api/fx/storage/go.mod | 4 +- api/fx/storage/go.sum | 36 ++-- api/ledger/go.mod | 6 +- api/ledger/go.sum | 42 ++-- api/notification/go.mod | 6 +- api/notification/go.sum | 42 ++-- api/notification/i18n/en.json | 4 + api/notification/i18n/ru.json | 6 +- api/notification/i18n/uk.json | 4 + .../server/notificationimp/confcode.go | 27 +++ .../server/notificationimp/notification.go | 6 + api/payments/orchestrator/go.mod | 6 +- api/payments/orchestrator/go.sum | 50 +++-- 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 + .../repositoryimp/builderimp/pipeline_test.go | 18 +- api/pkg/go.mod | 6 +- api/pkg/go.sum | 46 ++--- .../confirmation/notification.go | 47 +++++ .../notifications/confirmation/processor.go | 70 +++++++ .../confirmation/confirmation.go | 20 ++ .../confirmation/handler/interface.go | 9 + api/pkg/model/confirmation.go | 43 +++++ api/pkg/mservice/services.go | 26 +-- api/server/go.mod | 35 ++-- api/server/go.sum | 70 +++---- api/server/interface/api/register.go | 1 + .../interface/api/sresponse/login_pending.go | 31 +++ .../interface/api/sresponse/response.go | 6 +- api/server/interface/model/token.go | 23 +++ .../services/confirmation/confirmation.go | 11 ++ api/server/internal/api/api.go | 2 + api/server/internal/api/middleware.go | 12 +- .../api/routers/authorized/handler.go | 18 ++ api/server/internal/api/routers/dispatcher.go | 9 +- .../internal/api/routers/endpoint/token.go | 10 + .../internal/api/routers/public/login.go | 22 ++- .../internal/api/routers/public/respond.go | 56 ++---- .../internal/api/routers/public/router.go | 37 +++- api/server/internal/api/routers/router.go | 1 + .../internal/api/routers/tokens/tokens.go | 65 +++++++ .../server/confirmationimp/request.go | 48 +++++ .../internal/server/confirmationimp/resend.go | 56 ++++++ .../server/confirmationimp/service.go | 158 +++++++++++++++ .../internal/server/confirmationimp/store.go | 181 ++++++++++++++++++ .../internal/server/confirmationimp/types.go | 23 +++ .../internal/server/confirmationimp/verify.go | 88 +++++++++ .../lib/api/responses/login_pending.dart | 26 +++ .../lib/models/auth/login_outcome.dart | 17 ++ .../lib/models/auth/pending_login.dart | 33 ++++ .../lib/models/session_identifier.dart | 18 ++ frontend/pshared/lib/provider/account.dart | 25 ++- frontend/pshared/lib/service/account.dart | 41 +++- .../lib/service/authorization/service.dart | 20 +- frontend/pweb/lib/main.dart | 18 +- frontend/pweb/lib/pages/2fa/resend.dart | 9 +- frontend/pweb/lib/pages/login/form.dart | 11 +- frontend/pweb/lib/providers/two_factor.dart | 47 ++++- frontend/pweb/lib/services/auth.dart | 12 -- .../Flutter/GeneratedPluginRegistrant.swift | 2 - 72 files changed, 1660 insertions(+), 454 deletions(-) create mode 100644 api/notification/internal/server/notificationimp/confcode.go create mode 100644 api/pkg/db/confirmation/confirmation.go create mode 100644 api/pkg/db/internal/mongo/confirmationdb/db.go create mode 100644 api/pkg/db/internal/mongo/confirmationdb/delete.go create mode 100644 api/pkg/db/internal/mongo/confirmationdb/find.go create mode 100644 api/pkg/messaging/internal/notifications/confirmation/notification.go create mode 100644 api/pkg/messaging/internal/notifications/confirmation/processor.go create mode 100644 api/pkg/messaging/notifications/confirmation/confirmation.go create mode 100644 api/pkg/messaging/notifications/confirmation/handler/interface.go create mode 100644 api/pkg/model/confirmation.go create mode 100644 api/server/interface/api/sresponse/login_pending.go create mode 100644 api/server/interface/services/confirmation/confirmation.go create mode 100644 api/server/internal/api/routers/tokens/tokens.go create mode 100644 api/server/internal/server/confirmationimp/request.go create mode 100644 api/server/internal/server/confirmationimp/resend.go create mode 100644 api/server/internal/server/confirmationimp/service.go create mode 100644 api/server/internal/server/confirmationimp/store.go create mode 100644 api/server/internal/server/confirmationimp/types.go create mode 100644 api/server/internal/server/confirmationimp/verify.go create mode 100644 frontend/pshared/lib/api/responses/login_pending.dart create mode 100644 frontend/pshared/lib/models/auth/login_outcome.dart create mode 100644 frontend/pshared/lib/models/auth/pending_login.dart create mode 100644 frontend/pshared/lib/models/session_identifier.dart delete mode 100644 frontend/pweb/lib/services/auth.dart diff --git a/api/billing/fees/go.mod b/api/billing/fees/go.mod index 10bae4f..91255a8 100644 --- a/api/billing/fees/go.mod +++ b/api/billing/fees/go.mod @@ -10,7 +10,7 @@ require ( github.com/tech/sendico/fx/oracle v0.0.0 github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver v1.17.6 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 google.golang.org/grpc v1.77.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -36,7 +36,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect @@ -44,7 +44,7 @@ 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.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/api/billing/fees/go.sum b/api/billing/fees/go.sum index 13a4a5d..91ffdb4 100644 --- a/api/billing/fees/go.sum +++ b/api/billing/fees/go.sum @@ -9,8 +9,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= -github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM= github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= @@ -117,10 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= -github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -156,32 +152,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/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= @@ -218,8 +214,6 @@ 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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= diff --git a/api/chain/gateway/go.mod b/api/chain/gateway/go.mod index 3f79cec..be00771 100644 --- a/api/chain/gateway/go.mod +++ b/api/chain/gateway/go.mod @@ -14,7 +14,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver v1.17.6 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 @@ -65,7 +65,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect @@ -79,7 +79,7 @@ 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.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect diff --git a/api/chain/gateway/go.sum b/api/chain/gateway/go.sum index 704ef47..d7bb493 100644 --- a/api/chain/gateway/go.sum +++ b/api/chain/gateway/go.sum @@ -6,25 +6,17 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f h1:B/TfTw73mVqWKDzJZhU9Qi9wQyYfmiCz9FnmpQsyv5M= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251117160429-c598d23eddcf h1:aZI2VRIP0LAI6Rw934WEAxxL0SNYSVt9vR9h/cP5Pbo= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251117160429-c598d23eddcf/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7 h1:uups37roJCTtR/BrJa0WoMrxt3rzgV+Qrj+TxYyJoAo= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y= -github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= -github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM= github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= @@ -247,10 +239,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= -github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= @@ -285,12 +275,8 @@ github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5 github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY= github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= @@ -310,32 +296,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/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= @@ -378,8 +364,6 @@ 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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= diff --git a/api/fx/ingestor/go.mod b/api/fx/ingestor/go.mod index c856265..50ced22 100644 --- a/api/fx/ingestor/go.mod +++ b/api/fx/ingestor/go.mod @@ -12,7 +12,7 @@ require ( github.com/prometheus/client_golang v1.23.2 github.com/tech/sendico/fx/storage v0.0.0 github.com/tech/sendico/pkg v0.1.0 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -35,7 +35,7 @@ require ( github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect @@ -44,7 +44,7 @@ require ( go.mongodb.org/mongo-driver v1.17.6 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/api/fx/ingestor/go.sum b/api/fx/ingestor/go.sum index 53148e5..91ffdb4 100644 --- a/api/fx/ingestor/go.sum +++ b/api/fx/ingestor/go.sum @@ -9,8 +9,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= -github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM= github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= @@ -117,10 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= -github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -156,47 +152,41 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/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.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -205,8 +195,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -215,8 +203,6 @@ 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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -226,12 +212,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-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= diff --git a/api/fx/oracle/go.mod b/api/fx/oracle/go.mod index 1659166..b1b2718 100644 --- a/api/fx/oracle/go.mod +++ b/api/fx/oracle/go.mod @@ -12,7 +12,7 @@ require ( github.com/tech/sendico/fx/storage v0.0.0 github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver v1.17.6 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 @@ -37,7 +37,7 @@ require ( github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect @@ -45,7 +45,7 @@ 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.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/api/fx/oracle/go.sum b/api/fx/oracle/go.sum index 13a4a5d..91ffdb4 100644 --- a/api/fx/oracle/go.sum +++ b/api/fx/oracle/go.sum @@ -9,8 +9,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= -github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM= github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= @@ -117,10 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= -github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -156,32 +152,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/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= @@ -218,8 +214,6 @@ 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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= diff --git a/api/fx/storage/go.mod b/api/fx/storage/go.mod index 8dd247c..7b1bf9b 100644 --- a/api/fx/storage/go.mod +++ b/api/fx/storage/go.mod @@ -7,7 +7,7 @@ replace github.com/tech/sendico/pkg => ../../pkg require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver v1.17.6 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 ) require ( @@ -25,7 +25,7 @@ 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.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/text v0.31.0 // indirect google.golang.org/protobuf v1.36.10 // indirect diff --git a/api/fx/storage/go.sum b/api/fx/storage/go.sum index bab4975..170b262 100644 --- a/api/fx/storage/go.sum +++ b/api/fx/storage/go.sum @@ -7,8 +7,6 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= -github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM= github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= @@ -122,28 +120,26 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/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= @@ -151,8 +147,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -160,16 +154,14 @@ 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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/api/ledger/go.mod b/api/ledger/go.mod index 12767dd..9bf4afe 100644 --- a/api/ledger/go.mod +++ b/api/ledger/go.mod @@ -10,7 +10,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver v1.17.6 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 @@ -38,7 +38,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect @@ -46,7 +46,7 @@ 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.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/api/ledger/go.sum b/api/ledger/go.sum index 4ae6767..63f1b50 100644 --- a/api/ledger/go.sum +++ b/api/ledger/go.sum @@ -9,8 +9,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= -github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM= github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= @@ -117,10 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= -github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -158,32 +154,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/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= @@ -220,8 +216,6 @@ 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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= diff --git a/api/notification/go.mod b/api/notification/go.mod index 4f18fef..7d313cb 100644 --- a/api/notification/go.mod +++ b/api/notification/go.mod @@ -13,7 +13,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 github.com/xhit/go-simple-mail/v2 v2.16.0 go.mongodb.org/mongo-driver v1.17.6 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 golang.org/x/text v0.31.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -38,7 +38,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect @@ -48,7 +48,7 @@ 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.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/api/notification/go.sum b/api/notification/go.sum index acaab3e..5ae62e8 100644 --- a/api/notification/go.sum +++ b/api/notification/go.sum @@ -13,8 +13,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= -github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM= github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= @@ -123,10 +121,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= -github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -171,32 +167,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/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= @@ -233,8 +229,6 @@ 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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= diff --git a/api/notification/i18n/en.json b/api/notification/i18n/en.json index 0cc34ad..24afb54 100644 --- a/api/notification/i18n/en.json +++ b/api/notification/i18n/en.json @@ -20,6 +20,10 @@ "mail.reset-password.greeting": "Hi, {{.Name}}", "mail.reset-password.body": "It looks like you requested a new password.

If that sounds right, you can enter a new password by clicking the followinglink<\/a>.

If you have not requested a passowrd change please contact us under
{{.SupportMail}}<\/a>.", + "mail.confirmation-code.subj": "{{.ServiceName}}: your confirmation code", + "mail.confirmation-code.greeting": "Hi, {{.Name}}", + "mail.confirmation-code.body": "Use this code to complete your {{.Target}} request: {{.Code}}<\/b>. This code expires soon.", + "mail.email-verification.subj": "{{.ServiceName}}: verify your email address", "mail.email-verification.greeting": "Hi, {{.Name}}", "mail.email-verification.body": "It looks like you have changed your email address.

If that sounds right, verify your email address by clicking the following
link<\/a>.

If you have not changed your email address please contact us under
{{.SupportMail}}<\/a>.", diff --git a/api/notification/i18n/ru.json b/api/notification/i18n/ru.json index b7f28a6..0d42683 100644 --- a/api/notification/i18n/ru.json +++ b/api/notification/i18n/ru.json @@ -21,6 +21,10 @@ "mail.reset-password.greeting": "День добрый, {{.Name}}", "mail.reset-password.body": "От вашего имени пришел запрос на сброс пароля.

Если этот запрос отправили вы, то подтвердите сброс переходом по
ссылке<\/a>.

Если этот запрос отправили не вы, то дайте нам знать по адресу электронной почты
{{.SupportMail}}<\/a>", + "mail.confirmation-code.subj": "{{.ServiceName}}: ваш код подтверждения", + "mail.confirmation-code.greeting": "День добрый, {{.Name}}", + "mail.confirmation-code.body": "Используйте этот код, чтобы завершить запрос {{.Target}}: {{.Code}}<\/b>. Код скоро истекает.", + "mail.email-verification.subj": "{{.ServiceName}}: подтвердите ваш адрес электронной почты", "mail.email-verification.greeting": "День добрый, {{.Name}}", "mail.email-verification.body": "От вашего имени пришел запрос на смену адреса электронной почты.

Если этот запрос отправили вы, то подтвердите смену переходом по
ссылке <\/a>.

Если этот запрос отправили не вы, то дайте нам знать по адресу электронной почты
{{.SupportMail}}<\/a>", @@ -58,4 +62,4 @@ "___file_trailer": "" -} \ No newline at end of file +} diff --git a/api/notification/i18n/uk.json b/api/notification/i18n/uk.json index 8771068..e19651c 100644 --- a/api/notification/i18n/uk.json +++ b/api/notification/i18n/uk.json @@ -20,6 +20,10 @@ "mail.reset-password.greeting": "День добрий, {{.Name}}", "mail.reset-password.body": "Від вашого імені прийшов запит на скидання пароля.

Якщо цей запит відправили ви, то підтвердіть скидання переходом по
посилання<\/a>.
>
Якщо цей запит надіслали не ви, дайте нам знати за адресою електронної пошти
{{.SupportMail}}<\/a>", + "mail.confirmation-code.subj": "{{.ServiceName}}: ваш код підтвердження", + "mail.confirmation-code.greeting": "День добрий, {{.Name}}", + "mail.confirmation-code.body": "Використайте цей код, щоб завершити запит {{.Target}}: {{.Code}}<\/b>. Код скоро спливає.", + "mail.email-verification.subj": "{{.ServiceName}}: Перевірте свою адресу електронної пошти", "mail.email-verification.greeting": "День добрий, {{.Name}}", "mail.email-verification.body": "Від вашого імені надійшов запит на адресу електронної пошти.

Якщо цей запит надіслали, то підтвердьте зміну переходом за
адресою<\/a>.

Якщо цей запит відправили не ви, то дайте нам знати на адресу електронної пошти
{{.SupportMail}}<\/a>", diff --git a/api/notification/internal/server/notificationimp/confcode.go b/api/notification/internal/server/notificationimp/confcode.go new file mode 100644 index 0000000..bccf5ee --- /dev/null +++ b/api/notification/internal/server/notificationimp/confcode.go @@ -0,0 +1,27 @@ +package notificationimp + +import ( + "context" + "strings" + + "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" +) + +func (a *NotificationAPI) onConfirmationCode(ctx context.Context, account *model.Account, destination string, target model.ConfirmationTarget, code string) error { + builder := a.client.MailBuilder(). + AddRecipient(account.Name, strings.TrimSpace(destination)). + SetAccountID(account.ID.Hex()). + SetLocale(account.Locale). + SetTemplateID("confirmation-code"). + AddData("Name", account.Name). + AddData("Code", code). + AddData("Target", string(target)) + + 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)) + return err + } + a.logger.Info("Confirmation code email sent", zap.String("login", account.Login), zap.String("destination", destination), zap.String("target", string(target))) + return nil +} diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index 2805652..4f81929 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -9,6 +9,7 @@ import ( "github.com/tech/sendico/pkg/domainprovider" "github.com/tech/sendico/pkg/merrors" na "github.com/tech/sendico/pkg/messaging/notifications/account" + cnotifications "github.com/tech/sendico/pkg/messaging/notifications/confirmation" ni "github.com/tech/sendico/pkg/messaging/notifications/invitation" snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" "github.com/tech/sendico/pkg/mlogger" @@ -70,6 +71,11 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { return nil, err } + if err := a.Register().Consumer(cnotifications.NewConfirmationCodeProcessor(p.logger, db, p.onConfirmationCode)); err != nil { + p.logger.Error("Failed to create confirmation code handler", zap.Error(err)) + return nil, err + } + idb, err := a.DBFactory().NewInvitationsDB() if err != nil { p.logger.Error("Failed to create invitation db connection", zap.Error(err)) diff --git a/api/payments/orchestrator/go.mod b/api/payments/orchestrator/go.mod index 5973b59..f87cb3f 100644 --- a/api/payments/orchestrator/go.mod +++ b/api/payments/orchestrator/go.mod @@ -20,7 +20,7 @@ require ( github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000 github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver v1.17.6 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 @@ -46,7 +46,7 @@ require ( github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect @@ -54,7 +54,7 @@ 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.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/api/payments/orchestrator/go.sum b/api/payments/orchestrator/go.sum index 4babef9..44a7f0b 100644 --- a/api/payments/orchestrator/go.sum +++ b/api/payments/orchestrator/go.sum @@ -9,8 +9,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= -github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM= github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= @@ -117,10 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= -github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -142,10 +138,10 @@ github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5 github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY= github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -159,32 +155,32 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/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= @@ -221,8 +217,6 @@ 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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= diff --git a/api/pkg/db/confirmation/confirmation.go b/api/pkg/db/confirmation/confirmation.go new file mode 100644 index 0000000..7f5d510 --- /dev/null +++ b/api/pkg/db/confirmation/confirmation.go @@ -0,0 +1,16 @@ +package confirmation + +import ( + "context" + + "github.com/tech/sendico/pkg/db/template" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type DB interface { + template.DB[*model.ConfirmationCode] + + FindActive(ctx context.Context, accountRef primitive.ObjectID, destination string, target model.ConfirmationTarget, now int64) (*model.ConfirmationCode, error) + DeleteTuple(ctx context.Context, accountRef primitive.ObjectID, destination string, target model.ConfirmationTarget) error +} diff --git a/api/pkg/db/factory.go b/api/pkg/db/factory.go index dc8c7e8..3c37aac 100644 --- a/api/pkg/db/factory.go +++ b/api/pkg/db/factory.go @@ -3,6 +3,7 @@ package db import ( "github.com/tech/sendico/pkg/auth" "github.com/tech/sendico/pkg/db/account" + "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" @@ -17,6 +18,7 @@ import ( // Factory exposes high-level repositories used by application services. type Factory interface { NewRefreshTokensDB() (refreshtokens.DB, error) + NewConfirmationsDB() (confirmation.DB, error) NewAccountDB() (account.DB, error) NewOrganizationDB() (organization.DB, error) diff --git a/api/pkg/db/internal/mongo/confirmationdb/db.go b/api/pkg/db/internal/mongo/confirmationdb/db.go new file mode 100644 index 0000000..27cf571 --- /dev/null +++ b/api/pkg/db/internal/mongo/confirmationdb/db.go @@ -0,0 +1,67 @@ +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/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 new file mode 100644 index 0000000..c174435 --- /dev/null +++ b/api/pkg/db/internal/mongo/confirmationdb/delete.go @@ -0,0 +1,17 @@ +package confirmationdb + +import ( + "context" + + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func (db *ConfirmationDB) DeleteTuple(ctx context.Context, accountRef primitive.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 new file mode 100644 index 0000000..3be2f63 --- /dev/null +++ b/api/pkg/db/internal/mongo/confirmationdb/find.go @@ -0,0 +1,26 @@ +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/bson/primitive" +) + +func (db *ConfirmationDB) FindActive(ctx context.Context, accountRef primitive.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 6083f15..02cc9ff 100755 --- a/api/pkg/db/internal/mongo/db.go +++ b/api/pkg/db/internal/mongo/db.go @@ -7,7 +7,9 @@ import ( "github.com/mitchellh/mapstructure" "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/internal/mongo/accountdb" + "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/policiesdb" @@ -156,6 +158,10 @@ 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/repositoryimp/builderimp/pipeline_test.go b/api/pkg/db/internal/mongo/repositoryimp/builderimp/pipeline_test.go index 750b0e7..129f34a 100644 --- a/api/pkg/db/internal/mongo/repositoryimp/builderimp/pipeline_test.go +++ b/api/pkg/db/internal/mongo/repositoryimp/builderimp/pipeline_test.go @@ -3,9 +3,9 @@ package builderimp import ( "testing" + "github.com/stretchr/testify/assert" "github.com/tech/sendico/pkg/db/repository/builder" "github.com/tech/sendico/pkg/mservice" - "github.com/stretchr/testify/assert" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -45,7 +45,7 @@ func TestPipelineImp_Lookup(t *testing.T) { mockForeignField := &MockField{build: "foreignField"} mockAsField := &MockField{build: "asField"} - result := pipeline.Lookup(mservice.Projects, mockLocalField, mockForeignField, mockAsField) + result := pipeline.Lookup(mservice.Site, mockLocalField, mockForeignField, mockAsField) // Should return self for chaining assert.Same(t, pipeline, result) @@ -54,7 +54,7 @@ func TestPipelineImp_Lookup(t *testing.T) { assert.Len(t, built, 1) expected := bson.D{{Key: string(builder.Lookup), Value: bson.D{ - {Key: string(builder.MKFrom), Value: mservice.Projects}, + {Key: string(builder.MKFrom), Value: mservice.Site}, {Key: string(builder.MKLocalField), Value: "localField"}, {Key: string(builder.MKForeignField), Value: "foreignField"}, {Key: string(builder.MKAs), Value: "asField"}, @@ -70,7 +70,7 @@ func TestPipelineImp_LookupWithPipeline_WithoutLet(t *testing.T) { } mockAsField := &MockField{build: "asField"} - result := pipeline.LookupWithPipeline(mservice.Tasks, mockNestedPipeline, mockAsField, nil) + result := pipeline.LookupWithPipeline(mservice.Site, mockNestedPipeline, mockAsField, nil) // Should return self for chaining assert.Same(t, pipeline, result) @@ -79,7 +79,7 @@ func TestPipelineImp_LookupWithPipeline_WithoutLet(t *testing.T) { assert.Len(t, built, 1) expected := bson.D{{Key: string(builder.Lookup), Value: bson.D{ - {Key: string(builder.MKFrom), Value: mservice.Tasks}, + {Key: string(builder.MKFrom), Value: mservice.Site}, {Key: string(builder.MKPipeline), Value: mockNestedPipeline.build}, {Key: string(builder.MKAs), Value: "asField"}, }}} @@ -99,7 +99,7 @@ func TestPipelineImp_LookupWithPipeline_WithLet(t *testing.T) { "projRef": mockLetField, } - result := pipeline.LookupWithPipeline(mservice.Tasks, mockNestedPipeline, mockAsField, &letVars) + result := pipeline.LookupWithPipeline(mservice.Site, mockNestedPipeline, mockAsField, &letVars) // Should return self for chaining assert.Same(t, pipeline, result) @@ -108,7 +108,7 @@ func TestPipelineImp_LookupWithPipeline_WithLet(t *testing.T) { assert.Len(t, built, 1) expected := bson.D{{Key: string(builder.Lookup), Value: bson.D{ - {Key: string(builder.MKFrom), Value: mservice.Tasks}, + {Key: string(builder.MKFrom), Value: mservice.Site}, {Key: string(builder.MKPipeline), Value: mockNestedPipeline.build}, {Key: string(builder.MKAs), Value: "asField"}, {Key: string(builder.MKLet), Value: bson.D{{Key: "projRef", Value: "$_id"}}}, @@ -126,14 +126,14 @@ func TestPipelineImp_LookupWithPipeline_WithEmptyLet(t *testing.T) { emptyLetVars := map[string]builder.Field{} - pipeline.LookupWithPipeline(mservice.Tasks, mockNestedPipeline, mockAsField, &emptyLetVars) + pipeline.LookupWithPipeline(mservice.Site, mockNestedPipeline, mockAsField, &emptyLetVars) built := pipeline.Build() assert.Len(t, built, 1) // Should not include let field when empty expected := bson.D{{Key: string(builder.Lookup), Value: bson.D{ - {Key: string(builder.MKFrom), Value: mservice.Tasks}, + {Key: string(builder.MKFrom), Value: mservice.Site}, {Key: string(builder.MKPipeline), Value: mockNestedPipeline.build}, {Key: string(builder.MKAs), Value: "asField"}, }}} diff --git a/api/pkg/go.mod b/api/pkg/go.mod index e96d7fa..be42262 100644 --- a/api/pkg/go.mod +++ b/api/pkg/go.mod @@ -15,8 +15,8 @@ require ( github.com/testcontainers/testcontainers-go v0.33.0 github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 go.mongodb.org/mongo-driver v1.17.6 - go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.44.0 + go.uber.org/zap v1.27.1 + golang.org/x/crypto v0.45.0 google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 ) @@ -67,7 +67,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect diff --git a/api/pkg/go.sum b/api/pkg/go.sum index d3150a6..6c0ebad 100644 --- a/api/pkg/go.sum +++ b/api/pkg/go.sum @@ -11,8 +11,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= -github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM= github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= @@ -128,14 +126,12 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= -github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -174,27 +170,23 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= @@ -202,16 +194,16 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/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= @@ -275,12 +267,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-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc= -google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= diff --git a/api/pkg/messaging/internal/notifications/confirmation/notification.go b/api/pkg/messaging/internal/notifications/confirmation/notification.go new file mode 100644 index 0000000..c45f5ae --- /dev/null +++ b/api/pkg/messaging/internal/notifications/confirmation/notification.go @@ -0,0 +1,47 @@ +package notifications + +import ( + "encoding/json" + + messaging "github.com/tech/sendico/pkg/messaging/envelope" + "github.com/tech/sendico/pkg/model" + nm "github.com/tech/sendico/pkg/model/notification" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type confirmationCodePayload struct { + AccountRef string `json:"accountRef"` + Destination string `json:"destination"` + Target string `json:"target"` + Code string `json:"code"` +} + +type ConfirmationCodeNotification struct { + messaging.Envelope + payload confirmationCodePayload +} + +func (ccn *ConfirmationCodeNotification) Serialize() ([]byte, error) { + data, err := json.Marshal(ccn.payload) + if err != nil { + return nil, err + } + return ccn.Envelope.Wrap(data) +} + +func newConfirmationEvent(action nm.NotificationAction) model.NotificationEvent { + return model.NewNotification(mservice.Confirmations, action) +} + +func NewConfirmationCodeEnvelope(sender string, accountRef primitive.ObjectID, destination string, target model.ConfirmationTarget, code string) messaging.Envelope { + return &ConfirmationCodeNotification{ + Envelope: messaging.CreateEnvelope(sender, newConfirmationEvent(nm.NAPending)), + payload: confirmationCodePayload{ + AccountRef: accountRef.Hex(), + Destination: destination, + Target: string(target), + Code: code, + }, + } +} diff --git a/api/pkg/messaging/internal/notifications/confirmation/processor.go b/api/pkg/messaging/internal/notifications/confirmation/processor.go new file mode 100644 index 0000000..c085712 --- /dev/null +++ b/api/pkg/messaging/internal/notifications/confirmation/processor.go @@ -0,0 +1,70 @@ +package notifications + +import ( + "context" + "encoding/json" + + "github.com/tech/sendico/pkg/db/account" + "github.com/tech/sendico/pkg/merrors" + me "github.com/tech/sendico/pkg/messaging/envelope" + ch "github.com/tech/sendico/pkg/messaging/notifications/confirmation/handler" + np "github.com/tech/sendico/pkg/messaging/notifications/processor" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + nm "github.com/tech/sendico/pkg/model/notification" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +type ConfirmationCodeProcessor struct { + logger mlogger.Logger + db account.DB + handler ch.ConfirmationCodeHandler + event model.NotificationEvent +} + +func (ccp *ConfirmationCodeProcessor) Process(ctx context.Context, envelope me.Envelope) error { + var msg confirmationCodePayload + if err := json.Unmarshal(envelope.GetData(), &msg); err != nil { + ccp.logger.Warn("Failed to unmarshal confirmation code envelope", zap.Error(err), zap.String("topic", ccp.event.ToString())) + return err + } + + accountRef, err := primitive.ObjectIDFromHex(msg.AccountRef) + if err != nil { + ccp.logger.Warn("Failed to restore account id from envelope", zap.Error(err), zap.String("topic", ccp.event.ToString()), zap.String("account_ref", msg.AccountRef)) + return err + } + + var account model.Account + if err := ccp.db.Get(ctx, accountRef, &account); err != nil { + ccp.logger.Warn("Failed to fetch account for confirmation code", zap.Error(err), zap.String("topic", ccp.event.ToString()), zap.String("account_ref", msg.AccountRef)) + return err + } + + target := model.ConfirmationTarget(msg.Target) + if target != model.ConfirmationTargetLogin && target != model.ConfirmationTargetPayout { + return merrors.InvalidArgument("invalid confirmation target", "target") + } + if msg.Code == "" { + return merrors.InvalidArgument("empty confirmation code", "code") + } + if msg.Destination == "" { + return merrors.InvalidArgument("empty destination", "destination") + } + + return ccp.handler(ctx, &account, msg.Destination, target, msg.Code) +} + +func (ccp *ConfirmationCodeProcessor) GetSubject() model.NotificationEvent { + return ccp.event +} + +func NewConfirmationCodeProcessor(logger mlogger.Logger, db account.DB, handler ch.ConfirmationCodeHandler) np.EnvelopeProcessor { + return &ConfirmationCodeProcessor{ + logger: logger.Named("confirmation_code_processor"), + db: db, + handler: handler, + event: newConfirmationEvent(nm.NAPending), + } +} diff --git a/api/pkg/messaging/notifications/confirmation/confirmation.go b/api/pkg/messaging/notifications/confirmation/confirmation.go new file mode 100644 index 0000000..23ec6db --- /dev/null +++ b/api/pkg/messaging/notifications/confirmation/confirmation.go @@ -0,0 +1,20 @@ +package notifications + +import ( + "github.com/tech/sendico/pkg/db/account" + messaging "github.com/tech/sendico/pkg/messaging/envelope" + cinternal "github.com/tech/sendico/pkg/messaging/internal/notifications/confirmation" + ch "github.com/tech/sendico/pkg/messaging/notifications/confirmation/handler" + np "github.com/tech/sendico/pkg/messaging/notifications/processor" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func Code(sender string, accountRef primitive.ObjectID, destination string, target model.ConfirmationTarget, code string) messaging.Envelope { + return cinternal.NewConfirmationCodeEnvelope(sender, accountRef, destination, target, code) +} + +func NewConfirmationCodeProcessor(logger mlogger.Logger, db account.DB, handler ch.ConfirmationCodeHandler) np.EnvelopeProcessor { + return cinternal.NewConfirmationCodeProcessor(logger, db, handler) +} diff --git a/api/pkg/messaging/notifications/confirmation/handler/interface.go b/api/pkg/messaging/notifications/confirmation/handler/interface.go new file mode 100644 index 0000000..a7c2477 --- /dev/null +++ b/api/pkg/messaging/notifications/confirmation/handler/interface.go @@ -0,0 +1,9 @@ +package handler + +import ( + "context" + + "github.com/tech/sendico/pkg/model" +) + +type ConfirmationCodeHandler = func(context.Context, *model.Account, string, model.ConfirmationTarget, string) error diff --git a/api/pkg/model/confirmation.go b/api/pkg/model/confirmation.go new file mode 100644 index 0000000..1fdcfc6 --- /dev/null +++ b/api/pkg/model/confirmation.go @@ -0,0 +1,43 @@ +package model + +import ( + "time" + + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ConfirmationTarget string + +const ( + ConfirmationTargetLogin ConfirmationTarget = "login" + ConfirmationTargetPayout ConfirmationTarget = "payout" +) + +// ConfirmationCode stores verification codes for operations like login or payouts. +type ConfirmationCode struct { + AccountBoundBase `bson:",inline" json:",inline"` + + Destination string `bson:"destination" json:"destination"` + Target ConfirmationTarget `bson:"target" json:"target"` + CodeHash []byte `bson:"codeHash" json:"-"` + Salt []byte `bson:"salt" json:"-"` + ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"` + Attempts int `bson:"attempts" json:"attempts"` + MaxAttempts int `bson:"maxAttempts" json:"maxAttempts"` + ResendCount int `bson:"resendCount" json:"resendCount"` + ResendLimit int `bson:"resendLimit" json:"resendLimit"` + CooldownUntil time.Time `bson:"cooldownUntil" json:"cooldownUntil"` + Used bool `bson:"used" json:"used"` +} + +func (c *ConfirmationCode) Collection() string { + return mservice.Confirmations +} + +func NewConfirmationCode(accountRef primitive.ObjectID) *ConfirmationCode { + cc := &ConfirmationCode{} + cc.SetID(primitive.NewObjectID()) + cc.AccountRef = &accountRef + return cc +} diff --git a/api/pkg/mservice/services.go b/api/pkg/mservice/services.go index be4e30f..51b3b47 100644 --- a/api/pkg/mservice/services.go +++ b/api/pkg/mservice/services.go @@ -6,12 +6,11 @@ type Type = string const ( Accounts Type = "accounts" // Represents user accounts in the system + Confirmations Type = "confirmations" // Represents confirmation code flows Amplitude Type = "amplitude" // Represents analytics integration with Amplitude Site Type = "site" // Represents public site endpoints - Automations Type = "automation" // Represents automation workflows Changes Type = "changes" // Tracks changes made to resources Clients Type = "clients" // Represents client information - Comments Type = "comments" // Represents comments on tasks or other resources ChainGateway Type = "chain_gateway" // Represents chain gateway microservice FXOracle Type = "fx_oracle" // Represents FX oracle microservice FeePlans Type = "fee_plans" // Represents fee plans microservice @@ -37,37 +36,22 @@ const ( Permissions Type = "permissions" // Represents permissiosns service Policies Type = "policies" // Represents access control policies PolicyAssignements Type = "policy_assignments" // Represents policy assignments database - Priorities Type = "priorities" // Represents object properties - PriorityGroups Type = "priority_groups" // Represents task or project priorities - Projects Type = "projects" // Represents projects managed in the system - PropertyBindings Type = "property_bindings" // Represents properties bindings of resources - PropertySchemas Type = "property_schemas" // Represents properties or attributes of resources - Properties Type = "properties" // Represents property values of the propertites of specific objects - Reactions Type = "reactions" // Represents comment reactions RefreshTokens Type = "refresh_tokens" // Represents refresh tokens for authentication Roles Type = "roles" // Represents roles in access control - Statuses Type = "statuses" // Represents statuses of tasks or projects - StatusGroups Type = "status_groups" // Represents status groups - Steps Type = "steps" // Represents steps in workflows or processes Storage Type = "storage" // Represents statuses of tasks or projects - Tags Type = "tags" // Represents tags managed in the system - Tasks Type = "tasks" // Represents tasks managed in the system - Teams Type = "teams" // Represents teams managed in the system Tenants Type = "tenants" // Represents tenants managed in the system Workflows Type = "workflows" // Represents workflows for tasks or projects - Workspaces Type = "workspaces" // Represents workspaces containing projects and teams ) func StringToSType(s string) (Type, error) { switch Type(s) { - case Accounts, Amplitude, Site, Automations, Changes, Clients, Comments, ChainGateway, ChainWallets, ChainWalletBalances, + case Accounts, Confirmations, Amplitude, Site, Changes, Clients, ChainGateway, ChainWallets, ChainWalletBalances, ChainTransfers, ChainDeposits, FXOracle, FeePlans, FilterProjects, Invitations, Invoices, Logo, Ledger, LedgerAccounts, LedgerBalances, LedgerEntries, LedgerOutbox, LedgerParties, LedgerPlines, Notifications, - Organizations, Payments, PaymentOrchestrator, Permissions, Policies, PolicyAssignements, Priorities, - PriorityGroups, Projects, PropertyBindings, PropertySchemas, Properties, Reactions, RefreshTokens, Roles, - Statuses, StatusGroups, Steps, Storage, Tags, Tasks, Teams, Tenants, Workflows, Workspaces: + Organizations, Payments, PaymentOrchestrator, Permissions, Policies, PolicyAssignements, + RefreshTokens, Roles, Storage, Tenants, Workflows: return Type(s), nil default: - return "", merrors.DataConflict("invalid service type: " + s) + return "", merrors.InvalidArgument("invalid service type", s) } } diff --git a/api/server/go.mod b/api/server/go.mod index 4618b51..27cea8c 100644 --- a/api/server/go.mod +++ b/api/server/go.mod @@ -7,10 +7,10 @@ replace github.com/tech/sendico/pkg => ../pkg replace github.com/tech/sendico/chain/gateway => ../chain/gateway require ( - github.com/aws/aws-sdk-go-v2 v1.39.6 - github.com/aws/aws-sdk-go-v2/config v1.31.20 - github.com/aws/aws-sdk-go-v2/credentials v1.18.24 - github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2 + github.com/aws/aws-sdk-go-v2 v1.40.0 + github.com/aws/aws-sdk-go-v2/config v1.32.0 + github.com/aws/aws-sdk-go-v2/credentials v1.19.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0 github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/cors v1.2.2 github.com/go-chi/jwtauth/v5 v5.3.3 @@ -23,7 +23,7 @@ require ( github.com/testcontainers/testcontainers-go v0.33.0 github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 go.mongodb.org/mongo-driver v1.17.6 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 golang.org/x/net v0.47.0 gopkg.in/yaml.v3 v3.0.1 moul.io/chizap v1.0.3 @@ -40,18 +40,19 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 // indirect github.com/aws/smithy-go v1.23.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect @@ -104,7 +105,7 @@ require ( github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/shirou/gopsutil/v3 v3.24.5 // indirect @@ -126,7 +127,7 @@ require ( go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect diff --git a/api/server/go.sum b/api/server/go.sum index 6c9922f..e2d491e 100644 --- a/api/server/go.sum +++ b/api/server/go.sum @@ -6,40 +6,42 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= -github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= +github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= +github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y= -github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc= -github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0= -github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg= -github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2/config v1.32.0 h1:T5WWJYnam9SzBLbsVYDu2HscLDe+GU1AUJtfcDAc/vA= +github.com/aws/aws-sdk-go-v2/config v1.32.0/go.mod h1:pSRm/+D3TxBixGMXlgtX4+MPO9VNtEEtiFmNpxksoxw= +github.com/aws/aws-sdk-go-v2/credentials v1.19.0 h1:7zm+ez+qEqLaNsCSRaistkvJRJv8sByDOVuCnyHbP7M= +github.com/aws/aws-sdk-go-v2/credentials v1.19.0/go.mod h1:pHKPblrT7hqFGkNLxqoS3FlGoPrQg4hMIa+4asZzBfs= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2 h1:DhdbtDl4FdNlj31+xiRXANxEE+eC7n8JQz+/ilwQ8Uc= -github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= -github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0= -github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w= +github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0 h1:8FshVvnV2sr9kOSAbOnc/vwVmmAwMjOedKH6JW2ddPM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 h1:U//SlnkE1wOQiIImxzdY5PXat4Wq+8rlfVEw4Y7J8as= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.4/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8 h1:MvlNs/f+9eM0mOjD9JzBUbf5jghyTk3p+O9yHMXX94Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 h1:GdGmKtG+/Krag7VfyOXV17xjTCz0i9NT+JnqLTOI5nA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.1/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso= github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -196,8 +198,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= -github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -278,16 +280,16 @@ go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95a go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/api/server/interface/api/register.go b/api/server/interface/api/register.go index 4a46514..63683bd 100644 --- a/api/server/interface/api/register.go +++ b/api/server/interface/api/register.go @@ -11,6 +11,7 @@ import ( type Register interface { Handler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) + PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) WSHandler(messageType string, handler ws.HandlerFunc) Messaging() messaging.Register diff --git a/api/server/interface/api/sresponse/login_pending.go b/api/server/interface/api/sresponse/login_pending.go new file mode 100644 index 0000000..5ef2d4a --- /dev/null +++ b/api/server/interface/api/sresponse/login_pending.go @@ -0,0 +1,31 @@ +package sresponse + +import ( + "net/http" + + "github.com/tech/sendico/pkg/api/http/response" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" +) + +type pendingLoginResponse struct { + Account accountResponse `json:"account"` + PendingToken TokenData `json:"pendingToken"` + Destination string `json:"destination"` + TTLSeconds int `json:"ttlSeconds"` +} + +func LoginPending(logger mlogger.Logger, account *model.Account, pendingToken *TokenData, destination string, ttlSeconds int) http.HandlerFunc { + return response.Accepted( + logger, + &pendingLoginResponse{ + Account: accountResponse{ + Account: *_createAccount(account, false), + authResponse: authResponse{}, + }, + PendingToken: *pendingToken, + Destination: destination, + TTLSeconds: ttlSeconds, + }, + ) +} diff --git a/api/server/interface/api/sresponse/response.go b/api/server/interface/api/sresponse/response.go index 04a6cf8..8bd7211 100644 --- a/api/server/interface/api/sresponse/response.go +++ b/api/server/interface/api/sresponse/response.go @@ -4,9 +4,11 @@ import ( "net/http" "github.com/tech/sendico/pkg/model" + emodel "github.com/tech/sendico/server/interface/model" ) type ( - HandlerFunc = func(r *http.Request) http.HandlerFunc - AccountHandlerFunc = func(r *http.Request, account *model.Account, accessToken *TokenData) http.HandlerFunc + HandlerFunc = func(r *http.Request) http.HandlerFunc + AccountHandlerFunc = func(r *http.Request, account *model.Account, accessToken *TokenData) http.HandlerFunc + PendingAccountHandlerFunc = func(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc ) diff --git a/api/server/interface/model/token.go b/api/server/interface/model/token.go index 7f51ba6..9390ab6 100644 --- a/api/server/interface/model/token.go +++ b/api/server/interface/model/token.go @@ -17,6 +17,7 @@ type AccountToken struct { Name string Locale string Expiration time.Time + Pending bool } func createAccountToken(a *model.Account, expiration int) AccountToken { @@ -26,6 +27,7 @@ func createAccountToken(a *model.Account, expiration int) AccountToken { Name: a.Name, Locale: a.Locale, Expiration: time.Now().Add(mduration.Param2Duration(expiration, time.Hour)), + Pending: false, } } @@ -44,6 +46,7 @@ const ( paramNameLocale = "locale" paramNameLogin = "login" paramNameExpiration = "exp" + paramNamePending = "pending" ) func Claims2Token(claims middleware.MapClaims) (*AccountToken, error) { @@ -65,6 +68,11 @@ func Claims2Token(claims middleware.MapClaims) (*AccountToken, error) { if at.Locale, err = getTokenParam(claims, paramNameLocale); err != nil { return nil, err } + if pending, ok := claims[paramNamePending]; ok { + if pbool, ok := pending.(bool); ok { + at.Pending = pbool + } + } if expValue, ok := claims[paramNameExpiration]; ok { switch exp := expValue.(type) { case time.Time: @@ -90,5 +98,20 @@ func Account2Claims(a *model.Account, expiration int) middleware.MapClaims { paramNameName: t.Name, paramNameLocale: t.Locale, paramNameExpiration: int64(t.Expiration.Unix()), + paramNamePending: t.Pending, + } +} + +func PendingAccount2Claims(a *model.Account, expirationMinutes int) middleware.MapClaims { + t := createAccountToken(a, expirationMinutes/60) + t.Expiration = time.Now().Add(time.Duration(expirationMinutes) * time.Minute) + t.Pending = true + return middleware.MapClaims{ + paramNameID: t.AccountRef.Hex(), + paramNameLogin: t.Login, + paramNameName: t.Name, + paramNameLocale: t.Locale, + paramNameExpiration: t.Expiration.Unix(), + paramNamePending: t.Pending, } } diff --git a/api/server/interface/services/confirmation/confirmation.go b/api/server/interface/services/confirmation/confirmation.go new file mode 100644 index 0000000..39cc975 --- /dev/null +++ b/api/server/interface/services/confirmation/confirmation.go @@ -0,0 +1,11 @@ +package confirmation + +import ( + "github.com/tech/sendico/pkg/mservice" + "github.com/tech/sendico/server/interface/api" + "github.com/tech/sendico/server/internal/server/confirmationimp" +) + +func Create(a api.API) (mservice.MicroService, error) { + return confirmationimp.CreateAPI(a) +} diff --git a/api/server/internal/api/api.go b/api/server/internal/api/api.go index 822e514..d37506f 100644 --- a/api/server/internal/api/api.go +++ b/api/server/internal/api/api.go @@ -12,6 +12,7 @@ import ( "github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/server/interface/api" "github.com/tech/sendico/server/interface/services/account" + "github.com/tech/sendico/server/interface/services/confirmation" "github.com/tech/sendico/server/interface/services/invitation" "github.com/tech/sendico/server/interface/services/logo" "github.com/tech/sendico/server/interface/services/organization" @@ -76,6 +77,7 @@ func (a *APIImp) installServices() error { srvf := make([]api.MicroServiceFactoryT, 0) srvf = append(srvf, account.Create) + srvf = append(srvf, confirmation.Create) srvf = append(srvf, organization.Create) srvf = append(srvf, invitation.Create) srvf = append(srvf, logo.Create) diff --git a/api/server/internal/api/middleware.go b/api/server/internal/api/middleware.go index 800c800..c024582 100644 --- a/api/server/internal/api/middleware.go +++ b/api/server/internal/api/middleware.go @@ -45,6 +45,10 @@ func (mw *Middleware) AccountHandler(service mservice.Type, endpoint string, met mw.epdispatcher.AccountHandler(service, endpoint, method, handler) } +func (mw *Middleware) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) { + mw.epdispatcher.PendingAccountHandler(service, endpoint, method, handler) +} + func (mw *Middleware) WSHandler(messageType string, handler wsh.HandlerFunc) { mw.wshandler.InstallHandler(messageType, handler) } @@ -133,7 +137,13 @@ func CreateMiddleware(logger mlogger.Logger, db db.Factory, enforcer auth.Enforc return nil, err } - p.epdispatcher = routers.NewDispatcher(p.logger, p.router, adb, rtdb, enforcer, config) + cdb, err := db.NewConfirmationsDB() + if err != nil { + p.logger.Error("Failed to create confirmations database", zap.Error(err)) + return nil, err + } + + p.epdispatcher = routers.NewDispatcher(p.logger, p.router, adb, cdb, rtdb, enforcer, config) p.wshandler = ws.NewRouter(p.logger, p.router, &config.WebSocket, p.apiEndpoint) return p, nil } diff --git a/api/server/internal/api/routers/authorized/handler.go b/api/server/internal/api/routers/authorized/handler.go index b0a3580..29118ce 100644 --- a/api/server/internal/api/routers/authorized/handler.go +++ b/api/server/internal/api/routers/authorized/handler.go @@ -37,6 +37,9 @@ func (ar *AuthorizedRouter) tokenHandler(service mservice.Type, endpoint string, func (ar *AuthorizedRouter) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) { hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc { + if t.Pending { + return response.Forbidden(ar.logger, ar.service, "confirmation_required", "pending token requires confirmation") + } var a model.Account if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil { if errors.Is(err, merrors.ErrNoData) { @@ -54,3 +57,18 @@ func (ar *AuthorizedRouter) AccountHandler(service mservice.Type, endpoint strin } ar.tokenHandler(service, endpoint, method, hndlr) } + +func (ar *AuthorizedRouter) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) { + hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc { + var a model.Account + if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil { + if errors.Is(err, merrors.ErrNoData) { + ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.ObjRef("account_ref", t.AccountRef)) + return response.NotFound(ar.logger, ar.service, err.Error()) + } + return response.Internal(ar.logger, ar.service, err) + } + return handler(r, &a, t) + } + ar.tokenHandler(service, endpoint, method, hndlr) +} diff --git a/api/server/internal/api/routers/dispatcher.go b/api/server/internal/api/routers/dispatcher.go index 6f6ebb0..3ec2929 100644 --- a/api/server/internal/api/routers/dispatcher.go +++ b/api/server/internal/api/routers/dispatcher.go @@ -8,6 +8,7 @@ import ( api "github.com/tech/sendico/pkg/api/http" "github.com/tech/sendico/pkg/auth" "github.com/tech/sendico/pkg/db/account" + "github.com/tech/sendico/pkg/db/confirmation" "github.com/tech/sendico/pkg/db/refreshtokens" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mservice" @@ -31,7 +32,11 @@ func (d *Dispatcher) AccountHandler(service mservice.Type, endpoint string, meth d.protected.AccountHandler(service, endpoint, method, handler) } -func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, rtdb refreshtokens.DB, enforcer auth.Enforcer, config *middleware.Config) *Dispatcher { +func (d *Dispatcher) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) { + d.protected.PendingAccountHandler(service, endpoint, method, handler) +} + +func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, cdb confirmation.DB, rtdb refreshtokens.DB, enforcer auth.Enforcer, config *middleware.Config) *Dispatcher { d := &Dispatcher{ logger: logger.Named("api_dispatcher"), } @@ -40,7 +45,7 @@ func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, rtdb endpoint := os.Getenv(config.EndPointEnv) signature := middleware.SignatureConf(config) router.Group(func(r chi.Router) { - d.public = rpublic.NewRouter(d.logger, endpoint, db, rtdb, r, &config.Token, &signature) + d.public = rpublic.NewRouter(d.logger, endpoint, db, cdb, rtdb, r, &config.Token, &signature) }) router.Group(func(r chi.Router) { d.protected = rauthorized.NewRouter(d.logger, endpoint, r, db, enforcer, &config.Token, &signature) diff --git a/api/server/internal/api/routers/endpoint/token.go b/api/server/internal/api/routers/endpoint/token.go index bb01eb2..be85a2a 100644 --- a/api/server/internal/api/routers/endpoint/token.go +++ b/api/server/internal/api/routers/endpoint/token.go @@ -18,3 +18,13 @@ func (er *HttpEndpointRouter) CreateAccessToken(user *model.Account) (sresponse. } return token, err } + +func (er *HttpEndpointRouter) CreatePendingToken(user *model.Account, ttlMinutes int) (sresponse.TokenData, error) { + ja := jwtauth.New(er.signature.Algorithm, er.signature.PrivateKey, er.signature.PublicKey) + _, res, err := ja.Encode(emodel.PendingAccount2Claims(user, ttlMinutes)) + token := sresponse.TokenData{ + Token: res, + Expiration: time.Now().Add(time.Duration(ttlMinutes) * time.Minute), + } + return token, err +} diff --git a/api/server/internal/api/routers/public/login.go b/api/server/internal/api/routers/public/login.go index b668b9e..c8c90c1 100644 --- a/api/server/internal/api/routers/public/login.go +++ b/api/server/internal/api/routers/public/login.go @@ -6,14 +6,20 @@ import ( "errors" "net/http" "strings" + "time" "github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/server/interface/api/srequest" + "github.com/tech/sendico/server/interface/api/sresponse" + "github.com/tech/sendico/server/internal/server/confirmationimp" "go.uber.org/zap" ) +const pendingLoginTTLMinutes = 10 + func (pr *PublicRouter) logUserIn(ctx context.Context, r *http.Request, req *srequest.Login) http.HandlerFunc { // Get the account database entry trimmedLogin := strings.TrimSpace(req.Login) @@ -35,13 +41,23 @@ func (pr *PublicRouter) logUserIn(ctx context.Context, r *http.Request, req *sre return response.Unauthorized(pr.logger, pr.service, "password does not match") } - accessToken, err := pr.imp.CreateAccessToken(account) + pendingToken, err := pr.imp.CreatePendingToken(account, pendingLoginTTLMinutes) if err != nil { - pr.logger.Warn("Failed to generate access token", zap.Error(err)) + pr.logger.Warn("Failed to generate pending token", zap.Error(err)) return response.Internal(pr.logger, pr.service, err) } - return pr.refreshAndRespondLogin(ctx, r, &req.SessionIdentifier, account, &accessToken) + cfg := confirmationimp.DefaultConfig() + _, rec, err := pr.cstore.Create(ctx, account.ID, account.Login, model.ConfirmationTargetLogin, cfg, pr.generateCode) + if err != nil { + pr.logger.Warn("Failed to create login confirmation code", zap.Error(err)) + return response.Internal(pr.logger, pr.service, err) + } + pr.logger.Info("Login confirmation code issued", + zap.String("destination", pr.maskEmail(account.Login)), + zap.String("account", account.Login)) + + return sresponse.LoginPending(pr.logger, account, &pendingToken, pr.maskEmail(account.Login), int(time.Until(rec.ExpiresAt).Seconds())) } func (a *PublicRouter) login(r *http.Request) http.HandlerFunc { diff --git a/api/server/internal/api/routers/public/respond.go b/api/server/internal/api/routers/public/respond.go index 002c51f..71dff09 100644 --- a/api/server/internal/api/routers/public/respond.go +++ b/api/server/internal/api/routers/public/respond.go @@ -2,59 +2,16 @@ package routers import ( "context" - "crypto/rand" - "encoding/base64" - "io" "net/http" - "time" "github.com/tech/sendico/pkg/api/http/response" - "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mutil/mzap" "github.com/tech/sendico/server/interface/api/sresponse" + rtokens "github.com/tech/sendico/server/internal/api/routers/tokens" "go.uber.org/zap" ) -func generateRefreshTokenData(length int) (string, error) { - randomBytes := make([]byte, length) - if _, err := io.ReadFull(rand.Reader, randomBytes); err != nil { - return "", merrors.Internal("failed to generate secure random bytes: " + err.Error()) - } - - return base64.URLEncoding.EncodeToString(randomBytes), nil -} - -func (er *PublicRouter) prepareRefreshToken(ctx context.Context, r *http.Request, session *model.SessionIdentifier, account *model.Account) (*model.RefreshToken, error) { - refreshToken, err := generateRefreshTokenData(er.config.Length) - if err != nil { - er.logger.Warn("Failed to generate refresh token", zap.Error(err), mzap.StorableRef(account)) - return nil, err - } - - token := &model.RefreshToken{ - AccountBoundBase: model.AccountBoundBase{ - AccountRef: account.GetID(), - }, - ClientRefreshToken: model.ClientRefreshToken{ - SessionIdentifier: *session, - RefreshToken: refreshToken, - }, - ExpiresAt: time.Now().Add(time.Duration(er.config.Expiration.Refresh) * time.Hour), - IsRevoked: false, - UserAgent: r.UserAgent(), - IPAddress: r.RemoteAddr, - } - - if err = er.rtdb.Create(ctx, token); err != nil { - er.logger.Warn("Failed to store a refresh token", zap.Error(err), mzap.StorableRef(account), - zap.String("client_id", token.ClientID), zap.String("device_id", token.DeviceID)) - return nil, err - } - - return token, nil -} - func (pr *PublicRouter) refreshAndRespondLogin( ctx context.Context, r *http.Request, @@ -62,7 +19,16 @@ func (pr *PublicRouter) refreshAndRespondLogin( account *model.Account, accessToken *sresponse.TokenData, ) http.HandlerFunc { - refreshToken, err := pr.prepareRefreshToken(ctx, r, session, account) + refreshToken, err := rtokens.PrepareRefreshToken( + ctx, + r, + pr.rtdb, + pr.config.Length, + pr.config.Expiration.Refresh, + session, + account, + pr.logger, + ) if err != nil { pr.logger.Warn("Failed to create refresh token", zap.Error(err), mzap.StorableRef(account), zap.String("client_id", session.ClientID), zap.String("device_id", session.DeviceID)) diff --git a/api/server/internal/api/routers/public/router.go b/api/server/internal/api/routers/public/router.go index a2fcae3..2d5ab0f 100644 --- a/api/server/internal/api/routers/public/router.go +++ b/api/server/internal/api/routers/public/router.go @@ -1,20 +1,27 @@ package routers import ( + "crypto/rand" + "strings" + "github.com/go-chi/chi/v5" api "github.com/tech/sendico/pkg/api/http" "github.com/tech/sendico/pkg/db/account" + "github.com/tech/sendico/pkg/db/confirmation" "github.com/tech/sendico/pkg/db/refreshtokens" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/server/interface/api/sresponse" "github.com/tech/sendico/server/interface/middleware" re "github.com/tech/sendico/server/internal/api/routers/endpoint" + "github.com/tech/sendico/server/internal/server/confirmationimp" ) type PublicRouter struct { logger mlogger.Logger db account.DB + cdb confirmation.DB + cstore *confirmationimp.ConfirmationStore imp *re.HttpEndpointRouter rtdb refreshtokens.DB config middleware.TokenConfig @@ -26,11 +33,39 @@ func (pr *PublicRouter) InstallHandler(service mservice.Type, endpoint string, m pr.imp.InstallHandler(service, endpoint, method, handler) } -func NewRouter(logger mlogger.Logger, apiEndpoint string, db account.DB, rtdb refreshtokens.DB, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *PublicRouter { +func (pr *PublicRouter) generateCode() (string, error) { + const digits = "0123456789" + b := make([]byte, confirmationimp.DefaultConfig().CodeLength) + if _, err := rand.Read(b); err != nil { + return "", err + } + for i := range b { + b[i] = digits[int(b[i])%len(digits)] + } + return string(b), nil +} + +func (pr *PublicRouter) maskEmail(email string) string { + parts := strings.Split(email, "@") + if len(parts) != 2 { + return email + } + local := parts[0] + if len(local) > 2 { + local = local[:1] + "***" + local[len(local)-1:] + } else { + local = local[:1] + "***" + } + return local + "@" + parts[1] +} + +func NewRouter(logger mlogger.Logger, apiEndpoint string, db account.DB, cdb confirmation.DB, rtdb refreshtokens.DB, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *PublicRouter { l := logger.Named("public") hr := PublicRouter{ logger: l, db: db, + cdb: cdb, + cstore: confirmationimp.NewStore(cdb), rtdb: rtdb, config: *config, signature: *signature, diff --git a/api/server/internal/api/routers/router.go b/api/server/internal/api/routers/router.go index ef95937..90ea96f 100644 --- a/api/server/internal/api/routers/router.go +++ b/api/server/internal/api/routers/router.go @@ -12,4 +12,5 @@ type APIRouter interface { type ProtectedAPIRouter interface { AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) + PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) } diff --git a/api/server/internal/api/routers/tokens/tokens.go b/api/server/internal/api/routers/tokens/tokens.go new file mode 100644 index 0000000..e768532 --- /dev/null +++ b/api/server/internal/api/routers/tokens/tokens.go @@ -0,0 +1,65 @@ +package tokens + +import ( + "context" + "crypto/rand" + "encoding/base64" + "io" + "net/http" + "time" + + "github.com/tech/sendico/pkg/db/refreshtokens" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mzap" + "go.uber.org/zap" +) + +func generateRefreshTokenData(length int) (string, error) { + randomBytes := make([]byte, length) + if _, err := io.ReadFull(rand.Reader, randomBytes); err != nil { + return "", merrors.Internal("failed to generate secure random bytes: " + err.Error()) + } + + return base64.URLEncoding.EncodeToString(randomBytes), nil +} + +func PrepareRefreshToken( + ctx context.Context, + r *http.Request, + rtdb refreshtokens.DB, + length int, + refreshExpiration int, + session *model.SessionIdentifier, + account *model.Account, + logger mlogger.Logger, +) (*model.RefreshToken, error) { + refreshToken, err := generateRefreshTokenData(length) + if err != nil { + logger.Warn("Failed to generate refresh token", zap.Error(err), mzap.StorableRef(account)) + return nil, err + } + + token := &model.RefreshToken{ + AccountBoundBase: model.AccountBoundBase{ + AccountRef: account.GetID(), + }, + ClientRefreshToken: model.ClientRefreshToken{ + SessionIdentifier: *session, + RefreshToken: refreshToken, + }, + ExpiresAt: time.Now().Add(time.Duration(refreshExpiration) * time.Hour), + IsRevoked: false, + UserAgent: r.UserAgent(), + IPAddress: r.RemoteAddr, + } + + if err = rtdb.Create(ctx, token); err != nil { + logger.Warn("Failed to store a refresh token", zap.Error(err), mzap.StorableRef(account), + zap.String("client_id", token.ClientID), zap.String("device_id", token.DeviceID)) + return nil, err + } + + return token, nil +} diff --git a/api/server/internal/server/confirmationimp/request.go b/api/server/internal/server/confirmationimp/request.go new file mode 100644 index 0000000..68e61f3 --- /dev/null +++ b/api/server/internal/server/confirmationimp/request.go @@ -0,0 +1,48 @@ +package confirmationimp + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/tech/sendico/pkg/api/http/response" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mzap" + emodel "github.com/tech/sendico/server/interface/model" + "go.uber.org/zap" +) + +func (a *ConfirmationAPI) requestCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc { + var req confirmationRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + a.logger.Warn("Failed to decode confirmation request", zap.Error(err)) + return response.BadPayload(a.logger, a.Name(), err) + } + + target, err := a.parseTarget(req.Target) + if err != nil { + return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error()) + } + + if target == model.ConfirmationTargetLogin && (token == nil || !token.Pending) { + return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token") + } + + destination := a.resolveDestination(req.Destination, account) + if destination == "" { + return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required") + } + code, rec, err := a.store.Create(r.Context(), account.ID, destination, target, a.config, a.generateCode) + if err != nil { + a.logger.Warn("Failed to create confirmation code", zap.Error(err), mzap.ObjRef("account_ref", account.ID)) + return response.Internal(a.logger, a.Name(), err) + } + + a.sendCode(account, target, destination, code) + + return response.Accepted(a.logger, confirmationResponse{ + TTLSeconds: int(time.Until(rec.ExpiresAt).Seconds()), + CooldownSeconds: int(a.config.Cooldown.Seconds()), + Destination: maskEmail(destination), + }) +} diff --git a/api/server/internal/server/confirmationimp/resend.go b/api/server/internal/server/confirmationimp/resend.go new file mode 100644 index 0000000..4e7e1d2 --- /dev/null +++ b/api/server/internal/server/confirmationimp/resend.go @@ -0,0 +1,56 @@ +package confirmationimp + +import ( + "encoding/json" + "errors" + "net/http" + "time" + + "github.com/tech/sendico/pkg/api/http/response" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mzap" + emodel "github.com/tech/sendico/server/interface/model" + "go.uber.org/zap" +) + +func (a *ConfirmationAPI) resendCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc { + var req confirmationRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + a.logger.Warn("Failed to decode confirmation resend request", zap.Error(err)) + return response.BadPayload(a.logger, a.Name(), err) + } + + target, err := a.parseTarget(req.Target) + if err != nil { + return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error()) + } + + if target == model.ConfirmationTargetLogin && (token == nil || !token.Pending) { + return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token") + } + + destination := a.resolveDestination(req.Destination, account) + if destination == "" { + return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required") + } + code, rec, err := a.store.Resend(r.Context(), account.ID, destination, target, a.config, a.generateCode) + switch { + case errors.Is(err, errConfirmationNotFound): + return response.NotFound(a.logger, a.Name(), "no_active_code_for_resend") + case errors.Is(err, errConfirmationCooldown): + return response.Forbidden(a.logger, a.Name(), "cooldown_active", "please wait before requesting another code") + case errors.Is(err, errConfirmationResendLimit): + return response.Forbidden(a.logger, a.Name(), "resend_limit_reached", "too many resend attempts") + case err != nil: + a.logger.Warn("Failed to resend confirmation code", zap.Error(err), mzap.ObjRef("account_ref", account.ID)) + return response.Internal(a.logger, a.Name(), err) + } + + a.sendCode(account, target, destination, code) + + return response.Accepted(a.logger, confirmationResponse{ + TTLSeconds: int(time.Until(rec.ExpiresAt).Seconds()), + CooldownSeconds: int(a.config.Cooldown.Seconds()), + Destination: maskEmail(destination), + }) +} diff --git a/api/server/internal/server/confirmationimp/service.go b/api/server/internal/server/confirmationimp/service.go new file mode 100644 index 0000000..737e8c8 --- /dev/null +++ b/api/server/internal/server/confirmationimp/service.go @@ -0,0 +1,158 @@ +package confirmationimp + +import ( + "context" + "crypto/rand" + "fmt" + "strings" + "time" + + "github.com/go-chi/jwtauth/v5" + api "github.com/tech/sendico/pkg/api/http" + "github.com/tech/sendico/pkg/db/refreshtokens" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/messaging" + cnotifications "github.com/tech/sendico/pkg/messaging/notifications/confirmation" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + "github.com/tech/sendico/pkg/mutil/mzap" + eapi "github.com/tech/sendico/server/interface/api" + "github.com/tech/sendico/server/interface/api/sresponse" + "github.com/tech/sendico/server/interface/middleware" + emodel "github.com/tech/sendico/server/interface/model" + "go.uber.org/zap" +) + +type Config struct { + CodeLength int + TTL time.Duration + MaxAttempts int + Cooldown time.Duration + ResendLimit int +} + +func defaultConfig() Config { + return Config{ + CodeLength: 6, + TTL: 10 * time.Minute, + MaxAttempts: 5, + Cooldown: time.Minute, + ResendLimit: 5, + } +} + +func DefaultConfig() Config { + return defaultConfig() +} + +type ConfirmationAPI struct { + logger mlogger.Logger + config Config + store *ConfirmationStore + rtdb refreshtokens.DB + producer messaging.Producer + tokenConfig middleware.TokenConfig + signature middleware.Signature +} + +func (a *ConfirmationAPI) Name() mservice.Type { + return mservice.Confirmations +} + +func (a *ConfirmationAPI) Finish(_ context.Context) error { + return nil +} + +func CreateAPI(a eapi.API) (*ConfirmationAPI, error) { + cdb, err := a.DBFactory().NewConfirmationsDB() + if err != nil { + return nil, err + } + rtdb, err := a.DBFactory().NewRefreshTokensDB() + if err != nil { + return nil, err + } + + p := &ConfirmationAPI{ + logger: a.Logger().Named(mservice.Confirmations), + config: defaultConfig(), + store: NewStore(cdb), + rtdb: rtdb, + producer: a.Register().Messaging().Producer(), + tokenConfig: a.Config().Mw.Token, + signature: middleware.SignatureConf(a.Config().Mw), + } + + a.Register().PendingAccountHandler(p.Name(), "/confirmations", api.Post, p.requestCode) + a.Register().PendingAccountHandler(p.Name(), "/confirmations/resend", api.Post, p.resendCode) + a.Register().PendingAccountHandler(p.Name(), "/confirmations/verify", api.Post, p.verifyCode) + return p, nil +} + +func (a *ConfirmationAPI) generateCode() (string, error) { + const digits = "0123456789" + b := make([]byte, a.config.CodeLength) + _, err := rand.Read(b) + if err != nil { + return "", err + } + for i := range b { + b[i] = digits[int(b[i])%len(digits)] + } + return string(b), nil +} + +func (a *ConfirmationAPI) parseTarget(raw string) (model.ConfirmationTarget, error) { + switch strings.ToLower(strings.TrimSpace(raw)) { + case string(model.ConfirmationTargetLogin): + return model.ConfirmationTargetLogin, nil + case string(model.ConfirmationTargetPayout): + return model.ConfirmationTargetPayout, nil + default: + return "", merrors.InvalidArgument(fmt.Sprintf("unsupported target '%s'", raw), "target") + } +} + +func (a *ConfirmationAPI) resolveDestination(reqDest string, account *model.Account) string { + destination := strings.ToLower(strings.TrimSpace(reqDest)) + if destination == "" && account != nil { + destination = strings.ToLower(strings.TrimSpace(account.Login)) + } + return destination +} + +func (a *ConfirmationAPI) sendCode(account *model.Account, target model.ConfirmationTarget, destination, code string) { + a.logger.Info("Confirmation code generated", + zap.String("target", string(target)), + zap.String("destination", maskEmail(destination)), + mzap.ObjRef("account_ref", account.ID)) + if err := a.producer.SendMessage(cnotifications.Code(a.Name(), account.ID, destination, target, code)); err != nil { + a.logger.Warn("Failed to send confirmation code notification", zap.Error(err), mzap.ObjRef("account_ref", account.ID)) + } + a.logger.Debug("Confirmation code debug dump (do not log in production)", zap.String("code", code)) +} + +func maskEmail(email string) string { + parts := strings.Split(email, "@") + if len(parts) != 2 { + return email + } + local := parts[0] + if len(local) > 2 { + local = local[:1] + "***" + local[len(local)-1:] + } else { + local = local[:1] + "***" + } + return local + "@" + parts[1] +} + +func (a *ConfirmationAPI) createAccessToken(account *model.Account) (sresponse.TokenData, error) { + ja := jwtauth.New(a.signature.Algorithm, a.signature.PrivateKey, a.signature.PublicKey) + _, res, err := ja.Encode(emodel.Account2Claims(account, a.tokenConfig.Expiration.Account)) + token := sresponse.TokenData{ + Token: res, + Expiration: time.Now().Add(time.Duration(a.tokenConfig.Expiration.Account) * time.Hour), + } + return token, err +} diff --git a/api/server/internal/server/confirmationimp/store.go b/api/server/internal/server/confirmationimp/store.go new file mode 100644 index 0000000..a3e83fe --- /dev/null +++ b/api/server/internal/server/confirmationimp/store.go @@ -0,0 +1,181 @@ +package confirmationimp + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "errors" + "time" + + "github.com/tech/sendico/pkg/db/confirmation" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var ( + errConfirmationNotFound = errors.New("confirmation not found or expired") + errConfirmationUsed = errors.New("confirmation already used") + errConfirmationMismatch = errors.New("confirmation code mismatch") + errConfirmationAttemptsExceeded = errors.New("confirmation attempts exceeded") + errConfirmationCooldown = errors.New("confirmation cooldown active") + errConfirmationResendLimit = errors.New("confirmation resend limit reached") +) + +type ConfirmationStore struct { + db confirmation.DB +} + +func NewStore(db confirmation.DB) *ConfirmationStore { + return &ConfirmationStore{db: db} +} + +func (s *ConfirmationStore) Create( + ctx context.Context, + accountRef primitive.ObjectID, + destination string, + target model.ConfirmationTarget, + cfg Config, + generator func() (string, error), +) (string, *model.ConfirmationCode, error) { + if err := s.db.DeleteTuple(ctx, accountRef, destination, target); err != nil && !errors.Is(err, merrors.ErrNoData) { + return "", nil, err + } + + code, _, rec, err := s.buildRecord(accountRef, destination, target, cfg, generator) + if err != nil { + return "", nil, err + } + + if err := s.db.Create(ctx, rec); err != nil { + return "", nil, err + } + + return code, rec, nil +} + +func (s *ConfirmationStore) Resend( + ctx context.Context, + accountRef primitive.ObjectID, + destination string, + target model.ConfirmationTarget, + cfg Config, + generator func() (string, error), +) (string, *model.ConfirmationCode, error) { + now := time.Now().UTC() + active, err := s.db.FindActive(ctx, accountRef, destination, target, now.Unix()) + if errors.Is(err, merrors.ErrNoData) { + return s.Create(ctx, accountRef, destination, target, cfg, generator) + } + if err != nil { + return "", nil, err + } + if active.ResendCount >= active.ResendLimit { + return "", nil, errConfirmationResendLimit + } + if now.Before(active.CooldownUntil) { + return "", nil, errConfirmationCooldown + } + + code, salt, updated, err := s.buildRecord(accountRef, destination, target, cfg, generator) + if err != nil { + return "", nil, err + } + // Preserve attempt counters but bump resend count. + updated.ID = active.ID + updated.CreatedAt = active.CreatedAt + updated.Attempts = active.Attempts + updated.ResendCount = active.ResendCount + 1 + updated.Salt = salt + + if err := s.db.Update(ctx, updated); err != nil { + return "", nil, err + } + return code, updated, nil +} + +func (s *ConfirmationStore) Verify( + ctx context.Context, + accountRef primitive.ObjectID, + destination string, + target model.ConfirmationTarget, + code string, +) error { + now := time.Now().UTC() + rec, err := s.db.FindActive(ctx, accountRef, destination, target, now.Unix()) + if errors.Is(err, merrors.ErrNoData) { + return errConfirmationNotFound + } + if err != nil { + return err + } + if rec.Used { + return errConfirmationUsed + } + + rec.Attempts++ + if rec.Attempts > rec.MaxAttempts { + rec.Used = true + _ = s.db.Update(ctx, rec) + return errConfirmationAttemptsExceeded + } + + if subtle.ConstantTimeCompare(rec.CodeHash, hashCode(rec.Salt, code)) != 1 { + _ = s.db.Update(ctx, rec) + return errConfirmationMismatch + } + + rec.Used = true + return s.db.Update(ctx, rec) +} + +func (s *ConfirmationStore) buildRecord( + accountRef primitive.ObjectID, + destination string, + target model.ConfirmationTarget, + cfg Config, + generator func() (string, error), +) (string, []byte, *model.ConfirmationCode, error) { + code, err := generator() + if err != nil { + return "", nil, nil, err + } + salt, err := newSalt() + if err != nil { + return "", nil, nil, err + } + + now := time.Now().UTC() + rec := model.NewConfirmationCode(accountRef) + rec.Destination = destination + rec.Target = target + rec.CodeHash = hashCode(salt, code) + rec.Salt = salt + rec.ExpiresAt = now.Add(cfg.TTL) + rec.MaxAttempts = cfg.MaxAttempts + rec.ResendLimit = cfg.ResendLimit + rec.CooldownUntil = now.Add(cfg.Cooldown) + rec.Used = false + rec.Attempts = 0 + rec.ResendCount = 0 + rec.CreatedAt = now + rec.UpdatedAt = now + + return code, salt, rec, nil +} + +func hashCode(salt []byte, code string) []byte { + h := sha256.New() + h.Write(salt) + h.Write([]byte(code)) + return h.Sum(nil) +} + +func newSalt() ([]byte, error) { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + return nil, err + } + return buf, nil +} diff --git a/api/server/internal/server/confirmationimp/types.go b/api/server/internal/server/confirmationimp/types.go new file mode 100644 index 0000000..743c0b6 --- /dev/null +++ b/api/server/internal/server/confirmationimp/types.go @@ -0,0 +1,23 @@ +package confirmationimp + +import ( + "github.com/tech/sendico/pkg/model" +) + +type confirmationRequest struct { + Target string `json:"target"` + Destination string `json:"destination,omitempty"` +} + +type confirmationVerifyRequest struct { + Target string `json:"target"` + Code string `json:"code"` + Destination string `json:"destination,omitempty"` + SessionIdentifier model.SessionIdentifier `json:"sessionIdentifier"` +} + +type confirmationResponse struct { + TTLSeconds int `json:"ttl_seconds"` + CooldownSeconds int `json:"cooldown_seconds"` + Destination string `json:"destination"` +} diff --git a/api/server/internal/server/confirmationimp/verify.go b/api/server/internal/server/confirmationimp/verify.go new file mode 100644 index 0000000..4ff4434 --- /dev/null +++ b/api/server/internal/server/confirmationimp/verify.go @@ -0,0 +1,88 @@ +package confirmationimp + +import ( + "encoding/json" + "errors" + "net/http" + "strings" + + "github.com/tech/sendico/pkg/api/http/response" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mzap" + "github.com/tech/sendico/server/interface/api/sresponse" + emodel "github.com/tech/sendico/server/interface/model" + rtokens "github.com/tech/sendico/server/internal/api/routers/tokens" + "go.uber.org/zap" +) + +func (a *ConfirmationAPI) verifyCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc { + var req confirmationVerifyRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + a.logger.Warn("Failed to decode confirmation verification request", zap.Error(err)) + return response.BadPayload(a.logger, a.Name(), err) + } + + target, err := a.parseTarget(req.Target) + if err != nil { + return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error()) + } + + if target == model.ConfirmationTargetLogin && (token == nil || !token.Pending) { + return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token") + } + + if strings.TrimSpace(req.Code) == "" { + return response.BadRequest(a.logger, a.Name(), "missing_code", "confirmation code is required") + } + + destination := a.resolveDestination(req.Destination, account) + if destination == "" { + return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required") + } + err = a.store.Verify(r.Context(), account.ID, destination, target, strings.TrimSpace(req.Code)) + switch { + case errors.Is(err, errConfirmationNotFound): + return response.NotFound(a.logger, a.Name(), "code_not_found_or_expired") + case errors.Is(err, errConfirmationUsed): + return response.Forbidden(a.logger, a.Name(), "code_used", "code has already been used") + case errors.Is(err, errConfirmationAttemptsExceeded): + return response.Forbidden(a.logger, a.Name(), "attempt_limit_reached", "too many failed attempts") + case errors.Is(err, errConfirmationMismatch): + return response.Forbidden(a.logger, a.Name(), "invalid_code", "code does not match") + case err != nil: + a.logger.Warn("Failed to verify confirmation code", zap.Error(err), mzap.ObjRef("account_ref", account.ID)) + return response.Internal(a.logger, a.Name(), err) + } + + a.logger.Info("Confirmation code verified", zap.String("target", string(target)), mzap.ObjRef("account_ref", account.ID)) + if target == model.ConfirmationTargetLogin { + if req.SessionIdentifier.ClientID == "" || req.SessionIdentifier.DeviceID == "" { + return response.BadRequest(a.logger, a.Name(), "missing_session", "session identifier is required") + } + accessToken, err := a.createAccessToken(account) + if err != nil { + a.logger.Warn("Failed to generate access token", zap.Error(err)) + return response.Internal(a.logger, a.Name(), err) + } + refreshToken, err := rtokens.PrepareRefreshToken( + r.Context(), + r, + a.rtdb, + a.tokenConfig.Length, + a.tokenConfig.Expiration.Refresh, + &req.SessionIdentifier, + account, + a.logger, + ) + if err != nil { + a.logger.Warn("Failed to generate refresh token", zap.Error(err)) + return response.Internal(a.logger, a.Name(), err) + } + rt := sresponse.TokenData{ + Token: refreshToken.RefreshToken, + Expiration: refreshToken.ExpiresAt, + } + return sresponse.Login(a.logger, account, &accessToken, &rt) + } + return response.Success(a.logger) +} diff --git a/frontend/pshared/lib/api/responses/login_pending.dart b/frontend/pshared/lib/api/responses/login_pending.dart new file mode 100644 index 0000000..91e5241 --- /dev/null +++ b/frontend/pshared/lib/api/responses/login_pending.dart @@ -0,0 +1,26 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/api/responses/account.dart'; +import 'package:pshared/api/responses/token.dart'; + +part 'login_pending.g.dart'; + + +@JsonSerializable(explicitToJson: true) +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); + + Map toJson() => _$PendingLoginResponseToJson(this); +} diff --git a/frontend/pshared/lib/models/auth/login_outcome.dart b/frontend/pshared/lib/models/auth/login_outcome.dart new file mode 100644 index 0000000..24df29e --- /dev/null +++ b/frontend/pshared/lib/models/auth/login_outcome.dart @@ -0,0 +1,17 @@ +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/auth/pending_login.dart'; + + +class LoginOutcome { + final Account? account; + final PendingLogin? pending; + + const LoginOutcome._({this.account, this.pending}); + + factory LoginOutcome.completed(Account account) => LoginOutcome._(account: account); + + factory LoginOutcome.pending(PendingLogin pending) => LoginOutcome._(pending: pending); + + bool get isPending => pending != null; + bool get isCompleted => account != null; +} diff --git a/frontend/pshared/lib/models/auth/pending_login.dart b/frontend/pshared/lib/models/auth/pending_login.dart new file mode 100644 index 0000000..3585bcc --- /dev/null +++ b/frontend/pshared/lib/models/auth/pending_login.dart @@ -0,0 +1,33 @@ +import 'package:pshared/api/responses/login_pending.dart'; +import 'package:pshared/api/responses/token.dart'; +import 'package:pshared/data/mapper/account/account.dart'; +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/session_identifier.dart'; + + +class PendingLogin { + final Account account; + final TokenData pendingToken; + final String destination; + final int ttlSeconds; + final SessionIdentifier session; + + const PendingLogin({ + required this.account, + required this.pendingToken, + required this.destination, + required this.ttlSeconds, + required this.session, + }); + + factory PendingLogin.fromResponse( + PendingLoginResponse response, { + required SessionIdentifier session, + }) => PendingLogin( + account: response.account.account.toDomain(), + pendingToken: response.pendingToken, + destination: response.destination, + ttlSeconds: response.ttlSeconds, + session: session, + ); +} diff --git a/frontend/pshared/lib/models/session_identifier.dart b/frontend/pshared/lib/models/session_identifier.dart new file mode 100644 index 0000000..106f8f8 --- /dev/null +++ b/frontend/pshared/lib/models/session_identifier.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'session_identifier.g.dart'; + +@JsonSerializable() +class SessionIdentifier { + final String clientId; + final String deviceId; + + const SessionIdentifier({ + required this.clientId, + required this.deviceId, + }); + + factory SessionIdentifier.fromJson(Map json) => _$SessionIdentifierFromJson(json); + + Map toJson() => _$SessionIdentifierToJson(this); +} diff --git a/frontend/pshared/lib/provider/account.dart b/frontend/pshared/lib/provider/account.dart index f44906d..fea9b02 100644 --- a/frontend/pshared/lib/provider/account.dart +++ b/frontend/pshared/lib/provider/account.dart @@ -7,6 +7,8 @@ import 'package:pshared/api/requests/signup.dart'; import 'package:pshared/api/requests/login_data.dart'; import 'package:pshared/config/constants.dart'; import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/auth/login_outcome.dart'; +import 'package:pshared/models/auth/pending_login.dart'; import 'package:pshared/models/describable.dart'; import 'package:pshared/models/storable.dart'; import 'package:pshared/provider/locale.dart'; @@ -23,8 +25,10 @@ class AccountProvider extends ChangeNotifier { Resource _resource = Resource(data: null); Resource get resource => _resource; late LocaleProvider _localeProvider; + PendingLogin? _pendingLogin; Account? get account => _resource.data; + PendingLogin? get pendingLogin => _pendingLogin; bool get isLoggedIn => account != null; bool get isLoading => _resource.isLoading; Object? get error => _resource.error; @@ -57,27 +61,38 @@ class AccountProvider extends ChangeNotifier { void _pickupLocale(String locale) => _localeProvider.setLocale(Locale(locale)); - Future login({ + Future login({ required String email, required String password, required String locale, }) async { _setResource(_resource.copyWith(isLoading: true, error: null)); try { - final acc = await AccountService.login(LoginData.build( + final outcome = await AccountService.login(LoginData.build( login: email, password: password, locale: locale, )); - _setResource(Resource(data: acc, isLoading: false)); - _pickupLocale(acc.locale); - return acc; + if (outcome.account != null) { + _setResource(Resource(data: outcome.account, isLoading: false)); + _pickupLocale(outcome.account!.locale); + } else { + _pendingLogin = outcome.pending; + _setResource(_resource.copyWith(isLoading: false)); + } + return outcome; } catch (e) { _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } + void completePendingLogin(Account account) { + _pendingLogin = null; + _setResource(Resource(data: account, isLoading: false, error: null)); + _pickupLocale(account.locale); + } + Future isAuthorizationStored() async => AuthorizationService.isAuthorizationStored(); Future restore() async { diff --git a/frontend/pshared/lib/service/account.dart b/frontend/pshared/lib/service/account.dart index a935dc2..facb9a1 100644 --- a/frontend/pshared/lib/service/account.dart +++ b/frontend/pshared/lib/service/account.dart @@ -8,9 +8,13 @@ import 'package:pshared/api/requests/login_data.dart'; import 'package:pshared/api/requests/password/change.dart'; import 'package:pshared/api/requests/password/forgot.dart'; import 'package:pshared/api/requests/password/reset.dart'; +import 'package:pshared/api/responses/login.dart'; import 'package:pshared/data/mapper/account/account.dart'; import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/auth/login_outcome.dart'; +import 'package:pshared/models/auth/pending_login.dart'; import 'package:pshared/service/authorization/service.dart'; +import 'package:pshared/service/authorization/storage.dart'; import 'package:pshared/service/files.dart'; import 'package:pshared/service/services.dart'; import 'package:pshared/utils/http/requests.dart'; @@ -20,11 +24,46 @@ class AccountService { static final _logger = Logger('service.account'); static const String _objectType = Services.account; - static Future login(LoginData login) async { + static Future login(LoginData login) async { _logger.fine('Logging in'); return AuthorizationService.login(_objectType, login); } + static Future resendLoginCode(PendingLogin pending, {String? destination}) async { + await getPOSTResponse( + _objectType, + 'confirmations/resend', + { + 'target': 'login', + if (destination != null) 'destination': destination, + }, + authToken: pending.pendingToken.token, + ); + } + + static Future confirmLoginCode({ + required PendingLogin pending, + required String code, + String? destination, + }) async { + final response = await getPOSTResponse( + _objectType, + 'confirmations/verify', + { + 'target': 'login', + 'code': code, + if (destination != null) 'destination': destination, + 'sessionIdentifier': pending.session.toJson(), + }, + authToken: pending.pendingToken.token, + ); + + final loginResponse = LoginResponse.fromJson(response); + await AuthorizationStorage.updateToken(loginResponse.accessToken); + await AuthorizationStorage.updateRefreshToken(loginResponse.refreshToken); + return loginResponse.account.toDomain(); + } + static Future restore() async { return AuthorizationService.restore(); } diff --git a/frontend/pshared/lib/service/authorization/service.dart b/frontend/pshared/lib/service/authorization/service.dart index 57e615f..a73a752 100644 --- a/frontend/pshared/lib/service/authorization/service.dart +++ b/frontend/pshared/lib/service/authorization/service.dart @@ -5,9 +5,13 @@ import 'package:pshared/api/requests/login.dart'; import 'package:pshared/api/requests/login_data.dart'; import 'package:pshared/api/responses/account.dart'; import 'package:pshared/api/responses/login.dart'; +import 'package:pshared/api/responses/login_pending.dart'; import 'package:pshared/config/web.dart'; import 'package:pshared/data/mapper/account/account.dart'; import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/auth/login_outcome.dart'; +import 'package:pshared/models/auth/pending_login.dart'; +import 'package:pshared/models/session_identifier.dart'; import 'package:pshared/service/authorization/circuit_breaker.dart'; import 'package:pshared/service/authorization/retry_helper.dart'; import 'package:pshared/service/authorization/storage.dart'; @@ -22,7 +26,7 @@ import 'package:pshared/utils/http/requests.dart' as httpr; class AuthorizationService { static final _logger = Logger('service.authorization.auth_service'); - static Future login(String service, LoginData login) async { + static Future login(String service, LoginData login) async { _logger.fine('Logging in ${login.login} with ${login.locale} locale'); final deviceId = await DeviceIdManager.getDeviceId(); final response = await httpr.getPOSTResponse( @@ -31,7 +35,17 @@ class AuthorizationService { LoginRequest(login: login, deviceId: deviceId, clientId: Constants.clientId).toJson(), ); - return (await _completeLogin(response)).account.toDomain(); + if (response.containsKey('refreshToken')) { + return LoginOutcome.completed((await completeLogin(response)).account.toDomain()); + } + if (response.containsKey('pendingToken')) { + final pending = PendingLogin.fromResponse( + PendingLoginResponse.fromJson(response), + session: SessionIdentifier(clientId: Constants.clientId, deviceId: deviceId), + ); + return LoginOutcome.pending(pending); + } + throw AuthenticationFailedException('Unexpected login response', Exception(response.toString())); } static Future _updateAccessToken(AccountResponse response) async { @@ -49,6 +63,8 @@ class AuthorizationService { return lr; } + static Future completeLogin(Map response) => _completeLogin(response); + static Future restore() async { return (await TokenService.refreshAccessToken()).account.toDomain(); } diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index 3eaca66..a5bf863 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -24,7 +24,6 @@ import 'package:pweb/providers/two_factor.dart'; import 'package:pweb/providers/upload_history.dart'; import 'package:pweb/providers/wallets.dart'; import 'package:pweb/services/amplitude.dart'; -import 'package:pweb/services/auth.dart'; import 'package:pweb/services/balance.dart'; import 'package:pweb/services/payments/payment_methods.dart'; import 'package:pweb/services/payments/upload_history.dart'; @@ -53,17 +52,16 @@ void main() async { runApp( MultiProvider( providers: [ - Provider( - create: (_) => AuthenticationService(), - ), - ChangeNotifierProxyProvider( - create: (context) => TwoFactorProvider( - context.read(), - ), - update: (context, authService, previous) => TwoFactorProvider(authService), - ), ChangeNotifierProvider(create: (_) => LocaleProvider(null)), ChangeNotifierProvider(create: (_) => AccountProvider()), + ChangeNotifierProxyProvider( + create: (context) => TwoFactorProvider( + accountProvider: context.read(), + ), + update: (context, accountProvider, previous) => TwoFactorProvider( + accountProvider: accountProvider, + ), + ), ChangeNotifierProvider(create: (_) => OrganizationsProvider()), ChangeNotifierProvider(create: (_) => AccountProvider()), ChangeNotifierProvider(create: (_) => CarouselIndexProvider()), diff --git a/frontend/pweb/lib/pages/2fa/resend.dart b/frontend/pweb/lib/pages/2fa/resend.dart index 57bde0d..13eb971 100644 --- a/frontend/pweb/lib/pages/2fa/resend.dart +++ b/frontend/pweb/lib/pages/2fa/resend.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/providers/two_factor.dart'; class ResendCodeButton extends StatelessWidget { @@ -12,9 +15,7 @@ class ResendCodeButton extends StatelessWidget { final localizations = AppLocalizations.of(context)!; return TextButton( - onPressed: () { - // TODO: Add resend logic - }, + onPressed: () => context.read().resendCode(), style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: const Size(0, 0), @@ -28,4 +29,4 @@ class ResendCodeButton extends StatelessWidget { child: Text(localizations.twoFactorResend), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/login/form.dart b/frontend/pweb/lib/pages/login/form.dart index a145cb6..bf230b1 100644 --- a/frontend/pweb/lib/pages/login/form.dart +++ b/frontend/pweb/lib/pages/login/form.dart @@ -38,13 +38,16 @@ class _LoginFormState extends State { final provider = Provider.of(context, listen: false); try { - //final account = - await provider.login( + final outcome = await provider.login( email: _usernameController.text, password: _passwordController.text, locale: context.read().locale.languageCode, ); - onLogin(); + if (outcome.isPending) { + navigateAndReplace(context, Pages.sfactor); + } else { + onLogin(); + } return 'ok'; } catch (e) { onError(provider.error ?? e); @@ -92,7 +95,7 @@ class _LoginFormState extends State { onSignUp: () => navigate(context, Pages.signup), login: () => _login( context, - () => navigateAndReplace(context, Pages.sfactor), + () => navigateAndReplace(context, Pages.dashboard), (e) => postNotifyUserOfErrorX( context: context, errorSituation: AppLocalizations.of(context)!.errorLogin, diff --git a/frontend/pweb/lib/providers/two_factor.dart b/frontend/pweb/lib/providers/two_factor.dart index c5a74e4..f23bb52 100644 --- a/frontend/pweb/lib/providers/two_factor.dart +++ b/frontend/pweb/lib/providers/two_factor.dart @@ -1,38 +1,69 @@ import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; -import 'package:pweb/services/auth.dart'; - +import 'package:pshared/models/auth/pending_login.dart'; +import 'package:pshared/provider/account.dart'; +import 'package:pshared/service/account.dart'; class TwoFactorProvider extends ChangeNotifier { - final AuthenticationService _authService; + static final _logger = Logger('provider.two_factor'); + final AccountProvider _accountProvider; - TwoFactorProvider(this._authService); + TwoFactorProvider({required AccountProvider accountProvider}) : _accountProvider = accountProvider; bool _isSubmitting = false; bool _hasError = false; bool _verificationSuccess = false; + String? _errorMessage; bool get isSubmitting => _isSubmitting; bool get hasError => _hasError; bool get verificationSuccess => _verificationSuccess; + String? get errorMessage => _errorMessage; + PendingLogin? get pendingLogin => _accountProvider.pendingLogin; Future submitCode(String code) async { _isSubmitting = true; _hasError = false; + _errorMessage = null; _verificationSuccess = false; notifyListeners(); try { - final success = await _authService.verifyTwoFactorCode(code); - if (success) { - _verificationSuccess = true; + final pending = _accountProvider.pendingLogin; + if (pending == null) { + throw Exception('No pending login available'); } + final account = await AccountService.confirmLoginCode( + pending: pending, + code: code, + ); + _accountProvider.completePendingLogin(account); + _verificationSuccess = true; } catch (e) { _hasError = true; + _errorMessage = e.toString(); + _logger.warning('Failed to verify code', e); } finally { _isSubmitting = false; notifyListeners(); } } -} \ No newline at end of file + + Future resendCode() async { + final pending = _accountProvider.pendingLogin; + if (pending == null) { + _logger.warning('No pending login to resend code for'); + return; + } + try { + await AccountService.resendLoginCode(pending); + } catch (e) { + _logger.warning('Failed to resend login code', e); + _hasError = true; + _errorMessage = e.toString(); + notifyListeners(); + } + } +} diff --git a/frontend/pweb/lib/services/auth.dart b/frontend/pweb/lib/services/auth.dart deleted file mode 100644 index 95a75e7..0000000 --- a/frontend/pweb/lib/services/auth.dart +++ /dev/null @@ -1,12 +0,0 @@ - -class AuthenticationService { - Future verifyTwoFactorCode(String code) async { - await Future.delayed(const Duration(seconds: 2)); - - if (code == '000000') { - return true; - } else { - throw Exception('Wrong Code'); //TODO Localize - } - } -} diff --git a/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift index 79f5652..33de092 100644 --- a/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,7 +8,6 @@ import Foundation import amplitude_flutter import file_selector_macos import flutter_timezone -import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -18,7 +17,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AmplitudeFlutterPlugin.register(with: registry.registrar(forPlugin: "AmplitudeFlutterPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) From 4d7827db61d841cc7de0b433c064201de939e70e Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 21 Nov 2025 18:05:01 +0100 Subject: [PATCH 30/32] prod setting for frontend --- frontend/pshared/lib/config/common.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/pshared/lib/config/common.dart b/frontend/pshared/lib/config/common.dart index b0f6de5..c2240f9 100644 --- a/frontend/pshared/lib/config/common.dart +++ b/frontend/pshared/lib/config/common.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; class CommonConstants { - static String apiProto = const String.fromEnvironment('API_PROTO', defaultValue: 'http'); - static String apiHost = const String.fromEnvironment('API_HOST', defaultValue: 'localhost'); + static String apiProto = 'https'; + static String apiHost = 'app.sendico.io'; + // static String apiProto = const String.fromEnvironment('API_PROTO', defaultValue: 'http'); + // static String apiHost = const String.fromEnvironment('API_HOST', defaultValue: 'localhost'); // static String apiHost = 'localhost'; // static String apiHost = '10.0.2.2'; static String apiEndpoint = '/api/v1'; From d00d9275fe4f890b8d46bd082fac8ed333f7ad73 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 21 Nov 2025 22:56:36 +0100 Subject: [PATCH 31/32] prod conf update --- api/payments/orchestrator/config.yml | 2 +- api/server/config.yml | 9 +++++---- ci/prod/.env.runtime | 1 + ci/prod/compose/bff.yml | 1 + 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/api/payments/orchestrator/config.yml b/api/payments/orchestrator/config.yml index 037c328..645d3cc 100644 --- a/api/payments/orchestrator/config.yml +++ b/api/payments/orchestrator/config.yml @@ -46,7 +46,7 @@ ledger: insecure: true gateway: - address: "sendico_chain_gateway:50054" + address: "sendico_chain_gateway:50070" dial_timeout_seconds: 5 call_timeout_seconds: 3 insecure: true diff --git a/api/server/config.yml b/api/server/config.yml index 124b6ff..cb4c79e 100755 --- a/api/server/config.yml +++ b/api/server/config.yml @@ -71,12 +71,13 @@ api: # secret_access_key_env: S3_ACCESS_KEY_SECRET # region_env: S3_REGION # bucket_name_env: S3_BUCKET_NAME - driver: local_fs - settings: - root_path: ./storage + driver: local_fs + settings: + root_path: ./storage chain_gateway: - address: sendico-chain-gateway + address: sendico_chain_gateway:50070 + address_env: CHAIN_GATEWAY_ADDRESS dial_timeout_seconds: 5 call_timeout_seconds: 5 insecure: true diff --git a/ci/prod/.env.runtime b/ci/prod/.env.runtime index 13712ad..d67f10c 100644 --- a/ci/prod/.env.runtime +++ b/ci/prod/.env.runtime @@ -127,6 +127,7 @@ BFF_HTTP_PORT=8080 CHAIN_GATEWAY_DIR=chain_gateway CHAIN_GATEWAY_COMPOSE_PROJECT=sendico-chain-gateway CHAIN_GATEWAY_SERVICE_NAME=sendico_chain_gateway +CHAIN_GATEWAY_ADDRESS=sendico_chain_gateway:50070 CHAIN_GATEWAY_GRPC_PORT=50070 CHAIN_GATEWAY_METRICS_PORT=9404 diff --git a/ci/prod/compose/bff.yml b/ci/prod/compose/bff.yml index 605f3da..550c6b0 100644 --- a/ci/prod/compose/bff.yml +++ b/ci/prod/compose/bff.yml @@ -29,6 +29,7 @@ services: NATS_PORT: ${NATS_PORT} NATS_USER: ${NATS_USER} NATS_PASSWORD: ${NATS_PASSWORD} + CHAIN_GATEWAY_ADDRESS: ${CHAIN_GATEWAY_ADDRESS} MONGO_HOST: ${MONGO_HOST} MONGO_PORT: ${MONGO_PORT} MONGO_DATABASE: ${MONGO_DATABASE} From 0b0d329b9b1e9a8320dc7f25a8badcf7ec656534 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Sun, 23 Nov 2025 15:37:45 +0100 Subject: [PATCH 32/32] build script update --- .woodpecker/bump_version.yml | 10 +- api/chain/gateway/config.yml | 6 +- api/chain/gateway/entrypoint.sh | 15 + .../internal/keymanager/vault/manager.go | 4 +- ci/prod/compose/chain_gateway.dockerfile | 6 +- ci/prod/compose/chain_gateway.yml | 44 ++- ci/prod/compose/vault-agent/chain-gateway.hcl | 20 ++ ci/prod/scripts/deploy/chain_gateway.sh | 17 +- ci/scripts/chain_gateway/deploy.sh | 7 +- ci/scripts/common/bump_version.sh | 280 ++---------------- infra/vault/docker-compose.yml | 51 ++++ 11 files changed, 188 insertions(+), 272 deletions(-) create mode 100644 api/chain/gateway/entrypoint.sh create mode 100644 ci/prod/compose/vault-agent/chain-gateway.hcl create mode 100644 infra/vault/docker-compose.yml diff --git a/.woodpecker/bump_version.yml b/.woodpecker/bump_version.yml index 49e5703..9c13308 100644 --- a/.woodpecker/bump_version.yml +++ b/.woodpecker/bump_version.yml @@ -12,8 +12,8 @@ depends_on: - payments_orchestrator when: - - event: push - branch: main + event: push + branch: main steps: - name: bump-version @@ -21,7 +21,13 @@ steps: environment: GIT_AUTHOR_NAME: woodpecker GIT_AUTHOR_EMAIL: ci@sendico.io + GIT_COMMITTER_NAME: woodpecker + GIT_COMMITTER_EMAIL: ci@sendico.io commands: - set -euo pipefail - apk add --no-cache git + # make sure git knows who commits + - git config user.name "$GIT_AUTHOR_NAME" + - git config user.email "$GIT_AUTHOR_EMAIL" + # run your script (must do commit + push) - sh ci/scripts/common/bump_version.sh diff --git a/api/chain/gateway/config.yml b/api/chain/gateway/config.yml index dbd48ce..05dada2 100644 --- a/api/chain/gateway/config.yml +++ b/api/chain/gateway/config.yml @@ -50,8 +50,8 @@ service_wallet: key_management: driver: vault settings: - address: "https://vault.sendico.io:8200" - token_env: CHAIN_GATEWAY_VAULT_TOKEN + address: "https://vault.sendico.io" + token_env: VAULT_TOKEN namespace: "" - mount_path: secret + mount_path: kv key_prefix: chain/gateway/wallets diff --git a/api/chain/gateway/entrypoint.sh b/api/chain/gateway/entrypoint.sh new file mode 100644 index 0000000..965b66d --- /dev/null +++ b/api/chain/gateway/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -eu + +if [ -n "${VAULT_TOKEN_FILE:-}" ] && [ -f "${VAULT_TOKEN_FILE}" ]; then + token="$(cat "${VAULT_TOKEN_FILE}" 2>/dev/null | tr -d '[:space:]')" + if [ -n "${token}" ]; then + export VAULT_TOKEN="${token}" + fi +fi + +if [ -z "${VAULT_TOKEN:-}" ]; then + echo "[entrypoint] VAULT_TOKEN is not set; expected Vault Agent sink to write a token to ${VAULT_TOKEN_FILE:-/run/vault/token}" >&2 +fi + +exec /app/chain-gateway "$@" diff --git a/api/chain/gateway/internal/keymanager/vault/manager.go b/api/chain/gateway/internal/keymanager/vault/manager.go index 85f9b37..9e7db59 100644 --- a/api/chain/gateway/internal/keymanager/vault/manager.go +++ b/api/chain/gateway/internal/keymanager/vault/manager.go @@ -55,8 +55,8 @@ func New(logger mlogger.Logger, cfg Config) (*Manager, error) { } token := strings.TrimSpace(os.Getenv(tokenEnv)) if token == "" { - logger.Error("vault token env not set", zap.String("env", tokenEnv)) - return nil, merrors.InvalidArgument("vault key manager: token env " + tokenEnv + " is not set") + logger.Error("vault token missing; expected Vault Agent to export token", zap.String("env", tokenEnv)) + return nil, merrors.InvalidArgument("vault key manager: token env " + tokenEnv + " is not set (expected Vault Agent sink to populate it)") } mountPath := strings.Trim(strings.TrimSpace(cfg.MountPath), "/") if mountPath == "" { diff --git a/ci/prod/compose/chain_gateway.dockerfile b/ci/prod/compose/chain_gateway.dockerfile index 028eaa6..b02d311 100644 --- a/ci/prod/compose/chain_gateway.dockerfile +++ b/ci/prod/compose/chain_gateway.dockerfile @@ -35,7 +35,9 @@ RUN apk add --no-cache ca-certificates tzdata wget WORKDIR /app COPY api/chain/gateway/config.yml /app/config.yml COPY api/chain/gateway/env /app/env +COPY api/chain/gateway/entrypoint.sh /app/entrypoint.sh COPY --from=build /out/chain-gateway /app/chain-gateway +RUN chmod +x /app/entrypoint.sh EXPOSE 50070 9403 -ENTRYPOINT ["/app/chain-gateway"] -CMD ["--config.file", "/app/config.yml"] +ENTRYPOINT ["/app/entrypoint.sh"] +CMD ["/app/chain-gateway","--config.file","/app/config.yml"] diff --git a/ci/prod/compose/chain_gateway.yml b/ci/prod/compose/chain_gateway.yml index 66fc404..4a3a770 100644 --- a/ci/prod/compose/chain_gateway.yml +++ b/ci/prod/compose/chain_gateway.yml @@ -5,6 +5,14 @@ x-common-env: &common-env - ../env/.env.runtime - ../env/.env.version +volumes: + chain-gateway-vault-run: + driver: local + driver_opts: + type: tmpfs + device: tmpfs + o: size=8m,uid=0,gid=0,mode=0700 + networks: sendico-net: external: true @@ -33,11 +41,16 @@ services: CHAIN_GATEWAY_ARBITRUM_RPC_URL: ${CHAIN_GATEWAY_ARBITRUM_RPC_URL} CHAIN_GATEWAY_SERVICE_WALLET_KEY: ${CHAIN_GATEWAY_SERVICE_WALLET_KEY} CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS: ${CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS} - CHAIN_GATEWAY_VAULT_TOKEN: ${CHAIN_GATEWAY_VAULT_TOKEN} + VAULT_TOKEN_FILE: /run/vault/token command: ["--config.file", "/app/config.yml"] ports: - "0.0.0.0:${CHAIN_GATEWAY_GRPC_PORT}:50070" - "0.0.0.0:${CHAIN_GATEWAY_METRICS_PORT}:9403" + volumes: + - chain-gateway-vault-run:/run/vault:ro + depends_on: + sendico_chain_gateway_vault_agent: + condition: service_healthy healthcheck: test: ["CMD-SHELL","wget -qO- http://localhost:9403/health | grep -q '\"status\":\"ok\"'"] interval: 30s @@ -46,3 +59,32 @@ services: start_period: 60s networks: - sendico-net + + sendico_chain_gateway_vault_agent: + <<: *common-env + container_name: sendico-chain-gateway-vault-agent + restart: unless-stopped + image: hashicorp/vault:latest + pull_policy: always + cap_add: ["IPC_LOCK"] + environment: + VAULT_ADDR: ${VAULT_ADDR} + CHAIN_GATEWAY_VAULT_ROLE_ID: ${CHAIN_GATEWAY_VAULT_ROLE_ID} + CHAIN_GATEWAY_VAULT_SECRET_ID: ${CHAIN_GATEWAY_VAULT_SECRET_ID} + command: > + sh -lc 'set -euo pipefail; umask 077; + : "${CHAIN_GATEWAY_VAULT_ROLE_ID:?}"; : "${CHAIN_GATEWAY_VAULT_SECRET_ID:?}"; + printf "%s" "$CHAIN_GATEWAY_VAULT_ROLE_ID" > /run/vault/role_id; + printf "%s" "$CHAIN_GATEWAY_VAULT_SECRET_ID" > /run/vault/secret_id; + unset CHAIN_GATEWAY_VAULT_ROLE_ID CHAIN_GATEWAY_VAULT_SECRET_ID; + exec vault agent -config=/etc/vault/agent/chain-gateway.hcl' + volumes: + - ./vault-agent/chain-gateway.hcl:/etc/vault/agent/chain-gateway.hcl:ro + - chain-gateway-vault-run:/run/vault + healthcheck: + test: ["CMD","test","-s","/run/vault/token"] + interval: 10s + timeout: 5s + retries: 6 + networks: + - sendico-net diff --git a/ci/prod/compose/vault-agent/chain-gateway.hcl b/ci/prod/compose/vault-agent/chain-gateway.hcl new file mode 100644 index 0000000..3dbfc80 --- /dev/null +++ b/ci/prod/compose/vault-agent/chain-gateway.hcl @@ -0,0 +1,20 @@ +vault { + address = "https://vault.sendico.io" +} + +auto_auth { + method "approle" { + mount_path = "auth/approle" + config = { + role_id_file_path = "/run/vault/role_id" + secret_id_file_path = "/run/vault/secret_id" + } + } + + sink "file" { + config = { + path = "/run/vault/token" + mode = 0600 + } + } +} diff --git a/ci/prod/scripts/deploy/chain_gateway.sh b/ci/prod/scripts/deploy/chain_gateway.sh index 8207a74..d31d5dd 100755 --- a/ci/prod/scripts/deploy/chain_gateway.sh +++ b/ci/prod/scripts/deploy/chain_gateway.sh @@ -21,7 +21,8 @@ REQUIRED_SECRETS=( CHAIN_GATEWAY_ARBITRUM_RPC_URL CHAIN_GATEWAY_SERVICE_WALLET_KEY CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS - CHAIN_GATEWAY_VAULT_TOKEN + CHAIN_GATEWAY_VAULT_ROLE_ID + CHAIN_GATEWAY_VAULT_SECRET_ID NATS_USER NATS_PASSWORD NATS_URL @@ -48,7 +49,8 @@ CHAIN_GATEWAY_MONGO_PASSWORD_B64="$(b64enc "${CHAIN_GATEWAY_MONGO_PASSWORD}")" CHAIN_GATEWAY_ARBITRUM_RPC_URL_B64="$(b64enc "${CHAIN_GATEWAY_ARBITRUM_RPC_URL}")" CHAIN_GATEWAY_SERVICE_WALLET_KEY_B64="$(b64enc "${CHAIN_GATEWAY_SERVICE_WALLET_KEY}")" CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS_B64="$(b64enc "${CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS}")" -CHAIN_GATEWAY_VAULT_TOKEN_B64="$(b64enc "${CHAIN_GATEWAY_VAULT_TOKEN}")" +CHAIN_GATEWAY_VAULT_ROLE_ID_B64="$(b64enc "${CHAIN_GATEWAY_VAULT_ROLE_ID}")" +CHAIN_GATEWAY_VAULT_SECRET_ID_B64="$(b64enc "${CHAIN_GATEWAY_VAULT_SECRET_ID}")" NATS_USER_B64="$(b64enc "${NATS_USER}")" NATS_PASSWORD_B64="$(b64enc "${NATS_PASSWORD}")" NATS_URL_B64="$(b64enc "${NATS_URL}")" @@ -67,7 +69,7 @@ fi RSYNC_FLAGS=(-az --delete) [[ "${DEBUG_DEPLOY:-0}" = "1" ]] && RSYNC_FLAGS=(-avz --delete) -ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" "mkdir -p ${REMOTE_DIR}/{compose,env}" +ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" "mkdir -p ${REMOTE_DIR}/compose/secrets ${REMOTE_DIR}/env" rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" ci/prod/compose/ "$REMOTE_TARGET:${REMOTE_DIR}/compose/" rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" ci/prod/.env.runtime "$REMOTE_TARGET:${REMOTE_DIR}/env/.env.runtime" @@ -85,7 +87,8 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" \ CHAIN_GATEWAY_ARBITRUM_RPC_URL_B64="$CHAIN_GATEWAY_ARBITRUM_RPC_URL_B64" \ CHAIN_GATEWAY_SERVICE_WALLET_KEY_B64="$CHAIN_GATEWAY_SERVICE_WALLET_KEY_B64" \ CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS_B64="$CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS_B64" \ - CHAIN_GATEWAY_VAULT_TOKEN_B64="$CHAIN_GATEWAY_VAULT_TOKEN_B64" \ + CHAIN_GATEWAY_VAULT_ROLE_ID_B64="$CHAIN_GATEWAY_VAULT_ROLE_ID_B64" \ + CHAIN_GATEWAY_VAULT_SECRET_ID_B64="$CHAIN_GATEWAY_VAULT_SECRET_ID_B64" \ NATS_USER_B64="$NATS_USER_B64" \ NATS_PASSWORD_B64="$NATS_PASSWORD_B64" \ NATS_URL_B64="$NATS_URL_B64" \ @@ -135,7 +138,8 @@ CHAIN_GATEWAY_MONGO_PASSWORD="$(decode_b64 "$CHAIN_GATEWAY_MONGO_PASSWORD_B64")" CHAIN_GATEWAY_ARBITRUM_RPC_URL="$(decode_b64 "$CHAIN_GATEWAY_ARBITRUM_RPC_URL_B64")" CHAIN_GATEWAY_SERVICE_WALLET_KEY="$(decode_b64 "$CHAIN_GATEWAY_SERVICE_WALLET_KEY_B64")" CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS="$(decode_b64 "$CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS_B64")" -CHAIN_GATEWAY_VAULT_TOKEN="$(decode_b64 "$CHAIN_GATEWAY_VAULT_TOKEN_B64")" +CHAIN_GATEWAY_VAULT_ROLE_ID="$(decode_b64 "$CHAIN_GATEWAY_VAULT_ROLE_ID_B64")" +CHAIN_GATEWAY_VAULT_SECRET_ID="$(decode_b64 "$CHAIN_GATEWAY_VAULT_SECRET_ID_B64")" NATS_USER="$(decode_b64 "$NATS_USER_B64")" NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")" NATS_URL="$(decode_b64 "$NATS_URL_B64")" @@ -143,8 +147,9 @@ NATS_URL="$(decode_b64 "$NATS_URL_B64")" export CHAIN_GATEWAY_MONGO_USER CHAIN_GATEWAY_MONGO_PASSWORD export CHAIN_GATEWAY_ARBITRUM_RPC_URL export CHAIN_GATEWAY_SERVICE_WALLET_KEY CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS -export CHAIN_GATEWAY_VAULT_TOKEN +export CHAIN_GATEWAY_VAULT_ROLE_ID CHAIN_GATEWAY_VAULT_SECRET_ID export NATS_USER NATS_PASSWORD NATS_URL + COMPOSE_PROJECT_NAME="$COMPOSE_PROJECT" export COMPOSE_PROJECT_NAME read -r -a SERVICES <<<"${SERVICES_LINE}" diff --git a/ci/scripts/chain_gateway/deploy.sh b/ci/scripts/chain_gateway/deploy.sh index f3d701b..417b070 100755 --- a/ci/scripts/chain_gateway/deploy.sh +++ b/ci/scripts/chain_gateway/deploy.sh @@ -61,7 +61,12 @@ export CHAIN_GATEWAY_ARBITRUM_RPC_URL="$(./ci/vlt kv_get kv "${CHAIN_GATEWAY_RPC export CHAIN_GATEWAY_SERVICE_WALLET_KEY="$(./ci/vlt kv_get kv "${CHAIN_GATEWAY_WALLET_SECRET_PATH}" private_key)" export CHAIN_GATEWAY_SERVICE_WALLET_ADDRESS="$(./ci/vlt kv_get kv "${CHAIN_GATEWAY_WALLET_SECRET_PATH}" address || true)" -export CHAIN_GATEWAY_VAULT_TOKEN="$(./ci/vlt kv_get kv "${CHAIN_GATEWAY_VAULT_SECRET_PATH}" token)" +export CHAIN_GATEWAY_VAULT_ROLE_ID="$(./ci/vlt kv_get kv "${CHAIN_GATEWAY_VAULT_SECRET_PATH}" role_id)" +export CHAIN_GATEWAY_VAULT_SECRET_ID="$(./ci/vlt kv_get kv "${CHAIN_GATEWAY_VAULT_SECRET_PATH}" secret_id)" +if [ -z "${CHAIN_GATEWAY_VAULT_ROLE_ID}" ] || [ -z "${CHAIN_GATEWAY_VAULT_SECRET_ID}" ]; then + echo "[chain-gateway-deploy] vault approle creds are empty for path ${CHAIN_GATEWAY_VAULT_SECRET_PATH}" >&2 + exit 1 +fi export NATS_USER="$(./ci/vlt kv_get kv sendico/nats user)" export NATS_PASSWORD="$(./ci/vlt kv_get kv sendico/nats password)" diff --git a/ci/scripts/common/bump_version.sh b/ci/scripts/common/bump_version.sh index eba26d1..e126ad6 100755 --- a/ci/scripts/common/bump_version.sh +++ b/ci/scripts/common/bump_version.sh @@ -1,264 +1,34 @@ -#!/bin/sh -set -eu +# /bin/bash -log() { - printf '[bump-version] %s\n' "$*" -} +echo "====================================" +echo "Incrementing build version..." +echo "====================================" +VERSION_FILE=./version -error() { - printf '[bump-version] %s\n' "$*" >&2 -} +NEW_VERSION=$(cat $VERSION_FILE | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{$NF=sprintf("%0*d", length($NF), ($NF+1)); print}') +echo $NEW_VERSION > $VERSION_FILE -die() { - error "${1:-fatal error}" - exit "${2:-1}" -} +echo "New version is "$NEW_VERSION -var_value() { - # shellcheck disable=SC2039 - eval "printf '%s' \"\${$1:-}\"" -} - -normalize_host() { - value="$1" - if [ -z "${value}" ]; then - printf '%s' "" - return - fi - printf '%s' "${value}" | sed -E ' - s#^[[:alpha:]][[:alnum:]+.-]*://##; - s#^[^@]*@##; - s#[:/].*$##; - ' -} - -extract_proto() { - url="$1" - if printf '%s' "${url}" | grep -Eq '^[[:alpha:]][[:alnum:]+.-]*://'; then - printf '%s' "${url}" | sed -E 's#^([[:alpha:]][[:alnum:]+.-]*://).*#\1#' | tr -d ':/' - return - fi - if printf '%s' "${url}" | grep -Eq '^[^@]+@[^:]+:'; then - printf 'ssh' - return - fi - printf 'https' -} - -discover_env_credentials() { - DEFAULT_GIT_USER="${GIT_AUTH_DEFAULT_USER:-woodpecker}" - while IFS=: read -r user_var pass_var; do - [ -n "${pass_var}" ] || continue - pass="$(var_value "${pass_var}")" - [ -n "${pass}" ] || continue - if [ "${user_var}" = "-" ] || [ -z "${user_var}" ]; then - user="" - else - user="$(var_value "${user_var}")" - fi - if [ -z "${user}" ]; then - user="${DEFAULT_GIT_USER}" - fi - GIT_AUTH_USER="${user}" - GIT_AUTH_PASS="${pass}" - GIT_AUTH_SOURCE="env:${user_var:--}/${pass_var}" - return 0 - done <<'EOF' -CI_GIT_USERNAME:CI_GIT_PASSWORD -CI_GIT_USERNAME:CI_GIT_TOKEN -CI_GIT_USERNAME:CI_GIT_PASS -CI_GIT_USER:CI_GIT_PASSWORD -CI_GIT_USER:CI_GIT_TOKEN -CI_NETRC_USERNAME:CI_NETRC_PASSWORD -CI_NETRC_LOGIN:CI_NETRC_PASSWORD -WOODPECKER_GIT_USERNAME:WOODPECKER_GIT_PASSWORD -WOODPECKER_GIT_USERNAME:WOODPECKER_GIT_TOKEN -WOODPECKER_NETRC_USERNAME:WOODPECKER_NETRC_PASSWORD -WOODPECKER_NETRC_LOGIN:WOODPECKER_NETRC_PASSWORD -DRONE_GIT_USERNAME:DRONE_GIT_PASSWORD -DRONE_GIT_USERNAME:DRONE_GIT_TOKEN -DRONE_NETRC_USERNAME:DRONE_NETRC_PASSWORD -DRONE_NETRC_LOGIN:DRONE_NETRC_PASSWORD -GIT_USERNAME:GIT_PASSWORD -GIT_USER:GIT_PASS -GIT_AUTH_USERNAME:GIT_AUTH_PASSWORD --:GIT_PASSWORD --:GIT_PASS -EOF - - for token_var in CI_GIT_TOKEN WOODPECKER_GIT_TOKEN DRONE_GIT_TOKEN GIT_TOKEN GITEA_TOKEN; do - token="$(var_value "${token_var}")" - if [ -n "${token}" ]; then - user="$(var_value CI_GIT_USERNAME)" - [ -n "${user}" ] || user="$(var_value WOODPECKER_GIT_USERNAME)" - [ -n "${user}" ] || user="$(var_value DRONE_GIT_USERNAME)" - [ -n "${user}" ] || user="${DEFAULT_GIT_USER}" - GIT_AUTH_USER="${user}" - GIT_AUTH_PASS="${token}" - GIT_AUTH_SOURCE="env:${token_var}" - return 0 - fi - done - return 1 -} - -discover_vault_credentials() { - VAULT_MOUNT="${GIT_CREDENTIALS_VAULT_MOUNT:-kv}" - VAULT_PATH="${GIT_CREDENTIALS_VAULT_PATH:-${CI_GIT_VAULT_PATH:-}}" - [ -n "${VAULT_PATH}" ] || return 1 - VLT_BIN="${GIT_VAULT_HELPER:-./ci/vlt}" - if [ ! -x "${VLT_BIN}" ]; then - return 1 - fi - user_field="${GIT_CREDENTIALS_VAULT_USER_FIELD:-username}" - pass_field="${GIT_CREDENTIALS_VAULT_PASSWORD_FIELD:-password}" - if ! GIT_AUTH_USER="$("${VLT_BIN}" kv_get "${VAULT_MOUNT}" "${VAULT_PATH}" "${user_field}" 2>/dev/null)"; then - return 1 - fi - if ! GIT_AUTH_PASS="$("${VLT_BIN}" kv_get "${VAULT_MOUNT}" "${VAULT_PATH}" "${pass_field}" 2>/dev/null)"; then - return 1 - fi - GIT_AUTH_SOURCE="vault:${VAULT_MOUNT}/${VAULT_PATH}" - return 0 -} - -write_netrc() { - host="$1" - user="$2" - pass="$3" - file="${HOME:-/root}/.netrc" - { - printf 'machine %s\n' "${host}" - printf 'login %s\n' "${user}" - printf 'password %s\n' "${pass}" - } > "${file}" - chmod 600 "${file}" -} - -setup_https_credentials() { - host="$1" - netrc="${HOME:-/root}/.netrc" - if [ -s "${netrc}" ] && awk -v host="${host}" ' - $1 == "machine" && $2 == host { found=1; exit } - END { exit found ? 0 : 1 } - ' "${netrc}"; then - log "reusing credentials already present in ${netrc}" - return 0 - fi - if discover_env_credentials; then - masked="$(printf '%s' "${GIT_AUTH_USER}" | cut -c1-2)***" - log "using ${GIT_AUTH_SOURCE} for ${host} (user ${masked})" - write_netrc "${host}" "${GIT_AUTH_USER}" "${GIT_AUTH_PASS}" - return 0 - fi - if discover_vault_credentials; then - masked="$(printf '%s' "${GIT_AUTH_USER}" | cut -c1-2)***" - log "using ${GIT_AUTH_SOURCE} for ${host} (user ${masked})" - write_netrc "${host}" "${GIT_AUTH_USER}" "${GIT_AUTH_PASS}" - return 0 - fi - return 1 -} - -START_DIR="$(pwd)" -log "invoked from ${START_DIR}" - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="" -if command -v git >/dev/null 2>&1; then - REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" -fi -if [ -z "${REPO_ROOT}" ]; then - REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" -fi -log "repo root resolved to ${REPO_ROOT}" -cd "${REPO_ROOT}" - -VERSION_FILE="./version" -if [ ! -f "${VERSION_FILE}" ]; then - if git cat-file -e "HEAD:version" 2>/dev/null; then - error "version file missing in workspace, restoring from HEAD" - git show "HEAD:version" > "${VERSION_FILE}" - else - die "version file not found: ${VERSION_FILE}" - fi +echo "====================================" +echo "Bumping client version..." +echo "====================================" +FILE="./frontend/mweb/pubspec.yaml" +if sed --version >/dev/null 2>&1; then + # GNU sed + sed -i "s/^version: .*/version: ${NEW_VERSION}+1/" "$FILE" +else + # BSD/macOS sed + sed -i '' -e "s/^version: .*/version: ${NEW_VERSION}+1/" "$FILE" fi -CURRENT_VERSION="$(cat "${VERSION_FILE}")" -NEXT_VERSION="$(printf '%s' "${CURRENT_VERSION}" | awk -F. -v OFS=. ' - function pad(value, width, result, i) { - result=value "" - if (length(result) >= width) { - return result - } - i = width - length(result) - while (i-- > 0) { - result = "0" result - } - return result - } - NF==1 { print ++$NF; next } - { - last = $NF + 1 - $NF = pad(last, length($NF)) - print - }')" +set -euo pipefail -printf '%s\n' "${NEXT_VERSION}" > "${VERSION_FILE}" -log "${CURRENT_VERSION} -> ${NEXT_VERSION}" +# update version file(s) here +# e.g.: ./ci/scripts/common/update_version_file.sh -git add "${VERSION_FILE}" -if git diff --cached --quiet; then - log "no changes staged, skipping commit" - exit 0 +if ! git diff --quiet; then + git add . + git commit -m "chore: bump build version [skip ci]" + git push origin HEAD:main fi - -AUTHOR_NAME="${GIT_AUTHOR_NAME:-woodpecker}" -AUTHOR_EMAIL="${GIT_AUTHOR_EMAIL:-ci@sendico.io}" -git config user.name "${AUTHOR_NAME}" -git config user.email "${AUTHOR_EMAIL}" - -git commit -m "chore(ci): bump version to ${NEXT_VERSION}" - -BRANCH="${WOODPECKER_BRANCH:-}" -if [ -z "${BRANCH}" ] || [ "${BRANCH}" = "HEAD" ]; then - BRANCH="$(git rev-parse --abbrev-ref HEAD)" -fi - -REMOTE_URL="${CI_REPO_REMOTE:-${WOODPECKER_GIT_REMOTE:-${DRONE_REMOTE_URL:-}}}" -if [ -z "${REMOTE_URL}" ]; then - REMOTE_URL="$(git config --get remote.origin.url 2>/dev/null || true)" -fi -[ -n "${REMOTE_URL}" ] || die "unable to determine git remote url" - -REMOTE_PROTO="$(extract_proto "${REMOTE_URL}")" -REMOTE_HOST="$(normalize_host "${REMOTE_URL}")" -[ -n "${REMOTE_HOST}" ] || die "unable to determine remote host from ${REMOTE_URL}" - -log "using remote ${REMOTE_URL}" -git remote set-url origin "${REMOTE_URL}" - -case "${REMOTE_PROTO}" in - ssh) - if [ -n "${GIT_SSH_KEY_B64:-}" ]; then - SSH_KEY_FILE="${HOME:-/root}/.ssh/id_ci" - mkdir -p "$(dirname "${SSH_KEY_FILE}")" - printf '%s' "${GIT_SSH_KEY_B64}" | base64 -d > "${SSH_KEY_FILE}" - chmod 600 "${SSH_KEY_FILE}" - export GIT_SSH_COMMAND="ssh -i ${SSH_KEY_FILE} -o StrictHostKeyChecking=no" - log "configured SSH key for git push" - else - log "no SSH key provided, relying on existing agent configuration" - fi - ;; - http|https) - if ! setup_https_credentials "${REMOTE_HOST}"; then - die "no git credentials detected (set CI_GIT_USERNAME/CI_GIT_PASSWORD, WOODPECKER_GIT_USERNAME/WOODPECKER_GIT_PASSWORD, or configure GIT_CREDENTIALS_VAULT_PATH)" - fi - ;; - *) - log "unknown git protocol ${REMOTE_PROTO}, attempting push without extra credentials" - ;; -esac - -git push origin "HEAD:${BRANCH}" diff --git a/infra/vault/docker-compose.yml b/infra/vault/docker-compose.yml new file mode 100644 index 0000000..d4f2a9a --- /dev/null +++ b/infra/vault/docker-compose.yml @@ -0,0 +1,51 @@ +networks: + cicd: + external: true + +volumes: + vault1_data: + vault2_data: + vault3_data: + +services: + vault: + image: hashicorp/vault:latest + cap_add: [ "IPC_LOCK" ] + ulimits: { memlock: { soft: -1, hard: -1 } } + environment: { VAULT_ADDR: "http://127.0.0.1:8200" } + command: vault server -config=/vault/config/vault.hcl + volumes: + - vault1_data:/vault/file + - ./config/vault1.hcl:/vault/config/vault.hcl:ro + networks: [ cicd ] + labels: + - "traefik.enable=true" + - "traefik.docker.network=cicd" + - "traefik.http.routers.vault.rule=Host(`vault.sendico.io`)" + - "traefik.http.routers.vault.entrypoints=websecure" + - "traefik.http.routers.vault.tls.certresolver=letsencrypt" + - "traefik.http.routers.vault.middlewares=secure-headers@file" + - "traefik.http.services.vault.loadbalancer.server.port=8200" + - "traefik.http.services.vault.loadbalancer.server.scheme=http" + + vault2: + image: hashicorp/vault:latest + cap_add: [ "IPC_LOCK" ] + ulimits: { memlock: { soft: -1, hard: -1 } } + environment: { VAULT_ADDR: "http://127.0.0.1:8200" } + command: vault server -config=/vault/config/vault.hcl + volumes: + - vault2_data:/vault/file + - ./config/vault2.hcl:/vault/config/vault.hcl:ro + networks: [ cicd ] + + vault3: + image: hashicorp/vault:latest + cap_add: [ "IPC_LOCK" ] + ulimits: { memlock: { soft: -1, hard: -1 } } + environment: { VAULT_ADDR: "http://127.0.0.1:8200" } + command: vault server -config=/vault/config/vault.hcl + volumes: + - vault3_data:/vault/file + - ./config/vault3.hcl:/vault/config/vault.hcl:ro + networks: [ cicd ] \ No newline at end of file