+signup +email availability check
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
This commit is contained in:
@@ -11,17 +11,15 @@ type LoginData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AccountData struct {
|
type AccountData struct {
|
||||||
LoginData `bson:",inline" json:",inline"`
|
LoginData `bson:",inline" json:",inline"`
|
||||||
Name string `bson:"name" json:"name"`
|
Describable `bson:",inline" json:",inline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *AccountData) ToAccount() *Account {
|
func (ad *AccountData) ToAccount() *Account {
|
||||||
return &Account{
|
return &Account{
|
||||||
AccountPublic: AccountPublic{
|
AccountPublic: AccountPublic{
|
||||||
AccountBase: AccountBase{
|
AccountBase: AccountBase{
|
||||||
Describable: Describable{
|
Describable: ad.Describable,
|
||||||
Name: ad.Name,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
UserDataBase: ad.UserDataBase,
|
UserDataBase: ad.UserDataBase,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,6 +74,16 @@ api:
|
|||||||
settings:
|
settings:
|
||||||
root_path: ./storage
|
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:
|
app:
|
||||||
|
|
||||||
database:
|
database:
|
||||||
@@ -94,4 +104,4 @@ database:
|
|||||||
collection_name_env: PERMISSION_COLLECTION
|
collection_name_env: PERMISSION_COLLECTION
|
||||||
database_name_env: MONGO_DATABASE
|
database_name_env: MONGO_DATABASE
|
||||||
timeout_seconds_env: PERMISSION_TIMEOUT
|
timeout_seconds_env: PERMISSION_TIMEOUT
|
||||||
is_filtered_env: PERMISSION_IS_FILTERED
|
is_filtered_env: PERMISSION_IS_FILTERED
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ go 1.25.3
|
|||||||
|
|
||||||
replace github.com/tech/sendico/pkg => ../pkg
|
replace github.com/tech/sendico/pkg => ../pkg
|
||||||
|
|
||||||
|
replace github.com/tech/sendico/chain/gateway => ../chain/gateway
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.6
|
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/config v1.31.18
|
||||||
@@ -13,8 +15,10 @@ require (
|
|||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/go-chi/jwtauth/v5 v5.3.3
|
github.com/go-chi/jwtauth/v5 v5.3.3
|
||||||
github.com/go-chi/metrics v0.1.1
|
github.com/go-chi/metrics v0.1.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/stretchr/testify v1.11.1
|
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/tech/sendico/pkg v0.1.0
|
||||||
github.com/testcontainers/testcontainers-go v0.33.0
|
github.com/testcontainers/testcontainers-go v0.33.0
|
||||||
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0
|
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0
|
||||||
@@ -27,7 +31,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
|
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
|
github.com/casbin/govaluate v1.10.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -70,7 +74,6 @@ require (
|
|||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // 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/klauspost/compress v1.18.1 // indirect
|
||||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 // 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/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
|
|||||||
@@ -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.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 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
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.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
|
||||||
github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
|
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.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 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
|
||||||
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
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 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 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY=
|
||||||
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog=
|
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.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
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.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
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 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
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.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
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-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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
|||||||
@@ -6,6 +6,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Mw *mwa.Config `yaml:"middleware"`
|
Mw *mwa.Config `yaml:"middleware"`
|
||||||
Storage *fsc.Config `yaml:"storage"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import "github.com/tech/sendico/pkg/model"
|
|||||||
|
|
||||||
type Signup struct {
|
type Signup struct {
|
||||||
Account model.AccountData `json:"account"`
|
Account model.AccountData `json:"account"`
|
||||||
OrganizationName string `json:"organizationName"`
|
Organization model.Describable `json:"organization"`
|
||||||
OrganizationTimeZone string `json:"organizationTimeZone"`
|
OrganizationTimeZone string `json:"organizationTimeZone"`
|
||||||
AnonymousUser model.Describable `json:"anonymousUser"`
|
AnonymousUser model.Describable `json:"anonymousUser"`
|
||||||
OwnerRole model.Describable `json:"ownerRole"`
|
OwnerRole model.Describable `json:"ownerRole"`
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ func TestSignupRequest_JSONSerialization(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Password: "TestPassword123!",
|
Password: "TestPassword123!",
|
||||||
},
|
},
|
||||||
Name: "Test User",
|
Describable: model.Describable{
|
||||||
|
Name: "Test User",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Organization: model.Describable{
|
||||||
|
Name: "Test Organization",
|
||||||
},
|
},
|
||||||
OrganizationName: "Test Organization",
|
|
||||||
OrganizationTimeZone: "UTC",
|
OrganizationTimeZone: "UTC",
|
||||||
AnonymousUser: model.Describable{
|
AnonymousUser: model.Describable{
|
||||||
Name: "Anonymous User",
|
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.Name, unmarshaled.Account.Name)
|
||||||
assert.Equal(t, signup.Account.Login, unmarshaled.Account.Login)
|
assert.Equal(t, signup.Account.Login, unmarshaled.Account.Login)
|
||||||
assert.Equal(t, signup.Account.Password, unmarshaled.Account.Password)
|
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.OrganizationTimeZone, unmarshaled.OrganizationTimeZone)
|
||||||
assert.Equal(t, signup.AnonymousUser.Name, unmarshaled.AnonymousUser.Name)
|
assert.Equal(t, signup.AnonymousUser.Name, unmarshaled.AnonymousUser.Name)
|
||||||
assert.Equal(t, signup.OwnerRole.Name, unmarshaled.OwnerRole.Name)
|
assert.Equal(t, signup.OwnerRole.Name, unmarshaled.OwnerRole.Name)
|
||||||
@@ -65,9 +69,13 @@ func TestSignupRequest_MinimalValidRequest(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Password: "TestPassword123!",
|
Password: "TestPassword123!",
|
||||||
},
|
},
|
||||||
Name: "Test User",
|
Describable: model.Describable{
|
||||||
|
Name: "Test User",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Organization: model.Describable{
|
||||||
|
Name: "Test Organization",
|
||||||
},
|
},
|
||||||
OrganizationName: "Test Organization",
|
|
||||||
OrganizationTimeZone: "UTC",
|
OrganizationTimeZone: "UTC",
|
||||||
AnonymousUser: model.Describable{
|
AnonymousUser: model.Describable{
|
||||||
Name: "Anonymous",
|
Name: "Anonymous",
|
||||||
@@ -93,13 +101,13 @@ func TestSignupRequest_MinimalValidRequest(t *testing.T) {
|
|||||||
// Verify minimal request is valid
|
// Verify minimal request is valid
|
||||||
assert.Equal(t, signup.Account.Name, unmarshaled.Account.Name)
|
assert.Equal(t, signup.Account.Name, unmarshaled.Account.Name)
|
||||||
assert.Equal(t, signup.Account.Login, unmarshaled.Account.Login)
|
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) {
|
func TestSignupRequest_InvalidJSON(t *testing.T) {
|
||||||
invalidJSONs := []string{
|
invalidJSONs := []string{
|
||||||
`{"account": invalid}`,
|
`{"account": invalid}`,
|
||||||
`{"organizationName": 123}`,
|
`{"organization": 123}`,
|
||||||
`{"organizationTimeZone": true}`,
|
`{"organizationTimeZone": true}`,
|
||||||
`{"defaultPriorityGroup": "not_an_object"}`,
|
`{"defaultPriorityGroup": "not_an_object"}`,
|
||||||
`{"anonymousUser": []}`,
|
`{"anonymousUser": []}`,
|
||||||
@@ -125,9 +133,13 @@ func TestSignupRequest_UnicodeCharacters(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Password: "TestPassword123!",
|
Password: "TestPassword123!",
|
||||||
},
|
},
|
||||||
Name: "Test 用户 Üser",
|
Describable: model.Describable{
|
||||||
|
Name: "Test 用户 Üser",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Organization: model.Describable{
|
||||||
|
Name: "测试 Organization",
|
||||||
},
|
},
|
||||||
OrganizationName: "测试 Organization",
|
|
||||||
OrganizationTimeZone: "UTC",
|
OrganizationTimeZone: "UTC",
|
||||||
AnonymousUser: model.Describable{
|
AnonymousUser: model.Describable{
|
||||||
Name: "匿名 User",
|
Name: "匿名 User",
|
||||||
@@ -153,7 +165,7 @@ func TestSignupRequest_UnicodeCharacters(t *testing.T) {
|
|||||||
// Verify unicode characters are properly handled
|
// Verify unicode characters are properly handled
|
||||||
assert.Equal(t, "测试@example.com", unmarshaled.Account.Login)
|
assert.Equal(t, "测试@example.com", unmarshaled.Account.Login)
|
||||||
assert.Equal(t, "Test 用户 Üser", unmarshaled.Account.Name)
|
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, "匿名 User", unmarshaled.AnonymousUser.Name)
|
||||||
assert.Equal(t, "所有者", unmarshaled.OwnerRole.Name)
|
assert.Equal(t, "所有者", unmarshaled.OwnerRole.Name)
|
||||||
assert.Equal(t, "匿名", unmarshaled.AnonymousRole.Name)
|
assert.Equal(t, "匿名", unmarshaled.AnonymousRole.Name)
|
||||||
|
|||||||
23
api/server/interface/api/sresponse/signup_availability.go
Normal file
23
api/server/interface/api/sresponse/signup_availability.go
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,12 @@ package accountapiimp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
chaingatewayclient "github.com/tech/sendico/chain/gateway/client"
|
||||||
api "github.com/tech/sendico/pkg/api/http"
|
api "github.com/tech/sendico/pkg/api/http"
|
||||||
"github.com/tech/sendico/pkg/auth"
|
"github.com/tech/sendico/pkg/auth"
|
||||||
"github.com/tech/sendico/pkg/db/account"
|
"github.com/tech/sendico/pkg/db/account"
|
||||||
@@ -14,6 +19,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/messaging"
|
"github.com/tech/sendico/pkg/messaging"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
||||||
"github.com/tech/sendico/server/interface/accountservice"
|
"github.com/tech/sendico/server/interface/accountservice"
|
||||||
eapi "github.com/tech/sendico/server/interface/api"
|
eapi "github.com/tech/sendico/server/interface/api"
|
||||||
"github.com/tech/sendico/server/interface/services/fileservice"
|
"github.com/tech/sendico/server/interface/services/fileservice"
|
||||||
@@ -39,6 +45,13 @@ type AccountAPI struct {
|
|||||||
tph mutil.ParamHelper
|
tph mutil.ParamHelper
|
||||||
accountsPermissionRef primitive.ObjectID
|
accountsPermissionRef primitive.ObjectID
|
||||||
accService accountservice.AccountService
|
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 {
|
func (a *AccountAPI) Name() mservice.Type {
|
||||||
@@ -46,7 +59,15 @@ func (a *AccountAPI) Name() mservice.Type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *AccountAPI) Finish(ctx context.Context) error {
|
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) {
|
func CreateAPI(a eapi.API) (*AccountAPI, error) {
|
||||||
@@ -86,6 +107,7 @@ func CreateAPI(a eapi.API) (*AccountAPI, error) {
|
|||||||
|
|
||||||
// Account related api endpoints
|
// Account related api endpoints
|
||||||
a.Register().Handler(mservice.Accounts, "/signup", api.Post, p.signup)
|
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.Put, p.updateProfile)
|
||||||
a.Register().AccountHandler(mservice.Accounts, "", api.Get, p.getProfile)
|
a.Register().AccountHandler(mservice.Accounts, "", api.Get, p.getProfile)
|
||||||
@@ -120,5 +142,74 @@ func CreateAPI(a eapi.API) (*AccountAPI, error) {
|
|||||||
}
|
}
|
||||||
p.accountsPermissionRef = accountsPolicy.ID
|
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
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/tech/sendico/pkg/api/http/response"
|
"github.com/tech/sendico/pkg/api/http/response"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
"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/srequest"
|
||||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"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) {
|
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 {
|
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()))
|
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,
|
PermissionRef: permissionRef,
|
||||||
},
|
},
|
||||||
Describable: model.Describable{
|
Describable: model.Describable{
|
||||||
Name: sr.OrganizationName,
|
Name: name,
|
||||||
|
Description: sr.Organization.Description,
|
||||||
},
|
},
|
||||||
TimeZone: sr.OrganizationTimeZone,
|
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())
|
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()
|
newAccount := sr.Account.ToAccount()
|
||||||
if res := a.accService.ValidateAccount(newAccount); res != nil {
|
if res := a.accService.ValidateAccount(newAccount); res != nil {
|
||||||
a.logger.Warn("Invalid signup account received", zap.Error(res), zap.String("account", newAccount.Login))
|
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)
|
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 {
|
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) {
|
_, err := a.tf.CreateTransaction().Execute(ctxt, func(ctx context.Context) (any, error) {
|
||||||
return a.signupTransactionBody(ctx, sr, newAccount)
|
return a.signupTransactionBody(ctx, sr, newAccount)
|
||||||
@@ -116,6 +156,10 @@ func (a *AccountAPI) signupTransactionBody(ctx context.Context, sr *srequest.Sig
|
|||||||
return nil, err
|
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)
|
roleDescription, err := a.pmanager.Role().Create(ctx, org.ID, &sr.OwnerRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Warn("Failed to create owner role", zap.Error(err), zap.String("login", newAccount.Login))
|
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
|
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}
|
actions := []model.Action{model.ActionCreate, model.ActionRead, model.ActionUpdate, model.ActionDelete}
|
||||||
for _, policy := range policies {
|
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 {
|
for _, action := range actions {
|
||||||
a.logger.Debug("Adding permission", mzap.StorableRef(&policy), zap.String("action", string(action)),
|
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))
|
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
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,9 +65,13 @@ func TestSignupRequestSerialization(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Password: "TestPassword123!",
|
Password: "TestPassword123!",
|
||||||
},
|
},
|
||||||
Name: "Test User",
|
Describable: model.Describable{
|
||||||
|
Name: "Test User",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Organization: model.Describable{
|
||||||
|
Name: "Test Organization",
|
||||||
},
|
},
|
||||||
OrganizationName: "Test Organization",
|
|
||||||
OrganizationTimeZone: "UTC",
|
OrganizationTimeZone: "UTC",
|
||||||
AnonymousUser: model.Describable{
|
AnonymousUser: model.Describable{
|
||||||
Name: "Anonymous User",
|
Name: "Anonymous User",
|
||||||
@@ -93,7 +97,7 @@ func TestSignupRequestSerialization(t *testing.T) {
|
|||||||
// Verify data integrity
|
// Verify data integrity
|
||||||
assert.Equal(t, signupRequest.Account.Login, retrieved.Account.Login)
|
assert.Equal(t, signupRequest.Account.Login, retrieved.Account.Login)
|
||||||
assert.Equal(t, signupRequest.Account.Name, retrieved.Account.Name)
|
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)
|
assert.Equal(t, signupRequest.OrganizationTimeZone, retrieved.OrganizationTimeZone)
|
||||||
|
|
||||||
})
|
})
|
||||||
@@ -109,9 +113,13 @@ func TestSignupHTTPSerialization(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Password: "TestPassword123!",
|
Password: "TestPassword123!",
|
||||||
},
|
},
|
||||||
Name: "Test User",
|
Describable: model.Describable{
|
||||||
|
Name: "Test User",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Organization: model.Describable{
|
||||||
|
Name: "Test Organization",
|
||||||
},
|
},
|
||||||
OrganizationName: "Test Organization",
|
|
||||||
OrganizationTimeZone: "UTC",
|
OrganizationTimeZone: "UTC",
|
||||||
AnonymousUser: model.Describable{
|
AnonymousUser: model.Describable{
|
||||||
Name: "Anonymous User",
|
Name: "Anonymous User",
|
||||||
@@ -141,13 +149,13 @@ func TestSignupHTTPSerialization(t *testing.T) {
|
|||||||
// Verify parsing
|
// Verify parsing
|
||||||
assert.Equal(t, signupRequest.Account.Login, parsedRequest.Account.Login)
|
assert.Equal(t, signupRequest.Account.Login, parsedRequest.Account.Login)
|
||||||
assert.Equal(t, signupRequest.Account.Name, parsedRequest.Account.Name)
|
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) {
|
t.Run("UnicodeCharacters", func(t *testing.T) {
|
||||||
unicodeRequest := signupRequest
|
unicodeRequest := signupRequest
|
||||||
unicodeRequest.Account.Name = "Test 用户 Üser"
|
unicodeRequest.Account.Name = "Test 用户 Üser"
|
||||||
unicodeRequest.OrganizationName = "测试 Organization"
|
unicodeRequest.Organization.Name = "测试 Organization"
|
||||||
|
|
||||||
// Serialize to JSON
|
// Serialize to JSON
|
||||||
reqBody, err := json.Marshal(unicodeRequest)
|
reqBody, err := json.Marshal(unicodeRequest)
|
||||||
@@ -160,7 +168,7 @@ func TestSignupHTTPSerialization(t *testing.T) {
|
|||||||
|
|
||||||
// Verify unicode characters are preserved
|
// Verify unicode characters are preserved
|
||||||
assert.Equal(t, "Test 用户 Üser", parsedRequest.Account.Name)
|
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) {
|
t.Run("InvalidJSONRequest", func(t *testing.T) {
|
||||||
@@ -184,7 +192,9 @@ func TestAccountDataConversion(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Password: "TestPassword123!",
|
Password: "TestPassword123!",
|
||||||
},
|
},
|
||||||
Name: "Test User",
|
Describable: model.Describable{
|
||||||
|
Name: "Test User",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("ToAccount", func(t *testing.T) {
|
t.Run("ToAccount", func(t *testing.T) {
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
package accountapiimp
|
package accountapiimp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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/pkg/model"
|
||||||
"github.com/tech/sendico/server/interface/api/srequest"
|
"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
|
// Helper function to create string pointers
|
||||||
@@ -60,9 +66,13 @@ func TestCreateValidSignupRequest(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Password: "TestPassword123!",
|
Password: "TestPassword123!",
|
||||||
},
|
},
|
||||||
Name: "Test User",
|
Describable: model.Describable{
|
||||||
|
Name: "Test User",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Organization: model.Describable{
|
||||||
|
Name: "Test Organization",
|
||||||
},
|
},
|
||||||
OrganizationName: "Test Organization",
|
|
||||||
OrganizationTimeZone: "UTC",
|
OrganizationTimeZone: "UTC",
|
||||||
AnonymousUser: model.Describable{
|
AnonymousUser: model.Describable{
|
||||||
Name: "Anonymous User",
|
Name: "Anonymous User",
|
||||||
@@ -79,7 +89,7 @@ func TestCreateValidSignupRequest(t *testing.T) {
|
|||||||
assert.Equal(t, "test@example.com", request.Account.Login)
|
assert.Equal(t, "test@example.com", request.Account.Login)
|
||||||
assert.Equal(t, "TestPassword123!", request.Account.Password)
|
assert.Equal(t, "TestPassword123!", request.Account.Password)
|
||||||
assert.Equal(t, "Test User", request.Account.Name)
|
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)
|
assert.Equal(t, "UTC", request.OrganizationTimeZone)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,9 +104,13 @@ func TestSignupRequestValidation(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Password: "TestPassword123!",
|
Password: "TestPassword123!",
|
||||||
},
|
},
|
||||||
Name: "Test User",
|
Describable: model.Describable{
|
||||||
|
Name: "Test User",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Organization: model.Describable{
|
||||||
|
Name: "Test Organization",
|
||||||
},
|
},
|
||||||
OrganizationName: "Test Organization",
|
|
||||||
OrganizationTimeZone: "UTC",
|
OrganizationTimeZone: "UTC",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +118,7 @@ func TestSignupRequestValidation(t *testing.T) {
|
|||||||
assert.NotEmpty(t, request.Account.Login)
|
assert.NotEmpty(t, request.Account.Login)
|
||||||
assert.NotEmpty(t, request.Account.Password)
|
assert.NotEmpty(t, request.Account.Password)
|
||||||
assert.NotEmpty(t, request.Account.Name)
|
assert.NotEmpty(t, request.Account.Name)
|
||||||
assert.NotEmpty(t, request.OrganizationName)
|
assert.NotEmpty(t, request.Organization.Name)
|
||||||
assert.NotEmpty(t, request.OrganizationTimeZone)
|
assert.NotEmpty(t, request.OrganizationTimeZone)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -205,7 +219,9 @@ func TestAccountDataToAccount(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Password: "TestPassword123!",
|
Password: "TestPassword123!",
|
||||||
},
|
},
|
||||||
Name: "Test User",
|
Describable: model.Describable{
|
||||||
|
Name: "Test User",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
account := accountData.ToAccount()
|
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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,20 +5,20 @@ part 'signup.g.dart';
|
|||||||
|
|
||||||
@JsonSerializable(explicitToJson: true)
|
@JsonSerializable(explicitToJson: true)
|
||||||
class SignupRequest {
|
class SignupRequest {
|
||||||
final String name;
|
final SignupAccount account;
|
||||||
final String login;
|
final DescribableRequest organization;
|
||||||
final String password;
|
|
||||||
final String locale;
|
|
||||||
final String organizationName;
|
|
||||||
final String organizationTimeZone;
|
final String organizationTimeZone;
|
||||||
|
final DescribableRequest anonymousUser;
|
||||||
|
final DescribableRequest ownerRole;
|
||||||
|
final DescribableRequest anonymousRole;
|
||||||
|
|
||||||
const SignupRequest({
|
const SignupRequest({
|
||||||
required this.name,
|
required this.account,
|
||||||
required this.login,
|
required this.organization,
|
||||||
required this.password,
|
|
||||||
required this.locale,
|
|
||||||
required this.organizationName,
|
|
||||||
required this.organizationTimeZone,
|
required this.organizationTimeZone,
|
||||||
|
required this.anonymousUser,
|
||||||
|
required this.ownerRole,
|
||||||
|
required this.anonymousRole,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory SignupRequest.build({
|
factory SignupRequest.build({
|
||||||
@@ -28,15 +28,55 @@ class SignupRequest {
|
|||||||
required String locale,
|
required String locale,
|
||||||
required String organizationName,
|
required String organizationName,
|
||||||
required String organizationTimeZone,
|
required String organizationTimeZone,
|
||||||
}) => SignupRequest(
|
}) =>
|
||||||
name: name,
|
SignupRequest(
|
||||||
login: login,
|
account: SignupAccount(
|
||||||
password: password,
|
name: name,
|
||||||
locale: locale,
|
login: login,
|
||||||
organizationName: organizationName,
|
password: password,
|
||||||
organizationTimeZone: organizationTimeZone,
|
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<String, dynamic> json) => _$SignupRequestFromJson(json);
|
factory SignupRequest.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SignupRequestFromJson(json);
|
||||||
Map<String, dynamic> toJson() => _$SignupRequestToJson(this);
|
Map<String, dynamic> 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<String, dynamic> json) =>
|
||||||
|
_$SignupAccountFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$SignupAccountToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class DescribableRequest {
|
||||||
|
final String name;
|
||||||
|
final String? description;
|
||||||
|
|
||||||
|
const DescribableRequest({required this.name, this.description});
|
||||||
|
|
||||||
|
factory DescribableRequest.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$DescribableRequestFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$DescribableRequestToJson(this);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user