+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

This commit is contained in:
Stephan D
2025-11-17 18:00:38 +01:00
parent 4c64a8d6e6
commit 1ab7f2e7d3
13 changed files with 496 additions and 68 deletions

View File

@@ -12,16 +12,14 @@ 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,
}, },

View File

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

View File

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

View File

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

View File

@@ -8,4 +8,19 @@ 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"`
} }

View File

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

View File

@@ -20,9 +20,13 @@ func TestSignupRequest_JSONSerialization(t *testing.T) {
}, },
Password: "TestPassword123!", Password: "TestPassword123!",
}, },
Describable: model.Describable{
Name: "Test User", Name: "Test User",
}, },
OrganizationName: "Test Organization", },
Organization: model.Describable{
Name: "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!",
}, },
Describable: model.Describable{
Name: "Test User", Name: "Test User",
}, },
OrganizationName: "Test Organization", },
Organization: model.Describable{
Name: "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!",
}, },
Describable: model.Describable{
Name: "Test 用户 Üser", Name: "Test 用户 Üser",
}, },
OrganizationName: "测试 Organization", },
Organization: model.Describable{
Name: "测试 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)

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

View File

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

View File

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

View File

@@ -65,9 +65,13 @@ func TestSignupRequestSerialization(t *testing.T) {
}, },
Password: "TestPassword123!", Password: "TestPassword123!",
}, },
Describable: model.Describable{
Name: "Test User", Name: "Test User",
}, },
OrganizationName: "Test Organization", },
Organization: model.Describable{
Name: "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!",
}, },
Describable: model.Describable{
Name: "Test User", Name: "Test User",
}, },
OrganizationName: "Test Organization", },
Organization: model.Describable{
Name: "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!",
}, },
Describable: model.Describable{
Name: "Test User", Name: "Test User",
},
} }
t.Run("ToAccount", func(t *testing.T) { t.Run("ToAccount", func(t *testing.T) {

View File

@@ -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!",
}, },
Describable: model.Describable{
Name: "Test User", Name: "Test User",
}, },
OrganizationName: "Test Organization", },
Organization: model.Describable{
Name: "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!",
}, },
Describable: model.Describable{
Name: "Test User", Name: "Test User",
}, },
OrganizationName: "Test Organization", },
Organization: model.Describable{
Name: "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!",
}, },
Describable: model.Describable{
Name: "Test User", 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")
})
}

View File

@@ -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( }) =>
SignupRequest(
account: SignupAccount(
name: name, name: name,
login: login, login: login,
password: password, password: password,
locale: locale, locale: locale,
organizationName: organizationName, ),
organization: DescribableRequest(name: organizationName),
organizationTimeZone: organizationTimeZone, 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);
}