+signup +login
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats 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
ci/woodpecker/push/bump_version Pipeline failed

This commit is contained in:
Stephan D
2025-11-17 20:16:45 +01:00
parent 1ab7f2e7d3
commit c6a56071b5
89 changed files with 1308 additions and 3497 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -22,9 +22,9 @@ require (
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251117160429-c598d23eddcf // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.3 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.134.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

View File

@@ -8,12 +8,16 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f h1:B/TfTw73mVqWKDzJZhU9Qi9wQyYfmiCz9FnmpQsyv5M= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f h1:B/TfTw73mVqWKDzJZhU9Qi9wQyYfmiCz9FnmpQsyv5M=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251110112254-48a6e677648f/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251117160429-c598d23eddcf h1:aZI2VRIP0LAI6Rw934WEAxxL0SNYSVt9vR9h/cP5Pbo=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251117160429-c598d23eddcf/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y= github.com/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y=
github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.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=

View File

@@ -13,6 +13,7 @@ type AccountBase struct {
storable.Base `bson:",inline" json:",inline"` storable.Base `bson:",inline" json:",inline"`
ArchivableBase `bson:",inline" json:",inline"` ArchivableBase `bson:",inline" json:",inline"`
Describable `bson:",inline" json:",inline"` Describable `bson:",inline" json:",inline"`
LastName string `bson:"lastName" json:"lastName"`
AvatarURL *string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"` AvatarURL *string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"`
} }

View File

@@ -13,6 +13,7 @@ type LoginData struct {
type AccountData struct { type AccountData struct {
LoginData `bson:",inline" json:",inline"` LoginData `bson:",inline" json:",inline"`
Describable `bson:",inline" json:",inline"` Describable `bson:",inline" json:",inline"`
LastName string `bson:"lastName" json:"lastName"`
} }
func (ad *AccountData) ToAccount() *Account { func (ad *AccountData) ToAccount() *Account {

View File

@@ -8,9 +8,9 @@ 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.20
github.com/aws/aws-sdk-go-v2/credentials v1.18.22 github.com/aws/aws-sdk-go-v2/credentials v1.18.24
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2
github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/chi/v5 v5.2.3
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
@@ -49,9 +49,9 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 // indirect
github.com/aws/smithy-go v1.23.2 // indirect github.com/aws/smithy-go v1.23.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect

View File

@@ -12,8 +12,12 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
github.com/aws/aws-sdk-go-v2/config v1.31.18 h1:RouG3AcF2fLFhw+Z0qbnuIl9HZ0Kh4E/U9sKwTMRpMI= github.com/aws/aws-sdk-go-v2/config v1.31.18 h1:RouG3AcF2fLFhw+Z0qbnuIl9HZ0Kh4E/U9sKwTMRpMI=
github.com/aws/aws-sdk-go-v2/config v1.31.18/go.mod h1:aXZ13mSQC8S2VEHwGfL1COMuJ1Zty6pX5xU7hyqjvCg= github.com/aws/aws-sdk-go-v2/config v1.31.18/go.mod h1:aXZ13mSQC8S2VEHwGfL1COMuJ1Zty6pX5xU7hyqjvCg=
github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc=
github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0=
github.com/aws/aws-sdk-go-v2/credentials v1.18.22 h1:hyIVGBHhQPaNP9D4BaVRwpjLMCwMMdAkHqB3gGMiykU= github.com/aws/aws-sdk-go-v2/credentials v1.18.22 h1:hyIVGBHhQPaNP9D4BaVRwpjLMCwMMdAkHqB3gGMiykU=
github.com/aws/aws-sdk-go-v2/credentials v1.18.22/go.mod h1:B9E2qHs3/YGfeQZ4jrIE/nPvqxtyafZrJ5EQiZBG6pk= github.com/aws/aws-sdk-go-v2/credentials v1.18.22/go.mod h1:B9E2qHs3/YGfeQZ4jrIE/nPvqxtyafZrJ5EQiZBG6pk=
github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg=
github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=
@@ -34,12 +38,20 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1a
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8= github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw= github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2 h1:DhdbtDl4FdNlj31+xiRXANxEE+eC7n8JQz+/ilwQ8Uc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 h1:ZGDJVmlpPFiNFCb/I42nYVKUanJAdFUiSmUo/32AqPQ= github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 h1:ZGDJVmlpPFiNFCb/I42nYVKUanJAdFUiSmUo/32AqPQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= github.com/aws/aws-sdk-go-v2/service/sts v1.40.0/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0=
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=

View File

@@ -6,7 +6,5 @@ type Signup struct {
Account model.AccountData `json:"account"` Account model.AccountData `json:"account"`
Organization model.Describable `json:"organization"` Organization model.Describable `json:"organization"`
OrganizationTimeZone string `json:"organizationTimeZone"` OrganizationTimeZone string `json:"organizationTimeZone"`
AnonymousUser model.Describable `json:"anonymousUser"`
OwnerRole model.Describable `json:"ownerRole"` OwnerRole model.Describable `json:"ownerRole"`
AnonymousRole model.Describable `json:"anonymousRole"`
} }

View File

@@ -28,15 +28,9 @@ func TestSignupRequest_JSONSerialization(t *testing.T) {
Name: "Test Organization", Name: "Test Organization",
}, },
OrganizationTimeZone: "UTC", OrganizationTimeZone: "UTC",
AnonymousUser: model.Describable{
Name: "Anonymous User",
},
OwnerRole: model.Describable{ OwnerRole: model.Describable{
Name: "Owner", Name: "Owner",
}, },
AnonymousRole: model.Describable{
Name: "Anonymous",
},
} }
// Test JSON marshaling // Test JSON marshaling
@@ -55,9 +49,7 @@ func TestSignupRequest_JSONSerialization(t *testing.T) {
assert.Equal(t, signup.Account.Password, unmarshaled.Account.Password) assert.Equal(t, signup.Account.Password, unmarshaled.Account.Password)
assert.Equal(t, signup.Organization.Name, unmarshaled.Organization.Name) 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.OwnerRole.Name, unmarshaled.OwnerRole.Name) assert.Equal(t, signup.OwnerRole.Name, unmarshaled.OwnerRole.Name)
assert.Equal(t, signup.AnonymousRole.Name, unmarshaled.AnonymousRole.Name)
} }
func TestSignupRequest_MinimalValidRequest(t *testing.T) { func TestSignupRequest_MinimalValidRequest(t *testing.T) {
@@ -77,15 +69,9 @@ func TestSignupRequest_MinimalValidRequest(t *testing.T) {
Name: "Test Organization", Name: "Test Organization",
}, },
OrganizationTimeZone: "UTC", OrganizationTimeZone: "UTC",
AnonymousUser: model.Describable{
Name: "Anonymous",
},
OwnerRole: model.Describable{ OwnerRole: model.Describable{
Name: "Owner", Name: "Owner",
}, },
AnonymousRole: model.Describable{
Name: "Anonymous",
},
} }
// Test JSON marshaling // Test JSON marshaling
@@ -141,15 +127,9 @@ func TestSignupRequest_UnicodeCharacters(t *testing.T) {
Name: "测试 Organization", Name: "测试 Organization",
}, },
OrganizationTimeZone: "UTC", OrganizationTimeZone: "UTC",
AnonymousUser: model.Describable{
Name: "匿名 User",
},
OwnerRole: model.Describable{ OwnerRole: model.Describable{
Name: "所有者", Name: "所有者",
}, },
AnonymousRole: model.Describable{
Name: "匿名",
},
} }
// Test JSON marshaling // Test JSON marshaling
@@ -166,7 +146,5 @@ func TestSignupRequest_UnicodeCharacters(t *testing.T) {
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.Organization.Name) assert.Equal(t, "测试 Organization", unmarshaled.Organization.Name)
assert.Equal(t, "匿名 User", unmarshaled.AnonymousUser.Name)
assert.Equal(t, "所有者", unmarshaled.OwnerRole.Name) assert.Equal(t, "所有者", unmarshaled.OwnerRole.Name)
assert.Equal(t, "匿名", unmarshaled.AnonymousRole.Name)
} }

View File

@@ -22,27 +22,6 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
func (a *AccountAPI) createAnonymousAccount(ctx context.Context, org *model.Organization, sr *srequest.Signup) error {
anonymousUser := &model.Account{
AccountPublic: model.AccountPublic{
AccountBase: model.AccountBase{
Describable: sr.AnonymousUser,
},
UserDataBase: sr.Account.UserDataBase,
},
}
r, err := a.pmanager.Role().Create(ctx, org.ID, &sr.AnonymousRole)
if err != nil {
a.logger.Warn("Failed to create anonymous role", zap.Error(err))
return err
}
if err := a.accService.CreateAccount(ctx, org, anonymousUser, r.ID); err != nil {
a.logger.Warn("Failed to create account", zap.Error(err), zap.String("login", anonymousUser.Login))
return err
}
return nil
}
func (a *AccountAPI) createOrg(ctx context.Context, sr *srequest.Signup, permissionRef primitive.ObjectID) (*model.Organization, error) { func (a *AccountAPI) createOrg(ctx context.Context, sr *srequest.Signup, permissionRef primitive.ObjectID) (*model.Organization, error) {
name := strings.TrimSpace(sr.Organization.Name) name := strings.TrimSpace(sr.Organization.Name)
if name == "" { if name == "" {
@@ -175,10 +154,6 @@ func (a *AccountAPI) signupTransactionBody(ctx context.Context, sr *srequest.Sig
return nil, err return nil, err
} }
if err := a.createAnonymousAccount(ctx, org, sr); err != nil {
return nil, err
}
return nil, nil return nil, nil
} }

View File

@@ -73,15 +73,9 @@ func TestSignupRequestSerialization(t *testing.T) {
Name: "Test Organization", Name: "Test Organization",
}, },
OrganizationTimeZone: "UTC", OrganizationTimeZone: "UTC",
AnonymousUser: model.Describable{
Name: "Anonymous User",
},
OwnerRole: model.Describable{ OwnerRole: model.Describable{
Name: "Owner", Name: "Owner",
}, },
AnonymousRole: model.Describable{
Name: "Anonymous",
},
} }
// Store in MongoDB // Store in MongoDB
@@ -121,15 +115,9 @@ func TestSignupHTTPSerialization(t *testing.T) {
Name: "Test Organization", Name: "Test Organization",
}, },
OrganizationTimeZone: "UTC", OrganizationTimeZone: "UTC",
AnonymousUser: model.Describable{
Name: "Anonymous User",
},
OwnerRole: model.Describable{ OwnerRole: model.Describable{
Name: "Owner", Name: "Owner",
}, },
AnonymousRole: model.Describable{
Name: "Anonymous",
},
} }
t.Run("ValidJSONRequest", func(t *testing.T) { t.Run("ValidJSONRequest", func(t *testing.T) {

View File

@@ -74,15 +74,9 @@ func TestCreateValidSignupRequest(t *testing.T) {
Name: "Test Organization", Name: "Test Organization",
}, },
OrganizationTimeZone: "UTC", OrganizationTimeZone: "UTC",
AnonymousUser: model.Describable{
Name: "Anonymous User",
},
OwnerRole: model.Describable{ OwnerRole: model.Describable{
Name: "Owner", Name: "Owner",
}, },
AnonymousRole: model.Describable{
Name: "Anonymous",
},
} }
// Validate the request structure // Validate the request structure

View File

@@ -21,9 +21,21 @@ fi
CURRENT_VERSION="$(cat "${VERSION_FILE}")" CURRENT_VERSION="$(cat "${VERSION_FILE}")"
NEXT_VERSION="$(printf '%s' "${CURRENT_VERSION}" | awk -F. -v OFS=. ' NEXT_VERSION="$(printf '%s' "${CURRENT_VERSION}" | awk -F. -v OFS=. '
function pad(value, width, result, i) {
result=value ""
if (length(result) >= width) {
return result
}
i = width - length(result)
while (i-- > 0) {
result = "0" result
}
return result
}
NF==1 { print ++$NF; next } NF==1 { print ++$NF; next }
{ {
$NF=sprintf("%0*d", length($NF), ($NF+1)) last = $NF + 1
$NF = pad(last, length($NF))
print print
}')" }')"

BIN
frontend/.DS_Store vendored Normal file

Binary file not shown.

BIN
frontend/pshared/lib/.DS_Store vendored Normal file

Binary file not shown.

BIN
frontend/pshared/lib/api/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1,2 +1,22 @@
class AuthorizationFailed implements Exception { class AuthenticationFailedException implements Exception {
final String message;
final Exception? originalError;
const AuthenticationFailedException(this.message, [this.originalError]);
@override
String toString() {
return 'AuthenticationFailedException: $message${originalError != null ? ' (caused by: $originalError)' : ''}';
}
}
class CircuitBreakerOpenException implements Exception {
final String message;
const CircuitBreakerOpenException(this.message);
@override
String toString() {
return 'CircuitBreakerOpenException: $message';
}
} }

Binary file not shown.

View File

@@ -1,20 +1,18 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/api/requests/login_data.dart';
part 'login.g.dart'; part 'login.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class LoginRequest { class LoginRequest {
final String login; final LoginData login;
final String password;
final String locale;
final String clientId; final String clientId;
final String deviceId; final String deviceId;
const LoginRequest({ const LoginRequest({
required this.login, required this.login,
required this.password,
required this.locale,
required this.clientId, required this.clientId,
required this.deviceId, required this.deviceId,
}); });

View File

@@ -0,0 +1,76 @@
import 'package:json_annotation/json_annotation.dart';
part 'login_data.g.dart';
@JsonSerializable(explicitToJson: true, constructor: 'build')
class LoginData {
final String login;
final String password;
final String locale;
const LoginData._({
required this.login,
required this.password,
required this.locale,
});
factory LoginData.build({
required String login,
required String password,
required String locale,
}) => LoginData._(
login: login.trim().toLowerCase(),
password: password,
locale: locale,
);
factory LoginData.fromJson(Map<String, dynamic> json) => _$LoginDataFromJson(json);
Map<String, dynamic> toJson() => _$LoginDataToJson(this);
}
@JsonSerializable(explicitToJson: true, constructor: 'buildIstance')
class AccountData extends LoginData {
final String name;
final String lastName;
const AccountData._({
required super.login,
required super.password,
required super.locale,
required this.name,
required this.lastName,
}) : super._();
factory AccountData.buildIstance({
required String login,
required String password,
required String locale,
required String name,
required String lastName,
}) => AccountData._(
login: login,
password: password,
locale: locale,
name: name.trim(),
lastName: lastName.trim(),
);
factory AccountData.build({
required LoginData login,
required String name,
required String lastName,
}) => AccountData.buildIstance(
login: login.login,
password: login.password,
locale: login.locale,
name: name,
lastName: lastName,
);
factory AccountData.fromJson(Map<String, dynamic> json) => _$AccountDataFromJson(json);
@override
Map<String, dynamic> toJson() => _$AccountDataToJson(this);
}

View File

@@ -1,6 +1,6 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'change_password.g.dart'; part 'change.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)

View File

@@ -0,0 +1,20 @@
import 'package:json_annotation/json_annotation.dart';
part 'forgot.g.dart';
@JsonSerializable()
class ForgotPasswordRequest {
final String login;
const ForgotPasswordRequest({
required this.login,
});
factory ForgotPasswordRequest.fromJson(Map<String, dynamic> json) => _$ForgotPasswordRequestFromJson(json);
Map<String, dynamic> toJson() => _$ForgotPasswordRequestToJson(this);
static ForgotPasswordRequest build({
required String login,
}) => ForgotPasswordRequest(login: login);
}

View File

@@ -0,0 +1,20 @@
import 'package:json_annotation/json_annotation.dart';
part 'reset.g.dart';
@JsonSerializable()
class ResetPasswordRequest {
final String password;
const ResetPasswordRequest({
required this.password,
});
factory ResetPasswordRequest.fromJson(Map<String, dynamic> json) => _$ResetPasswordRequestFromJson(json);
Map<String, dynamic> toJson() => _$ResetPasswordRequestToJson(this);
static ResetPasswordRequest build({
required String password,
}) => ResetPasswordRequest(password: password);
}

View File

@@ -1,82 +1,39 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/api/requests/login_data.dart';
import 'package:pshared/data/dto/describable.dart';
import 'package:pshared/data/mapper/describable.dart';
import 'package:pshared/models/describable.dart';
part 'signup.g.dart'; part 'signup.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class SignupRequest { class SignupRequest {
final SignupAccount account; final AccountData account;
final DescribableRequest organization; final DescribableDTO organization;
final String organizationTimeZone; final String organizationTimeZone;
final DescribableRequest anonymousUser; final DescribableDTO ownerRole;
final DescribableRequest ownerRole;
final DescribableRequest anonymousRole;
const SignupRequest({ const SignupRequest({
required this.account, required this.account,
required this.organization, required this.organization,
required this.organizationTimeZone, required this.organizationTimeZone,
required this.anonymousUser,
required this.ownerRole, required this.ownerRole,
required this.anonymousRole,
}); });
factory SignupRequest.build({ factory SignupRequest.build({
required String name, required AccountData account,
required String login, required Describable organization,
required String password,
required String locale,
required String organizationName,
required String organizationTimeZone, required String organizationTimeZone,
}) => required Describable ownerRole,
SignupRequest( }) => SignupRequest(
account: SignupAccount( account: account,
name: name, organization: organization.toDTO(),
login: login, organizationTimeZone: organizationTimeZone,
password: password, ownerRole: ownerRole.toDTO(),
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) => factory SignupRequest.fromJson(Map<String, dynamic> json) => _$SignupRequestFromJson(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);
}

View File

@@ -1,6 +1,7 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/account/base.dart'; import 'package:pshared/data/dto/account/base.dart';
import 'package:pshared/data/dto/date_time.dart';
part 'account.g.dart'; part 'account.g.dart';
@@ -8,14 +9,17 @@ part 'account.g.dart';
@JsonSerializable() @JsonSerializable()
class AccountDTO extends AccountBaseDTO { class AccountDTO extends AccountBaseDTO {
final String login; final String login;
final String locale;
const AccountDTO({ const AccountDTO({
required super.id, required super.id,
required super.createdAt, required super.createdAt,
required super.updatedAt, required super.updatedAt,
required super.name, required super.name,
required super.lastName,
required super.description,
required super.avatarUrl, required super.avatarUrl,
required super.locale, required this.locale,
required this.login, required this.login,
}); });

View File

@@ -1,6 +1,7 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/storable.dart'; import 'package:pshared/data/dto/storable.dart';
import 'package:pshared/data/dto/date_time.dart';
part 'base.g.dart'; part 'base.g.dart';
@@ -8,7 +9,8 @@ part 'base.g.dart';
@JsonSerializable() @JsonSerializable()
class AccountBaseDTO extends StorableDTO { class AccountBaseDTO extends StorableDTO {
final String name; final String name;
final String locale; final String lastName;
final String? description;
final String? avatarUrl; final String? avatarUrl;
const AccountBaseDTO({ const AccountBaseDTO({
@@ -16,8 +18,9 @@ class AccountBaseDTO extends StorableDTO {
required super.createdAt, required super.createdAt,
required super.updatedAt, required super.updatedAt,
required this.name, required this.name,
required this.description,
required this.avatarUrl, required this.avatarUrl,
required this.locale, required this.lastName,
}); });
factory AccountBaseDTO.fromJson(Map<String, dynamic> json) => _$AccountBaseDTOFromJson(json); factory AccountBaseDTO.fromJson(Map<String, dynamic> json) => _$AccountBaseDTOFromJson(json);

View File

@@ -0,0 +1,12 @@
import 'package:json_annotation/json_annotation.dart';
class UtcIso8601Converter implements JsonConverter<DateTime, String> {
const UtcIso8601Converter();
@override
DateTime fromJson(String json) => DateTime.parse(json).toUtc();
@override
String toJson(DateTime value) => value.toUtc().toIso8601String();
}

View File

@@ -0,0 +1,18 @@
import 'package:json_annotation/json_annotation.dart';
part 'describable.g.dart';
@JsonSerializable()
class DescribableDTO {
final String name;
final String? description;
const DescribableDTO({
required this.name,
this.description,
});
factory DescribableDTO.fromJson(Map<String, dynamic> json) => _$DescribableDTOFromJson(json);
Map<String, dynamic> toJson() => _$DescribableDTOToJson(this);
}

View File

@@ -1,18 +1,28 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/storable.dart';
import 'package:pshared/data/dto/date_time.dart';
import 'package:pshared/data/dto/permissions/bound.dart';
part 'organization.g.dart'; part 'organization.g.dart';
@JsonSerializable() @JsonSerializable()
class OrganizationDTO extends StorableDTO { class OrganizationDTO extends PermissionBoundDTO {
final String name;
final String? description;
final String timeZone; final String timeZone;
final String? logoUrl; final String? logoUrl;
final String tenantRef;
const OrganizationDTO({ const OrganizationDTO({
required super.id, required super.id,
required super.createdAt, required super.createdAt,
required super.updatedAt, required super.updatedAt,
required super.permissionRef,
required super.organizationRef,
required this.name,
required this.tenantRef,
this.description,
required this.timeZone, required this.timeZone,
this.logoUrl, this.logoUrl,
}); });

View File

@@ -0,0 +1,23 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/storable.dart';
import 'package:pshared/data/dto/date_time.dart';
part 'bound.g.dart';
@JsonSerializable()
class OrganizationBoundDTO extends StorableDTO {
final String organizationRef;
const OrganizationBoundDTO({
required super.id,
required super.createdAt,
required super.updatedAt,
required this.organizationRef,
});
factory OrganizationBoundDTO.fromJson(Map<String, dynamic> json) => _$OrganizationBoundDTOFromJson(json);
@override
Map<String, dynamic> toJson() => _$OrganizationBoundDTOToJson(this);
}

View File

@@ -1,13 +1,17 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/describable.dart';
part 'description.g.dart'; part 'description.g.dart';
@JsonSerializable() @JsonSerializable()
class OrganizationDescriptionDTO { class OrganizationDescriptionDTO {
final DescribableDTO description;
final String? logoUrl; final String? logoUrl;
const OrganizationDescriptionDTO({ const OrganizationDescriptionDTO({
required this.description,
this.logoUrl, this.logoUrl,
}); });

View File

@@ -0,0 +1,26 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/date_time.dart';
import 'package:pshared/data/dto/storable.dart';
part 'bound.g.dart';
@JsonSerializable()
class PermissionBoundDTO extends StorableDTO {
final String permissionRef;
final String organizationRef;
const PermissionBoundDTO({
required super.id,
required super.createdAt,
required super.updatedAt,
required this.permissionRef,
required this.organizationRef,
});
factory PermissionBoundDTO.fromJson(Map<String, dynamic> json) => _$PermissionBoundDTOFromJson(json);
@override
Map<String, dynamic> toJson() => _$PermissionBoundDTOToJson(this);
}

View File

@@ -1,12 +1,14 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/storable.dart';
import 'package:pshared/data/dto/storable/describable.dart';
import 'package:pshared/data/dto/date_time.dart';
import 'package:pshared/models/resources.dart'; import 'package:pshared/models/resources.dart';
part 'policy.g.dart'; part 'policy.g.dart';
@JsonSerializable() @JsonSerializable()
class PolicyDescriptionDTO extends StorableDTO { class PolicyDescriptionDTO extends StorableDescribabaleDTO {
final List<ResourceType>? resourceTypes; final List<ResourceType>? resourceTypes;
final String? organizationRef; final String? organizationRef;
@@ -14,6 +16,8 @@ class PolicyDescriptionDTO extends StorableDTO {
required super.id, required super.id,
required super.createdAt, required super.createdAt,
required super.updatedAt, required super.updatedAt,
required super.name,
required super.description,
required this.resourceTypes, required this.resourceTypes,
required this.organizationRef, required this.organizationRef,
}); });

View File

@@ -1,17 +1,21 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/storable.dart';
import 'package:pshared/data/dto/date_time.dart';
import 'package:pshared/data/dto/storable/describable.dart';
part 'role.g.dart'; part 'role.g.dart';
@JsonSerializable() @JsonSerializable()
class RoleDescriptionDTO extends StorableDTO { class RoleDescriptionDTO extends StorableDescribabaleDTO {
final String organizationRef; final String organizationRef;
const RoleDescriptionDTO({ const RoleDescriptionDTO({
required super.id, required super.id,
required super.createdAt, required super.createdAt,
required super.updatedAt, required super.updatedAt,
required super.name,
required super.description,
required this.organizationRef, required this.organizationRef,
}); });

View File

@@ -0,0 +1,16 @@
import 'package:json_annotation/json_annotation.dart';
part 'reference.g.dart';
@JsonSerializable()
class ReferenceDTO {
final String ref;
const ReferenceDTO({
required this.ref,
});
factory ReferenceDTO.fromJson(Map<String, dynamic> json) => _$ReferenceDTOFromJson(json);
Map<String, dynamic> toJson() => _$ReferenceDTOToJson(this);
}

View File

@@ -1,12 +1,18 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/date_time.dart';
part 'storable.g.dart'; part 'storable.g.dart';
@JsonSerializable() @JsonSerializable()
class StorableDTO { class StorableDTO {
final String id; final String id;
@UtcIso8601Converter()
final DateTime createdAt; final DateTime createdAt;
@UtcIso8601Converter()
final DateTime updatedAt; final DateTime updatedAt;
const StorableDTO({ const StorableDTO({

View File

@@ -0,0 +1,26 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/date_time.dart';
import 'package:pshared/data/dto/storable.dart';
part 'describable.g.dart';
@JsonSerializable()
class StorableDescribabaleDTO extends StorableDTO {
final String name;
final String? description;
const StorableDescribabaleDTO({
required super.id,
required super.createdAt,
required super.updatedAt,
required this.name,
this.description,
});
factory StorableDescribabaleDTO.fromJson(Map<String, dynamic> json) => _$StorableDescribabaleDTOFromJson(json);
@override
Map<String, dynamic> toJson() => _$StorableDescribabaleDTOToJson(this);
}

View File

@@ -1,5 +1,6 @@
import 'package:pshared/data/dto/account/account.dart'; import 'package:pshared/data/dto/account/account.dart';
import 'package:pshared/models/account/account.dart'; import 'package:pshared/models/account/account.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';
@@ -9,6 +10,8 @@ extension AccountMapper on Account {
createdAt: createdAt, createdAt: createdAt,
updatedAt: updatedAt, updatedAt: updatedAt,
name: name, name: name,
lastName: lastName,
description: description,
avatarUrl: avatarUrl, avatarUrl: avatarUrl,
locale: locale, locale: locale,
login: login, login: login,
@@ -19,8 +22,9 @@ extension AccountDTOMapper on AccountDTO {
Account toDomain() => Account( Account toDomain() => Account(
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
avatarUrl: avatarUrl, avatarUrl: avatarUrl,
describable: newDescribable(name: name, description: description),
lastName: lastName,
locale: locale, locale: locale,
login: login, login: login,
name: name,
); );
} }

View File

@@ -1,5 +1,6 @@
import 'package:pshared/data/dto/account/base.dart'; import 'package:pshared/data/dto/account/base.dart';
import 'package:pshared/models/account/base.dart'; import 'package:pshared/models/account/base.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';
@@ -8,17 +9,18 @@ extension AccountBaseMapper on AccountBase {
id: storable.id, id: storable.id,
createdAt: storable.createdAt, createdAt: storable.createdAt,
updatedAt: storable.updatedAt, updatedAt: storable.updatedAt,
name: describable.name,
description: describable.description,
lastName: lastName,
avatarUrl: avatarUrl, avatarUrl: avatarUrl,
name: name,
locale: locale,
); );
} }
extension AccountDTOMapper on AccountBaseDTO { extension AccountDTOMapper on AccountBaseDTO {
AccountBase toDomain() => AccountBase( AccountBase toDomain() => AccountBase(
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
describable: newDescribable(name: name, description: description),
lastName: lastName,
avatarUrl: avatarUrl, avatarUrl: avatarUrl,
name: name,
locale: locale,
); );
} }

View File

@@ -0,0 +1,17 @@
import 'package:pshared/data/dto/describable.dart';
import 'package:pshared/models/describable.dart';
extension DescribableMapper on Describable {
DescribableDTO toDTO() => DescribableDTO(
name: name,
description: description,
);
}
extension DescribableDTOMapper on DescribableDTO {
Describable toDomain() => newDescribable(
name: name,
description: description,
);
}

View File

@@ -1,5 +1,8 @@
import 'package:pshared/data/dto/organization.dart'; import 'package:pshared/data/dto/organization.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/organization/bound.dart';
import 'package:pshared/models/organization/organization.dart'; import 'package:pshared/models/organization/organization.dart';
import 'package:pshared/models/permissions/bound.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';
@@ -8,15 +11,26 @@ extension OrganizationMapper on Organization {
id: storable.id, id: storable.id,
createdAt: storable.createdAt, createdAt: storable.createdAt,
updatedAt: storable.updatedAt, updatedAt: storable.updatedAt,
name: describable.name,
description: describable.description,
timeZone: timeZone, timeZone: timeZone,
logoUrl: logoUrl, logoUrl: logoUrl,
organizationRef: permissionBound.organizationRef,
permissionRef: permissionBound.permissionRef,
tenantRef: tenantRef,
); );
} }
extension OrganizationDTOMapper on OrganizationDTO { extension OrganizationDTOMapper on OrganizationDTO {
Organization toDomain() => Organization( Organization toDomain() => Organization(
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
describable: newDescribable(name: name, description: description),
timeZone: timeZone, timeZone: timeZone,
logoUrl: logoUrl, logoUrl: logoUrl,
permissionBound: newPermissionBound(
organizationBound: newOrganizationBound(organizationRef: organizationRef),
permissionRef: permissionRef,
),
tenantRef: tenantRef,
); );
} }

View File

@@ -0,0 +1,18 @@
import 'package:pshared/data/dto/organization/bound.dart';
import 'package:pshared/models/organization/bound.dart';
extension OrganizationBoundMapper on OrganizationBound {
OrganizationBoundDTO toDTO() => OrganizationBoundDTO(
id: '', // OrganizationBound doesn't have storable fields, so we need to provide defaults
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
organizationRef: organizationRef,
);
}
extension OrganizationBoundDTOMapper on OrganizationBoundDTO {
OrganizationBound toDomain() => newOrganizationBound(
organizationRef: organizationRef,
);
}

View File

@@ -1,9 +1,11 @@
import 'package:pshared/data/dto/organization/description.dart'; import 'package:pshared/data/dto/organization/description.dart';
import 'package:pshared/data/mapper/describable.dart';
import 'package:pshared/models/organization/description.dart'; import 'package:pshared/models/organization/description.dart';
extension OrganizationDescriptionMapper on OrganizationDescription { extension OrganizationDescriptionMapper on OrganizationDescription {
OrganizationDescriptionDTO toDTO() => OrganizationDescriptionDTO( OrganizationDescriptionDTO toDTO() => OrganizationDescriptionDTO(
description: description.toDTO(),
logoUrl: logoUrl, logoUrl: logoUrl,
); );
} }
@@ -11,5 +13,6 @@ extension OrganizationDescriptionMapper on OrganizationDescription {
extension AccountDescriptionDTOMapper on OrganizationDescriptionDTO { extension AccountDescriptionDTOMapper on OrganizationDescriptionDTO {
OrganizationDescription toDomain() => OrganizationDescription( OrganizationDescription toDomain() => OrganizationDescription(
logoUrl: logoUrl, logoUrl: logoUrl,
description: description.toDomain(),
); );
} }

View File

@@ -1,4 +1,5 @@
import 'package:pshared/data/dto/permissions/description/policy.dart'; import 'package:pshared/data/dto/permissions/description/policy.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/permissions/descriptions/policy.dart'; import 'package:pshared/models/permissions/descriptions/policy.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';
@@ -8,6 +9,8 @@ extension PolicyDescriptionMapper on PolicyDescription {
id: storable.id, id: storable.id,
createdAt: storable.createdAt, createdAt: storable.createdAt,
updatedAt: storable.updatedAt, updatedAt: storable.updatedAt,
name: describable.name,
description: describable.description,
resourceTypes: resourceTypes, resourceTypes: resourceTypes,
organizationRef: organizationRef, organizationRef: organizationRef,
); );
@@ -16,6 +19,7 @@ extension PolicyDescriptionMapper on PolicyDescription {
extension PolicyDescriptionDTOMapper on PolicyDescriptionDTO { extension PolicyDescriptionDTOMapper on PolicyDescriptionDTO {
PolicyDescription toDomain() => PolicyDescription( PolicyDescription toDomain() => PolicyDescription(
storable: newStorable(id: id, createdAt: createdAt, updatedAt: createdAt), storable: newStorable(id: id, createdAt: createdAt, updatedAt: createdAt),
describable: newDescribable(name: name, description: description),
resourceTypes: resourceTypes, resourceTypes: resourceTypes,
organizationRef: organizationRef, organizationRef: organizationRef,
); );

View File

@@ -1,4 +1,5 @@
import 'package:pshared/data/dto/permissions/description/role.dart'; import 'package:pshared/data/dto/permissions/description/role.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/permissions/descriptions/role.dart'; import 'package:pshared/models/permissions/descriptions/role.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';
@@ -8,6 +9,8 @@ extension RoleDescriptionMapper on RoleDescription {
id: storable.id, id: storable.id,
createdAt: storable.createdAt, createdAt: storable.createdAt,
updatedAt: storable.updatedAt, updatedAt: storable.updatedAt,
name: describable.name,
description: describable.description,
organizationRef: organizationRef, organizationRef: organizationRef,
); );
} }
@@ -15,6 +18,7 @@ extension RoleDescriptionMapper on RoleDescription {
extension RoleDescriptionDTOMapper on RoleDescriptionDTO { extension RoleDescriptionDTOMapper on RoleDescriptionDTO {
RoleDescription toDomain() => RoleDescription( RoleDescription toDomain() => RoleDescription(
storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt),
describable: newDescribable(name: name, description: description),
organizationRef: organizationRef, organizationRef: organizationRef,
); );
} }

View File

@@ -0,0 +1,11 @@
import 'package:pshared/data/dto/reference.dart';
import 'package:pshared/models/reference.dart';
extension ReferenceMapper on Reference {
ReferenceDTO toDTO() => ReferenceDTO(ref: ref);
}
extension ReferenceDTOMapper on ReferenceDTO {
Reference toDomain() => newReference(ref: ref);
}

View File

@@ -3,7 +3,7 @@ import 'package:pshared/models/storable.dart';
extension StorableMapper on Storable { extension StorableMapper on Storable {
StorableDTO toDTO() => StorableDTO(id: id, createdAt: createdAt, updatedAt: updatedAt); StorableDTO toDTO() => StorableDTO(id: id, createdAt: createdAt.toUtc(), updatedAt: updatedAt.toUtc());
} }
extension StorableDTOMapper on StorableDTO { extension StorableDTOMapper on StorableDTO {

BIN
frontend/pshared/lib/models/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1,36 +1,35 @@
import 'package:flutter/foundation.dart';
import 'package:pshared/models/account/base.dart'; import 'package:pshared/models/account/base.dart';
import 'package:pshared/models/describable.dart';
@immutable
class Account extends AccountBase { class Account extends AccountBase {
final String login; final String login;
final String locale;
const Account({ const Account({
required super.storable, required super.storable,
required super.describable,
required super.avatarUrl, required super.avatarUrl,
required super.lastName,
required this.login, required this.login,
required super.locale, required this.locale,
required super.name,
}); });
factory Account.fromBase(AccountBase accountBase, String login) => Account(
storable: accountBase.storable,
avatarUrl: accountBase.avatarUrl,
locale: accountBase.locale,
name: accountBase.name,
login: login,
);
@override @override
Account copyWith({ Account copyWith({
Describable? describable,
String? lastName,
String? Function()? avatarUrl, String? Function()? avatarUrl,
String? name,
String? locale, String? locale,
}) { }) => Account(
final updatedBase = super.copyWith( storable: storable,
avatarUrl: avatarUrl, describable: describableCopyWithOther(this.describable, describable),
name: name, lastName: lastName ?? this.lastName,
locale: locale, avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl,
); login: login,
return Account.fromBase(updatedBase, login); locale: locale ?? this.locale,
} );
} }

View File

@@ -1,8 +1,16 @@
import 'package:flutter/foundation.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';
import 'package:pshared/models/storable/describable.dart';
import 'package:pshared/utils/name_initials.dart';
class AccountBase implements Storable { @immutable
class AccountBase implements StorableDescribable {
final Storable storable; final Storable storable;
final Describable describable;
final String lastName;
@override @override
String get id => storable.id; String get id => storable.id;
@@ -10,26 +18,30 @@ class AccountBase implements Storable {
DateTime get createdAt => storable.createdAt; DateTime get createdAt => storable.createdAt;
@override @override
DateTime get updatedAt => storable.updatedAt; DateTime get updatedAt => storable.updatedAt;
@override
String get name => describable.name;
@override
String? get description => describable.description;
final String? avatarUrl; final String? avatarUrl;
final String name;
final String locale;
const AccountBase({ const AccountBase({
required this.storable, required this.storable,
required this.name, required this.describable,
required this.locale,
required this.avatarUrl, required this.avatarUrl,
required this.lastName,
}); });
String get nameInitials => getNameInitials(describable.name);
AccountBase copyWith({ AccountBase copyWith({
Describable? describable,
String? lastName,
String? Function()? avatarUrl, String? Function()? avatarUrl,
String? name,
String? locale,
}) => AccountBase( }) => AccountBase(
storable: storable, storable: storable,
avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl, avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl,
locale: locale ?? this.locale, describable: describable ?? this.describable,
name: name ?? this.name, lastName: lastName ?? this.lastName,
); );
} }

View File

@@ -0,0 +1,39 @@
import 'package:flutter/foundation.dart';
abstract class Describable {
String get name;
String? get description;
}
@immutable
class _DescribableImp implements Describable {
@override
final String name;
@override
final String? description;
const _DescribableImp({
required this.name,
required this.description,
});
}
Describable newDescribable({required String name, String? description}) =>
_DescribableImp(name: name, description: description);
extension DescribableCopier on Describable {
Describable copyWith({
String? name,
String? Function()? description,
}) => newDescribable(
name: name ?? this.name,
description: description != null ? description() : this.description,
);
}
Describable describableCopyWithOther(Describable current, Describable? other) => current.copyWith(
name: other?.name,
description: () => other?.description,
);

View File

@@ -0,0 +1,18 @@
import 'package:flutter/foundation.dart';
abstract class OrganizationBound {
String get organizationRef;
}
@immutable
class _OrganizationBoundImp implements OrganizationBound {
@override
final String organizationRef;
const _OrganizationBoundImp({
required this.organizationRef,
});
}
OrganizationBound newOrganizationBound({ required String organizationRef }) => _OrganizationBoundImp(organizationRef: organizationRef);

View File

@@ -1,7 +1,12 @@
import 'package:pshared/models/describable.dart';
class OrganizationDescription { class OrganizationDescription {
final Describable description;
final String? logoUrl; final String? logoUrl;
const OrganizationDescription({ const OrganizationDescription({
required this.description,
this.logoUrl, this.logoUrl,
}); });
} }

View File

@@ -1,8 +1,13 @@
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/permissions/bound.dart';
import 'package:pshared/models/permissions/bound/describable.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';
class Organization implements Storable { class Organization implements PermissionBoundStorableDescribable {
final Storable storable; final Storable storable;
final PermissionBound permissionBound;
final Describable describable;
@override @override
String get id => storable.id; String get id => storable.id;
@@ -10,25 +15,39 @@ class Organization implements Storable {
DateTime get createdAt => storable.createdAt; DateTime get createdAt => storable.createdAt;
@override @override
DateTime get updatedAt => storable.updatedAt; DateTime get updatedAt => storable.updatedAt;
@override
String get organizationRef => permissionBound.organizationRef;
@override
String get permissionRef => permissionBound.permissionRef;
@override
String get name => describable.name;
@override
String? get description => describable.description;
final String timeZone; final String timeZone;
final String? logoUrl; final String? logoUrl;
final String tenantRef;
const Organization({ const Organization({
required this.storable, required this.storable,
required this.describable,
required this.timeZone, required this.timeZone,
required this.permissionBound,
required this.tenantRef,
this.logoUrl, this.logoUrl,
}); });
Organization copyWith({ Organization copyWith({
String? name, Describable? describable,
String? Function()? description,
String? timeZone, String? timeZone,
String? Function()? logoUrl, String? Function()? logoUrl,
}) => Organization( }) => Organization(
storable: storable, // Same Storable, same id storable: storable, // Same Storable, same id
describable: describableCopyWithOther(this.describable, describable),
timeZone: timeZone ?? this.timeZone, timeZone: timeZone ?? this.timeZone,
logoUrl: logoUrl != null ? logoUrl() : this.logoUrl, logoUrl: logoUrl != null ? logoUrl() : this.logoUrl,
permissionBound: permissionBound,
tenantRef: tenantRef,
); );
} }

View File

@@ -1,22 +0,0 @@
import 'package:pshared/config/constants.dart';
abstract class PermissionBound {
String get permissionRef;
String get organizationRef;
}
class _PermissionBoundImp implements PermissionBound {
@override
final String permissionRef;
@override
final String organizationRef;
const _PermissionBoundImp({
required this.permissionRef,
required this.organizationRef,
});
}
PermissionBound newPermissionBound({ required String organizationRef, String? permissionRef}) =>
_PermissionBoundImp(permissionRef: permissionRef ?? Constants.nilObjectRef, organizationRef: organizationRef);

View File

@@ -0,0 +1,32 @@
import 'package:flutter/foundation.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/models/organization/bound.dart';
abstract class PermissionBound extends OrganizationBound {
String get permissionRef;
}
@immutable
class _PermissionBoundImp implements PermissionBound {
@override
final String permissionRef;
final OrganizationBound organizationBound;
@override
get organizationRef => organizationBound.organizationRef;
const _PermissionBoundImp({
required this.permissionRef,
required this.organizationBound,
});
}
PermissionBound newPermissionBound({
required OrganizationBound organizationBound,
String? permissionRef,
}) => _PermissionBoundImp(
permissionRef: permissionRef ?? Constants.nilObjectRef,
organizationBound: organizationBound,
);

View File

@@ -0,0 +1,6 @@
import 'package:pshared/models/permissions/bound/storable.dart';
import 'package:pshared/models/storable/describable.dart';
abstract class PermissionBoundStorableDescribable implements PermissionBoundStorable, StorableDescribable {
}

View File

@@ -1,4 +1,4 @@
import 'package:pshared/models/permission_bound.dart'; import 'package:pshared/models/permissions/bound.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';

View File

@@ -1,9 +1,12 @@
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/resources.dart'; import 'package:pshared/models/resources.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';
import 'package:pshared/models/storable/describable.dart';
class PolicyDescription implements Storable { class PolicyDescription implements StorableDescribable {
final Storable storable; final Storable storable;
final Describable describable;
final List<ResourceType>? resourceTypes; final List<ResourceType>? resourceTypes;
final String? organizationRef; final String? organizationRef;
@@ -13,9 +16,14 @@ class PolicyDescription implements Storable {
DateTime get createdAt => storable.createdAt; DateTime get createdAt => storable.createdAt;
@override @override
DateTime get updatedAt => storable.updatedAt; DateTime get updatedAt => storable.updatedAt;
@override
String get name => describable.name;
@override
String? get description => describable.description;
const PolicyDescription({ const PolicyDescription({
required this.storable, required this.storable,
required this.describable,
required this.resourceTypes, required this.resourceTypes,
required this.organizationRef, required this.organizationRef,
}); });

View File

@@ -1,8 +1,11 @@
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/storable.dart'; import 'package:pshared/models/storable.dart';
import 'package:pshared/models/storable/describable.dart';
class RoleDescription implements Storable { class RoleDescription implements StorableDescribable {
final Storable storable; final Storable storable;
final Describable describable;
@override @override
String get id => storable.id; String get id => storable.id;
@@ -10,18 +13,25 @@ class RoleDescription implements Storable {
DateTime get createdAt => storable.createdAt; DateTime get createdAt => storable.createdAt;
@override @override
DateTime get updatedAt => storable.updatedAt; DateTime get updatedAt => storable.updatedAt;
@override
String get name => describable.name;
@override
String? get description => describable.description;
final String organizationRef; final String organizationRef;
const RoleDescription({ const RoleDescription({
required this.storable, required this.storable,
required this.describable,
required this.organizationRef, required this.organizationRef,
}); });
factory RoleDescription.build({ factory RoleDescription.build({
required Describable roleDescription,
required String organizationRef, required String organizationRef,
}) => RoleDescription( }) => RoleDescription(
storable: newStorable(), storable: newStorable(),
describable: roleDescription,
organizationRef: organizationRef organizationRef: organizationRef
); );
} }

View File

@@ -0,0 +1,22 @@
abstract class Reference {
String get ref;
}
class _ReferenceImp implements Reference {
@override
final String ref;
const _ReferenceImp({
required this.ref,
});
}
Reference newReference({required String ref}) => _ReferenceImp(ref: ref);
extension ReferenceCopier on Reference {
Reference copyWith({
String? ref,
}) => newReference(
ref: ref ?? this.ref,
);
}

View File

@@ -1,4 +1,6 @@
import 'package:pshared/config/constants.dart'; import 'package:flutter/foundation.dart';
import 'package:pshared/config/web.dart';
abstract class Storable { abstract class Storable {
@@ -7,6 +9,7 @@ abstract class Storable {
DateTime get updatedAt; DateTime get updatedAt;
} }
@immutable
class _StorableImp implements Storable { class _StorableImp implements Storable {
@override @override
final String id; final String id;

Binary file not shown.

View File

@@ -0,0 +1,6 @@
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/storable.dart';
abstract class StorableDescribable implements Storable, Describable {
}

View File

@@ -4,21 +4,48 @@ import 'package:share_plus/share_plus.dart';
import 'package:pshared/api/errors/unauthorized.dart'; import 'package:pshared/api/errors/unauthorized.dart';
import 'package:pshared/api/requests/signup.dart'; import 'package:pshared/api/requests/signup.dart';
import 'package:pshared/api/requests/login_data.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/models/account/account.dart'; import 'package:pshared/models/account/account.dart';
import 'package:pshared/provider/exception.dart'; import 'package:pshared/models/describable.dart';
import 'package:pshared/models/storable.dart';
import 'package:pshared/provider/locale.dart';
import 'package:pshared/provider/resource.dart'; import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/account.dart'; import 'package:pshared/service/account.dart';
import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/utils/exception.dart';
class AccountProvider extends ChangeNotifier { class AccountProvider extends ChangeNotifier {
static String get currentUserRef => Constants.nilObjectRef;
// The resource now wraps our Account? state along with its loading/error state. // The resource now wraps our Account? state along with its loading/error state.
Resource<Account?> _resource = Resource(data: null); Resource<Account?> _resource = Resource(data: null);
Resource<Account?> get resource => _resource; Resource<Account?> get resource => _resource;
late LocaleProvider _localeProvider;
Account? get account => _resource.data; Account? get account => _resource.data;
bool get isLoggedIn => account != null; bool get isLoggedIn => account != null;
bool get isLoading => _resource.isLoading; bool get isLoading => _resource.isLoading;
Object? get error => _resource.error; Object? get error => _resource.error;
bool get isReady => (!isLoading) && (account != null);
Account? currentUser() {
final acc = account;
if (acc == null) return null;
return Account(
storable: newStorable(
id: currentUserRef,
createdAt: acc.createdAt,
updatedAt: acc.updatedAt,
),
describable: acc.describable,
lastName: acc.lastName,
avatarUrl: acc.avatarUrl,
login: acc.login,
locale: acc.locale,
);
}
// Private helper to update the resource and notify listeners. // Private helper to update the resource and notify listeners.
void _setResource(Resource<Account?> newResource) { void _setResource(Resource<Account?> newResource) {
@@ -26,16 +53,24 @@ class AccountProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void updateProvider(LocaleProvider localeProvider) => _localeProvider = localeProvider;
Future<Account?> login({ void _pickupLocale(String locale) => _localeProvider.setLocale(Locale(locale));
Future<Account> login({
required String email, required String email,
required String password, required String password,
required String locale, required String locale,
}) async { }) async {
_setResource(_resource.copyWith(isLoading: true, error: null)); _setResource(_resource.copyWith(isLoading: true, error: null));
try { try {
final acc = await AccountService.login(email, password, locale); final acc = await AccountService.login(LoginData.build(
login: email,
password: password,
locale: locale,
));
_setResource(Resource(data: acc, isLoading: false)); _setResource(Resource(data: acc, isLoading: false));
_pickupLocale(acc.locale);
return acc; return acc;
} catch (e) { } catch (e) {
_setResource(_resource.copyWith(isLoading: false, error: toException(e))); _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
@@ -43,11 +78,14 @@ class AccountProvider extends ChangeNotifier {
} }
} }
Future<bool> isAuthorizationStored() async => AuthorizationService.isAuthorizationStored();
Future<Account?> restore() async { Future<Account?> restore() async {
_setResource(_resource.copyWith(isLoading: true, error: null)); _setResource(_resource.copyWith(isLoading: true, error: null));
try { try {
final acc = await AccountService.restore(); final acc = await AccountService.restore();
_setResource(Resource(data: acc, isLoading: false)); _setResource(Resource(data: acc, isLoading: false));
_pickupLocale(acc.locale);
return acc; return acc;
} catch (e) { } catch (e) {
_setResource(_resource.copyWith(isLoading: false, error: toException(e))); _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
@@ -55,24 +93,20 @@ class AccountProvider extends ChangeNotifier {
} }
} }
Future<void> signup( Future<void> signup({
String name, required AccountData account,
String login, required Describable organization,
String password, required String timezone,
String locale, required Describable ownerRole,
String organizationName, }) async {
String timezone,
) async {
_setResource(_resource.copyWith(isLoading: true, error: null)); _setResource(_resource.copyWith(isLoading: true, error: null));
try { try {
await AccountService.signup( await AccountService.signup(
SignupRequest.build( SignupRequest.build(
name: name, account: account,
login: login.trim().toLowerCase(), organization: organization,
password: password,
locale: locale,
organizationName: organizationName,
organizationTimeZone: timezone, organizationTimeZone: timezone,
ownerRole: ownerRole,
), ),
); );
// Signup might not automatically log in the user, // Signup might not automatically log in the user,
@@ -96,6 +130,7 @@ class AccountProvider extends ChangeNotifier {
} }
Future<Account?> update({ Future<Account?> update({
Describable? describable,
String? locale, String? locale,
String? avatarUrl, String? avatarUrl,
String? notificationFrequency, String? notificationFrequency,
@@ -105,6 +140,7 @@ class AccountProvider extends ChangeNotifier {
try { try {
final updated = await AccountService.update( final updated = await AccountService.update(
account!.copyWith( account!.copyWith(
describable: describable,
avatarUrl: () => avatarUrl ?? account!.avatarUrl, avatarUrl: () => avatarUrl ?? account!.avatarUrl,
locale: locale ?? account!.locale, locale: locale ?? account!.locale,
), ),
@@ -141,4 +177,26 @@ class AccountProvider extends ChangeNotifier {
rethrow; rethrow;
} }
} }
Future<void> forgotPassword(String email) async {
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
await AccountService.forgotPassword(email);
_setResource(_resource.copyWith(isLoading: false));
} catch (e) {
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
rethrow;
}
}
Future<void> resetPassword(String accountId, String token, String newPassword) async {
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
await AccountService.resetPassword(accountId, token, newPassword);
_setResource(_resource.copyWith(isLoading: false));
} catch (e) {
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
rethrow;
}
}
} }

View File

@@ -5,9 +5,9 @@ import 'package:collection/collection.dart';
import 'package:pshared/config/constants.dart'; import 'package:pshared/config/constants.dart';
import 'package:pshared/models/organization/organization.dart'; import 'package:pshared/models/organization/organization.dart';
import 'package:pshared/provider/resource.dart'; import 'package:pshared/provider/resource.dart';
import 'package:pshared/provider/exception.dart';
import 'package:pshared/service/organization.dart'; import 'package:pshared/service/organization.dart';
import 'package:pshared/service/secure_storage.dart'; import 'package:pshared/service/secure_storage.dart';
import 'package:pshared/utils/exception.dart';
class OrganizationsProvider extends ChangeNotifier { class OrganizationsProvider extends ChangeNotifier {

View File

@@ -1,51 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pshared/service/pfe/service.dart';
import 'package:pshared/provider/exception.dart';
import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/account.dart';
class PfeProvider extends ChangeNotifier {
// The resource now wraps our Account? state along with its loading/error state.
Resource<String?> _resource = Resource(data: null);
Resource<String?> get resource => _resource;
String? get session => _resource.data;
bool get isLoggedIn => session != null;
bool get isLoading => _resource.isLoading;
Object? get error => _resource.error;
// Private helper to update the resource and notify listeners.
void _setResource(Resource<String?> newResource) {
_resource = newResource;
notifyListeners();
}
Future<String?> login({
required String email,
required String password,
}) async {
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
final acc = await PfeService.login(email, password);
_setResource(Resource(data: acc, isLoading: false));
return acc;
} catch (e) {
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
rethrow;
}
}
Future<void> logout() async {
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
await AccountService.logout();
_setResource(Resource(data: null, isLoading: false));
} catch (e) {
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
rethrow;
}
}
}

View File

@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:pshared/models/permission_bound_storable.dart'; import 'package:pshared/models/permissions/bound/storable.dart';
import 'package:pshared/provider/exception.dart';
import 'package:pshared/provider/resource.dart'; import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/template.dart'; import 'package:pshared/service/template.dart';
import 'package:pshared/utils/exception.dart';
List<T> mergeLists<T>({ List<T> mergeLists<T>({

View File

@@ -4,7 +4,10 @@ import 'package:share_plus/share_plus.dart';
import 'package:pshared/api/requests/signup.dart'; import 'package:pshared/api/requests/signup.dart';
import 'package:pshared/api/responses/account.dart'; import 'package:pshared/api/responses/account.dart';
import 'package:pshared/api/requests/change_password.dart'; import 'package:pshared/api/requests/login_data.dart';
import 'package:pshared/api/requests/password/change.dart';
import 'package:pshared/api/requests/password/forgot.dart';
import 'package:pshared/api/requests/password/reset.dart';
import 'package:pshared/data/mapper/account/account.dart'; import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/models/account/account.dart'; import 'package:pshared/models/account/account.dart';
import 'package:pshared/service/authorization/service.dart'; import 'package:pshared/service/authorization/service.dart';
@@ -17,9 +20,9 @@ class AccountService {
static final _logger = Logger('service.account'); static final _logger = Logger('service.account');
static const String _objectType = Services.account; static const String _objectType = Services.account;
static Future<Account> login(String email, String password, String locale) async { static Future<Account> login(LoginData login) async {
_logger.fine('Logging in'); _logger.fine('Logging in');
return AuthorizationService.login(_objectType, email, password, locale); return AuthorizationService.login(_objectType, login);
} }
static Future<Account> restore() async { static Future<Account> restore() async {
@@ -27,6 +30,7 @@ class AccountService {
} }
static Future<void> signup(SignupRequest request) async { static Future<void> signup(SignupRequest request) async {
// Use regular HTTP for public signup endpoint (no auth needed)
await getPOSTResponse(_objectType, 'signup', request.toJson()); await getPOSTResponse(_objectType, 'signup', request.toJson());
} }
@@ -42,9 +46,20 @@ class AccountService {
static Future<Account> update(Account account) async { static Future<Account> update(Account account) async {
_logger.fine('Patching account ${account.id}'); _logger.fine('Patching account ${account.id}');
// Use AuthorizationService for authenticated operations
return _getAccount(AuthorizationService.getPUTResponse(_objectType, '', account.toDTO().toJson())); return _getAccount(AuthorizationService.getPUTResponse(_objectType, '', account.toDTO().toJson()));
} }
static Future<void> forgotPassword(String email) async {
_logger.fine('Requesting password reset for email: $email');
await getPUTResponse(_objectType, 'password', ForgotPasswordRequest.build(login: email).toJson());
}
static Future<void> resetPassword(String accountRef, String token, String newPassword) async {
_logger.fine('Resetting password for account: $accountRef');
await getPOSTResponse(_objectType, 'password/reset/$accountRef/$token', ResetPasswordRequest.build(password: newPassword).toJson());
}
static Future<Account> changePassword(String oldPassword, String newPassword) async { static Future<Account> changePassword(String oldPassword, String newPassword) async {
_logger.fine('Changing password'); _logger.fine('Changing password');
return _getAccount(AuthorizationService.getPATCHResponse( return _getAccount(AuthorizationService.getPATCHResponse(

View File

@@ -0,0 +1,92 @@
import 'package:logging/logging.dart';
/// Circuit breaker pattern implementation for authentication service failures
class AuthCircuitBreaker {
static final _logger = Logger('service.auth_circuit_breaker');
static int _failureCount = 0;
static DateTime? _lastFailure;
static const int _failureThreshold = 3;
static const Duration _recoveryTime = Duration(minutes: 5);
/// Returns true if the circuit breaker is open (blocking operations)
static bool get isOpen {
if (_failureCount < _failureThreshold) return false;
if (_lastFailure == null) return false;
final isOpen = DateTime.now().difference(_lastFailure!) < _recoveryTime;
if (isOpen) {
_logger.warning('Circuit breaker is OPEN. Failure count: $_failureCount, last failure: $_lastFailure');
}
return isOpen;
}
/// Returns true if the circuit breaker is in half-open state (allowing test requests)
static bool get isHalfOpen {
if (_failureCount < _failureThreshold) return false;
if (_lastFailure == null) return false;
return DateTime.now().difference(_lastFailure!) >= _recoveryTime;
}
/// Executes an operation with circuit breaker protection
static Future<T> execute<T>(Future<T> Function() operation) async {
if (isOpen) {
final timeSinceFailure = _lastFailure != null
? DateTime.now().difference(_lastFailure!)
: Duration.zero;
final timeUntilRecovery = _recoveryTime - timeSinceFailure;
_logger.warning('Circuit breaker blocking operation. Recovery in: ${timeUntilRecovery.inSeconds}s');
throw Exception('Auth service temporarily unavailable. Try again in ${timeUntilRecovery.inMinutes} minutes.');
}
try {
_logger.fine('Executing operation through circuit breaker');
final result = await operation();
_reset();
return result;
} catch (e) {
_recordFailure();
rethrow;
}
}
/// Records a failure and updates the circuit breaker state
static void _recordFailure() {
_failureCount++;
_lastFailure = DateTime.now();
_logger.warning('Auth circuit breaker recorded failure #$_failureCount at $_lastFailure');
if (_failureCount >= _failureThreshold) {
_logger.severe('Auth circuit breaker OPENED after $_failureCount failures');
}
}
/// Resets the circuit breaker to closed state
static void _reset() {
if (_failureCount > 0) {
_logger.info('Auth circuit breaker CLOSED - resetting failure count from $_failureCount to 0');
}
_failureCount = 0;
_lastFailure = null;
}
/// Manual reset (for testing or administrative purposes)
static void manualReset() {
_logger.info('Auth circuit breaker manually reset');
_reset();
}
/// Get current status for debugging
static Map<String, dynamic> getStatus() {
return {
'failureCount': _failureCount,
'lastFailure': _lastFailure?.toIso8601String(),
'isOpen': isOpen,
'isHalfOpen': isHalfOpen,
'threshold': _failureThreshold,
'recoveryTime': _recoveryTime.inSeconds,
};
}
}

View File

@@ -0,0 +1,87 @@
import 'dart:math';
import 'package:logging/logging.dart';
import 'package:pshared/utils/exception.dart';
class RetryHelper {
static final _logger = Logger('auth.retry');
/// Executes an operation with exponential backoff retry logic
static Future<T> withExponentialBackoff<T>(
Future<T> Function() operation, {
int maxRetries = 3,
Duration initialDelay = const Duration(milliseconds: 500),
double backoffMultiplier = 2.0,
Duration maxDelay = const Duration(seconds: 30),
bool Function(Exception)? shouldRetry,
}) async {
Exception? lastException;
// Total attempts = initial attempt + maxRetries
final totalAttempts = maxRetries + 1;
for (int attempt = 1; attempt <= totalAttempts; attempt++) {
try {
_logger.fine('Attempting operation (attempt $attempt/$totalAttempts)');
return await operation();
} catch (e) {
lastException = toException(e);
// Don't retry if we've reached max attempts
if (attempt == totalAttempts) {
_logger.warning('Operation failed after $totalAttempts attempts: $lastException');
rethrow;
}
// Check if we should retry this specific error
if (shouldRetry != null && !shouldRetry(lastException)) {
_logger.fine('Operation failed with non-retryable error: $lastException');
rethrow;
}
// Calculate delay with exponential backoff
final delayMs = min(
initialDelay.inMilliseconds * pow(backoffMultiplier, attempt - 1).toInt(),
maxDelay.inMilliseconds,
);
final delay = Duration(milliseconds: delayMs);
_logger.fine('Operation failed (attempt $attempt), retrying in ${delay.inMilliseconds}ms: $lastException');
await Future.delayed(delay);
}
}
// This should never be reached due to rethrow above, but just in case
throw lastException ?? Exception('Retry logic error');
}
/// Determines if an error is retryable (network/temporary errors)
static bool isRetryableError(Exception error) {
final errorString = error.toString().toLowerCase();
// Network connectivity issues
if (errorString.contains('socket') ||
errorString.contains('connection') ||
errorString.contains('timeout') ||
errorString.contains('network')) {
return true;
}
// Server temporary errors (5xx)
if (errorString.contains('500') ||
errorString.contains('502') ||
errorString.contains('503') ||
errorString.contains('504')) {
return true;
}
// Rate limiting
if (errorString.contains('429')) {
return true;
}
return false;
}
}

View File

@@ -1,20 +1,38 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:pshared/api/errors/upload_failed.dart'; import 'package:pshared/api/errors/authorization_failed.dart';
import 'package:pshared/api/requests/login.dart'; import 'package:pshared/api/requests/login.dart';
import 'package:pshared/api/requests/login_data.dart';
import 'package:pshared/api/responses/account.dart'; import 'package:pshared/api/responses/account.dart';
import 'package:pshared/api/responses/login.dart'; import 'package:pshared/api/responses/login.dart';
import 'package:pshared/config/constants.dart'; import 'package:pshared/config/web.dart';
import 'package:pshared/data/mapper/account/account.dart'; import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/models/account/account.dart'; import 'package:pshared/models/account/account.dart';
import 'package:pshared/service/authorization/circuit_breaker.dart';
import 'package:pshared/service/authorization/retry_helper.dart';
import 'package:pshared/service/authorization/storage.dart'; import 'package:pshared/service/authorization/storage.dart';
import 'package:pshared/service/authorization/token.dart'; import 'package:pshared/service/authorization/token.dart';
import 'package:pshared/service/device_id.dart'; import 'package:pshared/service/device_id.dart';
import 'package:pshared/utils/exception.dart';
import 'package:pshared/utils/http/requests.dart' as httpr; import 'package:pshared/utils/http/requests.dart' as httpr;
/// AuthorizationService provides centralized authorization management
/// with token refresh, retry logic, and circuit breaker patterns
class AuthorizationService { class AuthorizationService {
static final _logger = Logger('service.authorization'); static final _logger = Logger('service.authorization.auth_service');
static Future<Account> login(String service, LoginData login) async {
_logger.fine('Logging in ${login.login} with ${login.locale} locale');
final deviceId = await DeviceIdManager.getDeviceId();
final response = await httpr.getPOSTResponse(
service,
'/login',
LoginRequest(login: login, deviceId: deviceId, clientId: Constants.clientId).toJson(),
);
return (await _completeLogin(response)).account.toDomain();
}
static Future<void> _updateAccessToken(AccountResponse response) async { static Future<void> _updateAccessToken(AccountResponse response) async {
await AuthorizationStorage.updateToken(response.accessToken); await AuthorizationStorage.updateToken(response.accessToken);
@@ -31,59 +49,86 @@ class AuthorizationService {
return lr; return lr;
} }
static Future<Account> login(String service, String email, String password, String locale) async {
_logger.fine('Logging in $email with $locale locale');
final deviceId = await DeviceIdManager.getDeviceId();
final response = await httpr.getPOSTResponse(
service,
'/login',
LoginRequest(
login: email.toLowerCase(),
password: password,
locale: locale,
deviceId: deviceId,
clientId: Constants.clientId,
).toJson());
return (await _completeLogin(response)).account.toDomain();
}
static Future<Account> restore() async { static Future<Account> restore() async {
return (await TokenService.rotateRefreshToken()).account.toDomain(); return (await TokenService.refreshAccessToken()).account.toDomain();
} }
static Future<void> logout() async { static Future<void> logout() async {
return AuthorizationStorage.removeTokens(); return AuthorizationStorage.removeTokens();
} }
static Future<Map<String, dynamic>> _authenticatedRequest( // Original AuthorizationService methods - keeping the interface unchanged
String service,
String url,
Future<Map<String, dynamic>> Function(String, String, Map<String, dynamic>, {String? authToken}) requestType,
{Map<String, dynamic>? body}) async {
final accessToken = await TokenService.getAccessToken();
return requestType(service, url, body ?? {}, authToken: accessToken);
}
static Future<Map<String, dynamic>> getPOSTResponse(String service, String url, Map<String, dynamic> body) async => _authenticatedRequest(service, url, httpr.getPOSTResponse, body: body);
static Future<Map<String, dynamic>> getGETResponse(String service, String url) async { static Future<Map<String, dynamic>> getGETResponse(String service, String url) async {
final accessToken = await TokenService.getAccessToken(); final token = await TokenService.getAccessTokenSafe();
return httpr.getGETResponse(service, url, authToken: accessToken); return httpr.getGETResponse(service, url, authToken: token);
} }
static Future<Map<String, dynamic>> getPUTResponse(String service, String url, Map<String, dynamic> body) async => _authenticatedRequest(service, url, httpr.getPUTResponse, body: body); static Future<Map<String, dynamic>> getPOSTResponse(String service, String url, Map<String, dynamic> body) async {
final token = await TokenService.getAccessTokenSafe();
return httpr.getPOSTResponse(service, url, body, authToken: token);
}
static Future<Map<String, dynamic>> getPATCHResponse(String service, String url, Map<String, dynamic> body) async => _authenticatedRequest(service, url, httpr.getPATCHResponse, body: body); static Future<Map<String, dynamic>> getPUTResponse(String service, String url, Map<String, dynamic> body) async {
final token = await TokenService.getAccessTokenSafe();
return httpr.getPUTResponse(service, url, body, authToken: token);
}
static Future<Map<String, dynamic>> getDELETEResponse(String service, String url, Map<String, dynamic> body) async => _authenticatedRequest(service, url, httpr.getDELETEResponse, body: body); static Future<Map<String, dynamic>> getPATCHResponse(String service, String url, Map<String, dynamic> body) async {
final token = await TokenService.getAccessTokenSafe();
return httpr.getPATCHResponse(service, url, body, authToken: token);
}
static Future<Map<String, dynamic>> getDELETEResponse(String service, String url, Map<String, dynamic> body) async {
final token = await TokenService.getAccessTokenSafe();
return httpr.getDELETEResponse(service, url, body, authToken: token);
}
static Future<String> getFileUploadResponseAuth(String service, String url, String fileName, String fileType, String mediaType, List<int> bytes) async { static Future<String> getFileUploadResponseAuth(String service, String url, String fileName, String fileType, String mediaType, List<int> bytes) async {
final accessToken = await TokenService.getAccessToken(); final token = await TokenService.getAccessTokenSafe();
final res = await httpr.getFileUploadResponse(service, url, fileName, fileType, mediaType, bytes, authToken: accessToken); final res = await httpr.getFileUploadResponse(service, url, fileName, fileType, mediaType, bytes, authToken: token);
if (res == null) { if (res == null) {
throw ErrorUploadFailed(); throw Exception('Upload failed');
} }
return res.url; return res.url;
} }
static Future<bool> isAuthorizationStored() async => AuthorizationStorage.isAuthorizationStored();
/// Execute an operation with automatic token management and retry logic
static Future<T> executeWithAuth<T>(
Future<T> Function() operation,
String description, {
int? maxRetries,
}) async => AuthCircuitBreaker.execute(() async => RetryHelper.withExponentialBackoff(
operation,
maxRetries: maxRetries ?? 3,
initialDelay: Duration(milliseconds: 100),
maxDelay: Duration(seconds: 5),
shouldRetry: (error) => RetryHelper.isRetryableError(error),
));
/// Handle 401 unauthorized errors with automatic token recovery
static Future<T> handleUnauthorized<T>(
Future<T> Function() operation,
String description,
) async {
_logger.warning('Handling unauthorized error with token recovery: $description');
return executeWithAuth(
() async {
try {
// Attempt token recovery first
await TokenService.handleUnauthorized();
// Retry the original operation
return await operation();
} catch (e) {
_logger.severe('Token recovery failed', e);
throw AuthenticationFailedException('Token recovery failed', toException(e));
}
},
'unauthorized recovery: $description',
);
}
} }

View File

@@ -20,6 +20,34 @@ class AuthorizationStorage {
return TokenData.fromJson(jsonDecode(tokenJson)); return TokenData.fromJson(jsonDecode(tokenJson));
} }
static Future<bool> _checkTokenUsable(String keyName) async {
final hasKey = await SecureStorageService.containsKey(keyName);
if (!hasKey) return false;
try {
final tokenData = await _getTokenData(keyName);
return tokenData.expiration.isAfter(DateTime.now());
} catch (e, st) {
_logger.warning('Error reading token from $keyName: $e', e, st);
rethrow;
}
}
static Future<bool> isAuthorizationStored() async {
_logger.fine('Checking if authorization is stored');
final accessUsable = await _checkTokenUsable(Constants.accessTokenStorageKey);
if (accessUsable) return true;
final refreshUsable = await _checkTokenUsable(Constants.refreshTokenStorageKey);
if (refreshUsable) return true;
return false;
}
static Future<TokenData> getAccessToken() async { static Future<TokenData> getAccessToken() async {
_logger.fine('Getting access token'); _logger.fine('Getting access token');
return _getTokenData(Constants.accessTokenStorageKey); return _getTokenData(Constants.accessTokenStorageKey);

View File

@@ -1,20 +1,25 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:pshared/api/errors/authorization_failed.dart';
import 'package:pshared/api/errors/unauthorized.dart'; import 'package:pshared/api/errors/unauthorized.dart';
import 'package:pshared/api/requests/tokens/access_refresh.dart'; import 'package:pshared/api/requests/tokens/access_refresh.dart';
import 'package:pshared/api/requests/tokens/refresh_rotate.dart'; import 'package:pshared/api/requests/tokens/refresh_rotate.dart';
import 'package:pshared/api/responses/account.dart'; import 'package:pshared/api/responses/account.dart';
import 'package:pshared/api/responses/error/server.dart';
import 'package:pshared/api/responses/login.dart'; import 'package:pshared/api/responses/login.dart';
import 'package:pshared/api/responses/token.dart'; import 'package:pshared/api/responses/token.dart';
import 'package:pshared/config/constants.dart'; import 'package:pshared/config/constants.dart';
import 'package:pshared/service/authorization/circuit_breaker.dart';
import 'package:pshared/service/authorization/retry_helper.dart';
import 'package:pshared/service/authorization/storage.dart'; import 'package:pshared/service/authorization/storage.dart';
import 'package:pshared/service/authorization/token_mutex.dart';
import 'package:pshared/service/device_id.dart'; import 'package:pshared/service/device_id.dart';
import 'package:pshared/service/services.dart'; import 'package:pshared/service/services.dart';
import 'package:pshared/utils/exception.dart';
import 'package:pshared/utils/http/requests.dart'; import 'package:pshared/utils/http/requests.dart';
class TokenService { class TokenService {
static final _logger = Logger('service.authorization.token'); static final _logger = Logger('service.authorization.token');
static const String _objectType = Services.account; static const String _objectType = Services.account;
@@ -26,7 +31,11 @@ class TokenService {
static Future<String> getAccessToken() async { static Future<String> getAccessToken() async {
TokenData token = await AuthorizationStorage.getAccessToken(); TokenData token = await AuthorizationStorage.getAccessToken();
if (_isTokenExpiringSoon(token, const Duration(hours: 4))) { if (_isTokenExpiringSoon(token, const Duration(hours: 4))) {
token = (await _refreshAccessToken()).accessToken; // Use mutex to prevent concurrent refresh operations
final refreshedToken = await TokenRefreshMutex().executeRefresh(() async {
return (await refreshAccessToken()).accessToken.token;
});
return refreshedToken;
} }
return token.token; return token.token;
} }
@@ -36,13 +45,13 @@ class TokenService {
await AuthorizationStorage.updateRefreshToken(response.refreshToken); await AuthorizationStorage.updateRefreshToken(response.refreshToken);
} }
static Future<AccountResponse> _refreshAccessToken() async { static Future<AccountResponse> refreshAccessToken() async {
_logger.fine('Refreshing access token...'); _logger.fine('Refreshing access token...');
final deviceId = await DeviceIdManager.getDeviceId(); final deviceId = await DeviceIdManager.getDeviceId();
final refresh = await AuthorizationStorage.getRefreshToken(); final refresh = await AuthorizationStorage.getRefreshToken();
if (_isTokenExpiringSoon(refresh, const Duration(days: 7))) { if (_isTokenExpiringSoon(refresh, const Duration(days: 7))) {
return await rotateRefreshToken(); return await _rotateRefreshToken();
} }
final response = await getPOSTResponse( final response = await getPOSTResponse(
@@ -60,7 +69,7 @@ class TokenService {
return accountResp; return accountResp;
} }
static Future<LoginResponse> rotateRefreshToken() async { static Future<LoginResponse> _rotateRefreshToken() async {
_logger.fine('Rotating refresh token...'); _logger.fine('Rotating refresh token...');
final refresh = await AuthorizationStorage.getRefreshToken(); final refresh = await AuthorizationStorage.getRefreshToken();
@@ -82,4 +91,89 @@ class TokenService {
return loginResponse; return loginResponse;
} }
/// Enhanced method to handle unexpected 401 errors with fallback logic
static Future<void> handleUnauthorized() async {
_logger.warning('Handling unexpected 401 unauthorized error');
return AuthCircuitBreaker.execute(() async {
return RetryHelper.withExponentialBackoff(
() async {
try {
// Try refresh first (faster)
final currentRefresh = await AuthorizationStorage.getRefreshToken();
if (!_isTokenExpiringSoon(currentRefresh, const Duration(days: 1))) {
_logger.fine('Attempting access token refresh for 401 recovery');
await TokenRefreshMutex().executeRefresh(() async {
await refreshAccessToken();
return 'refreshed';
});
return;
}
// Fallback to rotation if refresh token expiring soon
_logger.fine('Attempting refresh token rotation for 401 recovery');
await TokenRefreshMutex().executeRotation(() async {
await _rotateRefreshToken();
});
} catch (e) {
_logger.severe('Token recovery failed: $e');
throw AuthenticationFailedException('Token recovery failed', toException(e));
}
},
maxRetries: 2,
shouldRetry: (error) {
// Only retry on network errors, not auth errors
return RetryHelper.isRetryableError(error) && !_isAuthError(error);
},
);
});
}
/// Enhanced getAccessToken with better error handling
static Future<String> getAccessTokenSafe() async {
return AuthCircuitBreaker.execute(() async {
return RetryHelper.withExponentialBackoff(
() async {
TokenData token = await AuthorizationStorage.getAccessToken();
if (_isTokenExpiringSoon(token, const Duration(hours: 4))) {
// Use mutex to prevent concurrent refresh operations
final refreshedToken = await TokenRefreshMutex().executeRefresh(() async {
return (await refreshAccessToken()).accessToken.token;
});
return refreshedToken;
}
return token.token;
},
maxRetries: 2,
shouldRetry: (error) => RetryHelper.isRetryableError(error),
);
});
}
/// Check if error is authentication-related (non-retryable)
static bool _isAuthError(Exception error) {
if (error is ErrorUnauthorized || error is AuthenticationFailedException) {
return true;
}
if (error is ErrorResponse && error.code == 401) {
return true;
}
final errorString = error.toString().toLowerCase();
return errorString.contains('unauthorized') ||
errorString.contains('401') ||
errorString.contains('authentication') ||
errorString.contains('token');
}
/// Get circuit breaker status for debugging
static Map<String, dynamic> getAuthStatus() {
return {
'circuitBreaker': AuthCircuitBreaker.getStatus(),
'tokenMutex': TokenRefreshMutex().getStatus(),
'timestamp': DateTime.now().toIso8601String(),
};
}
} }

View File

@@ -0,0 +1,103 @@
import 'dart:async';
import 'package:logging/logging.dart';
/// Mutex to prevent concurrent token refresh operations
/// This ensures only one refresh operation happens at a time,
/// preventing race conditions during app startup when multiple
/// providers try to refresh tokens simultaneously.
class TokenRefreshMutex {
static final _instance = TokenRefreshMutex._();
factory TokenRefreshMutex() => _instance;
TokenRefreshMutex._();
static final _logger = Logger('service.authorization.token_mutex');
Completer<String>? _currentRefresh;
Completer<void>? _currentRotation;
/// Execute a token refresh operation with mutex protection
/// If another refresh is in progress, wait for it to complete
Future<String> executeRefresh(Future<String> Function() refreshOperation) async {
if (_currentRefresh != null) {
_logger.fine('Token refresh already in progress, waiting for completion');
return await _currentRefresh!.future;
}
_logger.fine('Starting new token refresh operation');
_currentRefresh = Completer<String>();
try {
final result = await refreshOperation();
if (_currentRefresh != null) {
_currentRefresh!.complete(result);
_logger.fine('Token refresh completed successfully');
}
return result;
} catch (e, st) {
_logger.warning('Token refresh failed', e, st);
if (_currentRefresh != null) {
_currentRefresh!.completeError(e, st);
}
rethrow;
} finally {
_currentRefresh = null;
}
}
/// Execute a token rotation operation with mutex protection
/// If another rotation is in progress, wait for it to complete
Future<void> executeRotation(Future<void> Function() rotationOperation) async {
if (_currentRotation != null) {
_logger.fine('Token rotation already in progress, waiting for completion');
return await _currentRotation!.future;
}
_logger.fine('Starting new token rotation operation');
_currentRotation = Completer<void>();
try {
await rotationOperation();
if (_currentRotation != null) {
_currentRotation!.complete();
_logger.fine('Token rotation completed successfully');
}
} catch (e, st) {
_logger.warning('Token rotation failed', e, st);
if (_currentRotation != null) {
_currentRotation!.completeError(e, st);
}
rethrow;
} finally {
_currentRotation = null;
}
}
/// Check if a refresh operation is currently in progress
bool get isRefreshInProgress => _currentRefresh != null;
/// Check if a rotation operation is currently in progress
bool get isRotationInProgress => _currentRotation != null;
/// Get current status for debugging
Map<String, dynamic> getStatus() {
return {
'refreshInProgress': isRefreshInProgress,
'rotationInProgress': isRotationInProgress,
'timestamp': DateTime.now().toIso8601String(),
};
}
/// Force reset the mutex (for testing or emergency situations)
void forceReset() {
_logger.warning('Force resetting token refresh mutex');
if (_currentRefresh != null && !_currentRefresh!.isCompleted) {
_currentRefresh!.completeError(Exception('Mutex force reset'));
}
if (_currentRotation != null && !_currentRotation!.isCompleted) {
_currentRotation!.completeError(Exception('Mutex force reset'));
}
_currentRefresh = null;
_currentRotation = null;
}
}

View File

@@ -1,5 +1,6 @@
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class SecureStorageService { class SecureStorageService {
static Future<String?> get(String key) async { static Future<String?> get(String key) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@@ -18,6 +19,11 @@ class SecureStorageService {
return _setImp(prefs, key, value); return _setImp(prefs, key, value);
} }
static Future<bool> containsKey(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.containsKey(key);
}
static Future<void> delete(String key) async { static Future<void> delete(String key) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.remove(key); await prefs.remove(key);

View File

@@ -6,27 +6,13 @@ class Services {
static const String invitations = 'invitations'; static const String invitations = 'invitations';
static const String organization = 'organizations'; static const String organization = 'organizations';
static const String permission = 'permissions'; static const String permission = 'permissions';
static const String project = 'projects';
static const String pgroup = 'priority_groups';
static const String priorities = 'priorities';
static const String reactions = 'reactions';
static const String storage = 'storage'; static const String storage = 'storage';
static const String taskStatus = 'statuses';
static const String tasks = 'tasks';
static const String amplitude = 'amplitude'; static const String amplitude = 'amplitude';
static const String automations = 'automation';
static const String changes = 'changes';
static const String clients = 'clients'; static const String clients = 'clients';
static const String invoices = 'invoices';
static const String logo = 'logo'; static const String logo = 'logo';
static const String notifications = 'notifications'; static const String notifications = 'notifications';
static const String policies = 'policies'; static const String policies = 'policies';
static const String properties = 'properties';
static const String refreshTokens = 'refresh_tokens'; static const String refreshTokens = 'refresh_tokens';
static const String roles = 'roles'; static const String roles = 'roles';
static const String steps = 'steps';
static const String teams = 'teams';
static const String workflows = 'workflows';
static const String workspaces = 'workspaces';
} }

1
frontend/pweb/lib/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,779 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get login => 'Login';
@override
String get logout => 'Logout';
@override
String get profile => 'Profile';
@override
String get signup => 'Sign up';
@override
String get username => 'Email';
@override
String get usernameHint => 'email@example.com';
@override
String get usernameErrorInvalid => 'Provide a valid email address';
@override
String usernameUnknownTLD(Object domain) {
return 'Domain .$domain is not known, please, check it';
}
@override
String get password => 'Password';
@override
String get confirmPassword => 'Confirm password';
@override
String get passwordValidationRuleDigit => 'has digit';
@override
String get passwordValidationRuleUpperCase => 'has uppercase letter';
@override
String get passwordValidationRuleLowerCase => 'has lowercase letter';
@override
String get passwordValidationRuleSpecialCharacter =>
'has special character letter';
@override
String passwordValidationRuleMinCharacters(Object charNum) {
return 'is $charNum characters long at least';
}
@override
String get passwordsDoNotMatch => 'Passwords do not match';
@override
String passwordValidationError(Object matchesCriteria) {
return 'Check that your password $matchesCriteria';
}
@override
String notificationError(Object error) {
return 'Error occurred: $error';
}
@override
String loginUserNotFound(Object account) {
return 'Account $account has not been registered in the system';
}
@override
String get loginPasswordIncorrect =>
'Authorization failed, please check your password';
@override
String internalErrorOccurred(Object error) {
return 'An internal server error occurred: $error, we already know about it and working hard to fix it';
}
@override
String get noErrorInformation =>
'Some error occurred, but we have not error information. We are already investigating the issue';
@override
String get yourName => 'Your name';
@override
String get nameHint => 'John Doe';
@override
String get errorPageNotFoundTitle => 'Page Not Found';
@override
String get errorPageNotFoundMessage => 'Oops! We couldn\'t find that page.';
@override
String get errorPageNotFoundHint =>
'The page you\'re looking for doesn\'t exist or has been moved. Please check the URL or return to the home page.';
@override
String get errorUnknown => 'Unknown error occurred';
@override
String get unknown => 'unknown';
@override
String get goToLogin => 'Go to Login';
@override
String get goBack => 'Go Back';
@override
String get goToMainPage => 'Go to Main Page';
@override
String get goToSignUp => 'Go to Sign Up';
@override
String signupError(Object error) {
return 'Failed to signup: $error';
}
@override
String signupSuccess(Object email) {
return 'Email confirmation message has been sent to $email. Please, open it and click link to activate your account.';
}
@override
String connectivityError(Object serverAddress) {
return 'Cannot reach the server at $serverAddress. Check your network and try again.';
}
@override
String get errorAccountExists => 'Account already exists';
@override
String get errorAccountNotVerified =>
'Your account hasn\'t been verified yet. Please check your email to complete the verification';
@override
String get errorLoginUnauthorized =>
'Login or password is incorrect. Please try again';
@override
String get errorInternalError =>
'An internal error occurred. We\'re aware of the issue and working to resolve it. Please try again later';
@override
String get errorVerificationTokenNotFound =>
'Account for verification not found. Sign up again';
@override
String get created => 'Created';
@override
String get edited => 'Edited';
@override
String get errorDataConflict =>
'We cant process your data because it has conflicting or contradictory information.';
@override
String get errorAccessDenied =>
'You do not have permission to access this resource. If you need access, please contact an administrator.';
@override
String get errorBrokenPayload =>
'The data you sent is invalid or incomplete. Please check your submission and try again.';
@override
String get errorInvalidArgument =>
'One or more arguments are invalid. Verify your input and try again.';
@override
String get errorBrokenReference =>
'The resource you\'re trying to access could not be referenced. It may have been moved or deleted.';
@override
String get errorInvalidQueryParameter =>
'One or more query parameters are missing or incorrect. Check them and try again.';
@override
String get errorNotImplemented =>
'This feature is not yet available. Please try again later or contact support.';
@override
String get errorLicenseRequired =>
'A valid license is required to perform this action. Please contact your administrator.';
@override
String get errorNotFound =>
'We couldn\'t find the resource you requested. It may have been removed or is temporarily unavailable.';
@override
String get errorNameMissing => 'Please provide a name before continuing.';
@override
String get errorEmailMissing =>
'Please provide an email address before continuing.';
@override
String get errorPasswordMissing =>
'Please provide a password before continuing.';
@override
String get errorEmailNotRegistered =>
'We could not find an account associated with that email address.';
@override
String get errorDuplicateEmail =>
'This email address is already in use. Try another one or reset your password.';
@override
String get showDetailsAction => 'Show Details';
@override
String get errorLogin => 'Error logging in';
@override
String get errorCreatingInvitation => 'Failed to create invitaiton';
@override
String get footerCompanyName => 'Sibilla Solutions LTD';
@override
String get footerAddress =>
'27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus';
@override
String get footerSupport => 'Support';
@override
String get footerEmail => 'Email TBD';
@override
String get footerPhoneLabel => 'Phone';
@override
String get footerPhone => '+357 22 000 253';
@override
String get footerTermsOfService => 'Terms of Service';
@override
String get footerPrivacyPolicy => 'Privacy Policy';
@override
String get footerCookiePolicy => 'Cookie Policy';
@override
String get navigationLogout => 'Logout';
@override
String get dashboard => 'Dashboard';
@override
String get navigationUsersSettings => 'Users';
@override
String get navigationRolesSettings => 'Roles';
@override
String get navigationPermissionsSettings => 'Permissions';
@override
String get usersManagement => 'User Management';
@override
String get navigationOrganizationSettings => 'Organization settings';
@override
String get navigationAccountSettings => 'Profile settings';
@override
String get twoFactorPrompt => 'Enter the 6-digit code we sent to your device';
@override
String get twoFactorResend => 'Didnt receive a code? Resend';
@override
String get twoFactorTitle => 'Two-Factor Authentication';
@override
String get twoFactorError => 'Invalid code. Please try again.';
@override
String get payoutNavDashboard => 'Dashboard';
@override
String get payoutNavSendPayout => 'Send payout';
@override
String get payoutNavRecipients => 'Recipients';
@override
String get payoutNavReports => 'Reports';
@override
String get payoutNavSettings => 'Settings';
@override
String get payoutNavLogout => 'Logout';
@override
String get payoutNavMethods => 'Payouts';
@override
String get expand => 'Expand';
@override
String get collapse => 'Collapse';
@override
String get pageTitleRecipients => 'Recipient address book';
@override
String get actionAddNew => 'Add new';
@override
String get colDataOwner => 'Data owner';
@override
String get colAvatar => 'Avatar';
@override
String get colName => 'Name';
@override
String get colEmail => 'Email';
@override
String get colStatus => 'Status';
@override
String get statusReady => 'Ready';
@override
String get statusRegistered => 'Registered';
@override
String get statusNotRegistered => 'Not registered';
@override
String get typeInternal => 'Managed by me';
@override
String get typeExternal => 'Selfmanaged';
@override
String get searchHint => 'Search recipients';
@override
String get colActions => 'Actions';
@override
String get menuEdit => 'Edit';
@override
String get menuSendPayout => 'Send payout';
@override
String get tooltipRowActions => 'More actions';
@override
String get accountSettings => 'Account Settings';
@override
String get accountNameUpdateError => 'Failed to update account name';
@override
String get settingsSuccessfullyUpdated => 'Settings successfully updated';
@override
String get language => 'Language';
@override
String get failedToUpdateLanguage => 'Failed to update language';
@override
String get settingsImageUpdateError => 'Couldn\'t update the image';
@override
String get settingsImageTitle => 'Image';
@override
String get settingsImageHint => 'Tap to change the image';
@override
String get accountName => 'Name';
@override
String get accountNameHint => 'Specify your name';
@override
String get avatar => 'Profile photo';
@override
String get avatarHint => 'Tap to update';
@override
String get avatarUpdateError => 'Failed to update profile photo';
@override
String get settings => 'Settings';
@override
String get notSet => 'not set';
@override
String get search => 'Search...';
@override
String get ok => 'Ok';
@override
String get cancel => 'Cancel';
@override
String get confirm => 'Confirm';
@override
String get back => 'Back';
@override
String get operationfryTitle => 'Operation history';
@override
String get filters => 'Filters';
@override
String get period => 'Period';
@override
String get selectPeriod => 'Select period';
@override
String get apply => 'Apply';
@override
String status(String status) {
return '$status';
}
@override
String get operationStatusSuccessful => 'Successful';
@override
String get operationStatusPending => 'Pending';
@override
String get operationStatusUnsuccessful => 'Unsuccessful';
@override
String get statusColumn => 'Status';
@override
String get fileNameColumn => 'File name';
@override
String get amountColumn => 'Amount';
@override
String get toAmountColumn => 'To amount';
@override
String get payIdColumn => 'Pay ID';
@override
String get cardNumberColumn => 'Card number';
@override
String get nameColumn => 'Name';
@override
String get dateColumn => 'Date';
@override
String get commentColumn => 'Comment';
@override
String get paymentConfigTitle => 'Where to receive money';
@override
String get paymentConfigSubtitle =>
'Add multiple methods and choose your primary one.';
@override
String get addPaymentMethod => 'Add payment method';
@override
String get makeMain => 'Make primary';
@override
String get advanced => 'Advanced';
@override
String get fallbackExplanation =>
'If the primary method is unavailable, we will try the next enabled one in the list.';
@override
String get delete => 'Delete';
@override
String get deletePaymentConfirmation =>
'Are you sure you want to delete this payment method?';
@override
String get edit => 'Edit';
@override
String get moreActions => 'More actions';
@override
String get noPayouts => 'No Payouts';
@override
String get enterBankName => 'Enter bank name';
@override
String get paymentType => 'Payment Method Type';
@override
String get selectPaymentType => 'Please select a payment method type';
@override
String get paymentTypeCard => 'Credit Card';
@override
String get paymentTypeBankAccount => 'Russian Bank Account';
@override
String get paymentTypeIban => 'IBAN';
@override
String get paymentTypeWallet => 'Wallet';
@override
String get cardNumber => 'Card Number';
@override
String get enterCardNumber => 'Enter the card number';
@override
String get cardholderName => 'Cardholder Name';
@override
String get iban => 'IBAN';
@override
String get enterIban => 'Enter IBAN';
@override
String get bic => 'BIC';
@override
String get bankName => 'Bank Name';
@override
String get accountHolder => 'Account Holder';
@override
String get enterAccountHolder => 'Enter account holder';
@override
String get enterBic => 'Enter BIC';
@override
String get walletId => 'Wallet ID';
@override
String get enterWalletId => 'Enter wallet ID';
@override
String get recipients => 'Recipients';
@override
String get recipientName => 'Recipient Name';
@override
String get enterRecipientName => 'Enter recipient name';
@override
String get inn => 'INN';
@override
String get enterInn => 'Enter INN';
@override
String get kpp => 'KPP';
@override
String get enterKpp => 'Enter KPP';
@override
String get accountNumber => 'Account Number';
@override
String get enterAccountNumber => 'Enter account number';
@override
String get correspondentAccount => 'Correspondent Account';
@override
String get enterCorrespondentAccount => 'Enter correspondent account';
@override
String get bik => 'BIK';
@override
String get enterBik => 'Enter BIK';
@override
String get add => 'Add';
@override
String get expiryDate => 'Expiry (MM/YY)';
@override
String get firstName => 'First Name';
@override
String get enterFirstName => 'Enter First Name';
@override
String get lastName => 'Last Name';
@override
String get enterLastName => 'Enter Last Name';
@override
String get sendSingle => 'Send single transaction';
@override
String get sendMultiple => 'Send multiple transactions';
@override
String get addFunds => 'Add Funds';
@override
String get close => 'Close';
@override
String get multiplePayout => 'Multiple Payout';
@override
String get howItWorks => 'How it works?';
@override
String get exampleTitle => 'File Format & Sample';
@override
String get downloadSampleCSV => 'Download sample.csv';
@override
String get tokenColumn => 'Token (required)';
@override
String get currency => 'Currency';
@override
String get amount => 'Amount';
@override
String get comment => 'Comment';
@override
String get uploadCSV => 'Upload your CSV';
@override
String get upload => 'Upload';
@override
String get hintUpload => 'Supported format: .CSV · Max size 1 MB';
@override
String get uploadHistory => 'Upload History';
@override
String get payout => 'Payout';
@override
String get sendTo => 'Send Payout To';
@override
String get send => 'Send Payout';
@override
String get recipientPaysFee => 'Recipient pays the fee';
@override
String sentAmount(String amount) {
return 'Sent amount: \$$amount';
}
@override
String fee(String fee) {
return 'Fee: \$$fee';
}
@override
String recipientWillReceive(String amount) {
return 'Recipient will receive: \$$amount';
}
@override
String total(String total) {
return 'Total: \$$total';
}
@override
String get hideDetails => 'Hide Details';
@override
String get showDetails => 'Show Details';
@override
String get whereGetMoney => 'Source of funds for debit';
@override
String get details => 'Details';
@override
String get addRecipient => 'Add Recipient';
@override
String get editRecipient => 'Edit Recipient';
@override
String get saveRecipient => 'Save Recipient';
@override
String get choosePaymentMethod => 'Payment Methods (choose at least 1)';
@override
String get recipientFormRule =>
'Recipient must have at least one payment method';
@override
String get allStatus => 'All';
@override
String get readyStatus => 'Ready';
@override
String get registeredStatus => 'Registered';
@override
String get notRegisteredStatus => 'Not registered';
@override
String get noRecipientSelected => 'No recipient selected';
@override
String get companyName => 'Name of your company';
@override
String get companynameRequired => 'Company name required';
@override
String get errorSignUp => 'Error occured while signing up, try again later';
@override
String get companyDescription => 'Company Description';
@override
String get companyDescriptionHint =>
'Describe any of the fields of the Company\'s business';
@override
String get optional => 'optional';
}

View File

@@ -1,782 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Russian (`ru`).
class AppLocalizationsRu extends AppLocalizations {
AppLocalizationsRu([String locale = 'ru']) : super(locale);
@override
String get login => 'Войти';
@override
String get logout => 'Выйти';
@override
String get profile => 'Профиль';
@override
String get signup => 'Регистрация';
@override
String get username => 'Email';
@override
String get usernameHint => 'email@example.com';
@override
String get usernameErrorInvalid =>
'Укажите действительный адрес электронной почты';
@override
String usernameUnknownTLD(Object domain) {
return 'Домен .$domain неизвестен, пожалуйста, проверьте его';
}
@override
String get password => 'Пароль';
@override
String get confirmPassword => 'Подтвердите пароль';
@override
String get passwordValidationRuleDigit => 'содержит цифру';
@override
String get passwordValidationRuleUpperCase => 'содержит заглавную букву';
@override
String get passwordValidationRuleLowerCase => 'содержит строчную букву';
@override
String get passwordValidationRuleSpecialCharacter =>
'содержит специальный символ';
@override
String passwordValidationRuleMinCharacters(Object charNum) {
return 'длина не менее $charNum символов';
}
@override
String get passwordsDoNotMatch => 'Пароли не совпадают';
@override
String passwordValidationError(Object matchesCriteria) {
return 'Убедитесь, что ваш пароль $matchesCriteria';
}
@override
String notificationError(Object error) {
return 'Произошла ошибка: $error';
}
@override
String loginUserNotFound(Object account) {
return 'Аккаунт $account не зарегистрирован в системе';
}
@override
String get loginPasswordIncorrect =>
'Ошибка авторизации, пожалуйста, проверьте пароль';
@override
String internalErrorOccurred(Object error) {
return 'Произошла внутренняя ошибка сервера: $error, мы уже знаем о ней и усердно работаем над исправлением';
}
@override
String get noErrorInformation =>
'Произошла ошибка, но у нас нет информации о ней. Мы уже расследуем этот вопрос';
@override
String get yourName => 'Ваше имя';
@override
String get nameHint => 'Иван Иванов';
@override
String get errorPageNotFoundTitle => 'Страница не найдена';
@override
String get errorPageNotFoundMessage =>
'Упс! Мы не смогли найти эту страницу.';
@override
String get errorPageNotFoundHint =>
'Запрашиваемая страница не существует или была перемещена. Пожалуйста, проверьте URL или вернитесь на главную страницу.';
@override
String get errorUnknown => 'Произошла неизвестная ошибка';
@override
String get unknown => 'неизвестно';
@override
String get goToLogin => 'Перейти к входу';
@override
String get goBack => 'Назад';
@override
String get goToMainPage => 'На главную';
@override
String get goToSignUp => 'Перейти к регистрации';
@override
String signupError(Object error) {
return 'Не удалось зарегистрироваться: $error';
}
@override
String signupSuccess(Object email) {
return 'Письмо с подтверждением email отправлено на $email. Пожалуйста, откройте его и перейдите по ссылке для активации вашего аккаунта.';
}
@override
String connectivityError(Object serverAddress) {
return 'Не удается связаться с сервером $serverAddress. Проверьте ваше интернет-соединение и попробуйте снова.';
}
@override
String get errorAccountExists => 'Account already exists';
@override
String get errorAccountNotVerified =>
'Ваш аккаунт еще не подтвержден. Пожалуйста, проверьте вашу электронную почту для завершения верификации';
@override
String get errorLoginUnauthorized =>
'Неверный логин или пароль. Пожалуйста, попробуйте снова';
@override
String get errorInternalError =>
'Произошла внутренняя ошибка. Мы в курсе проблемы и работаем над ее решением. Пожалуйста, попробуйте позже';
@override
String get errorVerificationTokenNotFound =>
'Аккаунт для верификации не найден. Зарегистрируйтесь снова';
@override
String get created => 'Создано';
@override
String get edited => 'Изменено';
@override
String get errorDataConflict =>
'Мы не можем обработать ваши данные, так как они содержат конфликтующую или противоречивую информацию.';
@override
String get errorAccessDenied =>
'У вас нет разрешения на доступ к этому ресурсу. Если вам нужен доступ, пожалуйста, обратитесь к администратору.';
@override
String get errorBrokenPayload =>
'Отправленные данные недействительны или неполны. Пожалуйста, проверьте введенные данные и попробуйте снова.';
@override
String get errorInvalidArgument =>
'Один или несколько аргументов недействительны. Проверьте введенные данные и попробуйте снова.';
@override
String get errorBrokenReference =>
'Ресурс, к которому вы пытаетесь получить доступ, не может быть найден. Возможно, он был перемещен или удален.';
@override
String get errorInvalidQueryParameter =>
'Один или несколько параметров запроса отсутствуют или указаны неверно. Проверьте их и попробуйте снова.';
@override
String get errorNotImplemented =>
'Эта функция еще недоступна. Пожалуйста, попробуйте позже или обратитесь в службу поддержки.';
@override
String get errorLicenseRequired =>
'Для выполнения этого действия требуется действующая лицензия. Пожалуйста, обратитесь к вашему администратору.';
@override
String get errorNotFound =>
'Мы не смогли найти запрошенный ресурс. Возможно, он был удален или временно недоступен.';
@override
String get errorNameMissing => 'Пожалуйста, укажите имя для продолжения.';
@override
String get errorEmailMissing =>
'Пожалуйста, укажите адрес электронной почты для продолжения.';
@override
String get errorPasswordMissing =>
'Пожалуйста, укажите пароль для продолжения.';
@override
String get errorEmailNotRegistered =>
'Мы не нашли аккаунт, связанный с этим адресом электронной почты.';
@override
String get errorDuplicateEmail =>
'Этот адрес электронной почты уже используется. Попробуйте другой или восстановите пароль.';
@override
String get showDetailsAction => 'Показать детали';
@override
String get errorLogin => 'Ошибка входа';
@override
String get errorCreatingInvitation => 'Не удалось создать приглашение';
@override
String get footerCompanyName => 'Sibilla Solutions LTD';
@override
String get footerAddress =>
'27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus';
@override
String get footerSupport => 'Поддержка';
@override
String get footerEmail => 'Email TBD';
@override
String get footerPhoneLabel => 'Телефон';
@override
String get footerPhone => '+357 22 000 253';
@override
String get footerTermsOfService => 'Условия обслуживания';
@override
String get footerPrivacyPolicy => 'Политика конфиденциальности';
@override
String get footerCookiePolicy => 'Политика использования файлов cookie';
@override
String get navigationLogout => 'Выйти';
@override
String get dashboard => 'Дашборд';
@override
String get navigationUsersSettings => 'Пользователи';
@override
String get navigationRolesSettings => 'Роли';
@override
String get navigationPermissionsSettings => 'Разрешения';
@override
String get usersManagement => 'Управление пользователями';
@override
String get navigationOrganizationSettings => 'Настройки организации';
@override
String get navigationAccountSettings => 'Настройки профиля';
@override
String get twoFactorPrompt =>
'Введите 6-значный код, отправленный на ваше устройство';
@override
String get twoFactorResend => 'Не получили код? Отправить снова';
@override
String get twoFactorTitle => 'Двухфакторная аутентификация';
@override
String get twoFactorError => 'Неверный код. Пожалуйста, попробуйте снова.';
@override
String get payoutNavDashboard => 'Дашборд';
@override
String get payoutNavSendPayout => 'Отправить выплату';
@override
String get payoutNavRecipients => 'Получатели';
@override
String get payoutNavReports => 'Отчеты';
@override
String get payoutNavSettings => 'Настройки';
@override
String get payoutNavLogout => 'Выйти';
@override
String get payoutNavMethods => 'Выплаты';
@override
String get expand => 'Развернуть';
@override
String get collapse => 'Свернуть';
@override
String get pageTitleRecipients => 'Адресная книга получателей';
@override
String get actionAddNew => 'Добавить';
@override
String get colDataOwner => 'Владелец данных';
@override
String get colAvatar => 'Аватар';
@override
String get colName => 'Имя';
@override
String get colEmail => 'Email';
@override
String get colStatus => 'Статус';
@override
String get statusReady => 'Готов';
@override
String get statusRegistered => 'Зарегистрирован';
@override
String get statusNotRegistered => 'Не зарегистрирован';
@override
String get typeInternal => 'Управляется мной';
@override
String get typeExternal => 'Самоуправляемый';
@override
String get searchHint => 'Поиск получателей';
@override
String get colActions => 'Действия';
@override
String get menuEdit => 'Редактировать';
@override
String get menuSendPayout => 'Отправить выплату';
@override
String get tooltipRowActions => 'Другие действия';
@override
String get accountSettings => 'Настройки аккаунта';
@override
String get accountNameUpdateError => 'Не удалось обновить имя аккаунта';
@override
String get settingsSuccessfullyUpdated => 'Настройки успешно обновлены';
@override
String get language => 'Язык';
@override
String get failedToUpdateLanguage => 'Не удалось обновить язык';
@override
String get settingsImageUpdateError => 'Не удалось обновить изображение';
@override
String get settingsImageTitle => 'Изображение';
@override
String get settingsImageHint => 'Нажмите, чтобы изменить изображение';
@override
String get accountName => 'Имя';
@override
String get accountNameHint => 'Укажите ваше имя';
@override
String get avatar => 'Фото профиля';
@override
String get avatarHint => 'Нажмите для обновления';
@override
String get avatarUpdateError => 'Не удалось обновить фото профиля';
@override
String get settings => 'Настройки';
@override
String get notSet => 'не задано';
@override
String get search => 'Поиск...';
@override
String get ok => 'Ок';
@override
String get cancel => 'Отмена';
@override
String get confirm => 'Подтвердить';
@override
String get back => 'Назад';
@override
String get operationfryTitle => 'История операций';
@override
String get filters => 'Фильтры';
@override
String get period => 'Период';
@override
String get selectPeriod => 'Выберите период';
@override
String get apply => 'Применить';
@override
String status(String status) {
return '$status';
}
@override
String get operationStatusSuccessful => 'Успешно';
@override
String get operationStatusPending => 'В ожидании';
@override
String get operationStatusUnsuccessful => 'Неуспешно';
@override
String get statusColumn => 'Статус';
@override
String get fileNameColumn => 'Имя файла';
@override
String get amountColumn => 'Сумма';
@override
String get toAmountColumn => 'На сумму';
@override
String get payIdColumn => 'Pay ID';
@override
String get cardNumberColumn => 'Номер карты';
@override
String get nameColumn => 'Имя';
@override
String get dateColumn => 'Дата';
@override
String get commentColumn => 'Комментарий';
@override
String get paymentConfigTitle => 'Куда получать деньги';
@override
String get paymentConfigSubtitle =>
'Добавьте несколько методов и выберите основной.';
@override
String get addPaymentMethod => 'Добавить способ оплаты';
@override
String get makeMain => 'Сделать основным';
@override
String get advanced => 'Дополнительно';
@override
String get fallbackExplanation =>
'Если основной метод недоступен, мы попробуем следующий включенный метод в списке.';
@override
String get delete => 'Удалить';
@override
String get deletePaymentConfirmation =>
'Вы уверены, что хотите удалить этот способ оплаты?';
@override
String get edit => 'Редактировать';
@override
String get moreActions => 'Еще действия';
@override
String get noPayouts => 'Нет выплат';
@override
String get enterBankName => 'Введите название банка';
@override
String get paymentType => 'Тип способа оплаты';
@override
String get selectPaymentType => 'Пожалуйста, выберите тип способа оплаты';
@override
String get paymentTypeCard => 'Кредитная карта';
@override
String get paymentTypeBankAccount => 'Российский банковский счет';
@override
String get paymentTypeIban => 'IBAN';
@override
String get paymentTypeWallet => 'Кошелек';
@override
String get cardNumber => 'Номер карты';
@override
String get enterCardNumber => 'Введите номер карты';
@override
String get cardholderName => 'Имя держателя карты';
@override
String get iban => 'IBAN';
@override
String get enterIban => 'Введите IBAN';
@override
String get bic => 'BIC';
@override
String get bankName => 'Название банка';
@override
String get accountHolder => 'Владелец счета';
@override
String get enterAccountHolder => 'Введите владельца счета';
@override
String get enterBic => 'Введите BIC';
@override
String get walletId => 'ID кошелька';
@override
String get enterWalletId => 'Введите ID кошелька';
@override
String get recipients => 'Получатели';
@override
String get recipientName => 'Имя получателя';
@override
String get enterRecipientName => 'Введите имя получателя';
@override
String get inn => 'ИНН';
@override
String get enterInn => 'Введите ИНН';
@override
String get kpp => 'КПП';
@override
String get enterKpp => 'Введите КПП';
@override
String get accountNumber => 'Номер счета';
@override
String get enterAccountNumber => 'Введите номер счета';
@override
String get correspondentAccount => 'Корреспондентский счет';
@override
String get enterCorrespondentAccount => 'Введите корреспондентский счет';
@override
String get bik => 'БИК';
@override
String get enterBik => 'Введите БИК';
@override
String get add => 'Добавить';
@override
String get expiryDate => 'Срок действия (ММ/ГГ)';
@override
String get firstName => 'Имя';
@override
String get enterFirstName => 'Введите имя';
@override
String get lastName => 'Фамилия';
@override
String get enterLastName => 'Введите фамилию';
@override
String get sendSingle => 'Отправить одну транзакцию';
@override
String get sendMultiple => 'Отправить несколько транзакций';
@override
String get addFunds => 'Пополнить счет';
@override
String get close => 'Закрыть';
@override
String get multiplePayout => 'Множественная выплата';
@override
String get howItWorks => 'Как это работает?';
@override
String get exampleTitle => 'Формат файла и образец';
@override
String get downloadSampleCSV => 'Скачать sample.csv';
@override
String get tokenColumn => 'Токен (обязательно)';
@override
String get currency => 'Валюта';
@override
String get amount => 'Сумма';
@override
String get comment => 'Комментарий';
@override
String get uploadCSV => 'Загрузите ваш CSV';
@override
String get upload => 'Загрузить';
@override
String get hintUpload => 'Поддерживаемый формат: .CSV · Макс. размер 1 МБ';
@override
String get uploadHistory => 'История загрузок';
@override
String get payout => 'Выплата';
@override
String get sendTo => 'Отправить выплату';
@override
String get send => 'Отправить выплату';
@override
String get recipientPaysFee => 'Получатель оплачивает комиссию';
@override
String sentAmount(String amount) {
return 'Отправленная сумма: \$$amount';
}
@override
String fee(String fee) {
return 'Комиссия: \$$fee';
}
@override
String recipientWillReceive(String amount) {
return 'Получатель получит: \$$amount';
}
@override
String total(String total) {
return 'Итого: \$$total';
}
@override
String get hideDetails => 'Скрыть детали';
@override
String get showDetails => 'Показать детали';
@override
String get whereGetMoney => 'Источник средств для списания';
@override
String get details => 'Детали';
@override
String get addRecipient => 'Добавить получателя';
@override
String get editRecipient => 'Редактировать получателя';
@override
String get saveRecipient => 'Сохранить получателя';
@override
String get choosePaymentMethod => 'Способы оплаты (выберите хотя бы 1)';
@override
String get recipientFormRule =>
'Получатель должен иметь хотя бы один способ оплаты';
@override
String get allStatus => 'Все';
@override
String get readyStatus => 'Готов';
@override
String get registeredStatus => 'Зарегистрирован';
@override
String get notRegisteredStatus => 'Не зарегистрирован';
@override
String get noRecipientSelected => 'Получатель не выбран';
@override
String get companyName => 'Name of your company';
@override
String get companynameRequired => 'Company name required';
@override
String get errorSignUp => 'Error occured while signing up, try again later';
@override
String get companyDescription => 'Company Description';
@override
String get companyDescriptionHint =>
'Describe any of the fields of the Company\'s business';
@override
String get optional => 'optional';
}

View File

@@ -431,5 +431,7 @@
"errorSignUp": "Error occured while signing up, try again later", "errorSignUp": "Error occured while signing up, try again later",
"companyDescription": "Company Description", "companyDescription": "Company Description",
"companyDescriptionHint": "Describe any of the fields of the Company's business", "companyDescriptionHint": "Describe any of the fields of the Company's business",
"optional": "optional" "optional": "optional",
"ownerRole": "Organization Owner",
"ownerRoleDescription": "This role is granted to the organizations creator, providing full administrative privileges"
} }

View File

@@ -422,5 +422,8 @@
"registeredStatus": "Зарегистрирован", "registeredStatus": "Зарегистрирован",
"notRegisteredStatus": "Не зарегистрирован", "notRegisteredStatus": "Не зарегистрирован",
"noRecipientSelected": "Получатель не выбран" "noRecipientSelected": "Получатель не выбран",
"ownerRole": "Владелец организации",
"ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права"
} }

View File

@@ -11,7 +11,6 @@ import 'package:pshared/config/constants.dart';
import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/account.dart';
import 'package:pshared/provider/locale.dart'; import 'package:pshared/provider/locale.dart';
import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/pfe/provider.dart';
import 'package:pweb/app/app.dart'; import 'package:pweb/app/app.dart';
import 'package:pweb/app/timeago.dart'; import 'package:pweb/app/timeago.dart';
@@ -66,7 +65,7 @@ void main() async {
ChangeNotifierProvider(create: (_) => LocaleProvider(null)), ChangeNotifierProvider(create: (_) => LocaleProvider(null)),
ChangeNotifierProvider(create: (_) => AccountProvider()), ChangeNotifierProvider(create: (_) => AccountProvider()),
ChangeNotifierProvider(create: (_) => OrganizationsProvider()), ChangeNotifierProvider(create: (_) => OrganizationsProvider()),
ChangeNotifierProvider(create: (_) => PfeProvider()), ChangeNotifierProvider(create: (_) => AccountProvider()),
ChangeNotifierProvider(create: (_) => CarouselIndexProvider()), ChangeNotifierProvider(create: (_) => CarouselIndexProvider()),
ChangeNotifierProvider( ChangeNotifierProvider(

View File

@@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/provider/pfe/provider.dart'; import 'package:pshared/provider/account.dart';
import 'package:pshared/provider/locale.dart';
import 'package:pweb/app/router/pages.dart'; import 'package:pweb/app/router/pages.dart';
import 'package:pweb/pages/login/buttons.dart'; import 'package:pweb/pages/login/buttons.dart';
@@ -34,17 +35,19 @@ class _LoginFormState extends State<LoginForm> {
final ValueNotifier<bool> _isPasswordAcceptable = ValueNotifier<bool>(false); final ValueNotifier<bool> _isPasswordAcceptable = ValueNotifier<bool>(false);
Future<String?> _login(BuildContext context, VoidCallback onLogin, void Function(Object e) onError) async { Future<String?> _login(BuildContext context, VoidCallback onLogin, void Function(Object e) onError) async {
final pfeProvider = Provider.of<PfeProvider>(context, listen: false); final provider = Provider.of<AccountProvider>(context, listen: false);
try { try {
// final account = await pfeProvider.login( //final account =
// email: _usernameController.text, await provider.login(
// password: _passwordController.text, email: _usernameController.text,
// ); password: _passwordController.text,
locale: context.read<LocaleProvider>().locale.languageCode,
);
onLogin(); onLogin();
return 'ok'; return 'ok';
} catch (e) { } catch (e) {
onError(pfeProvider.error == null ? e : pfeProvider.error!); onError(provider.error ?? e);
} }
return null; return null;
} }

View File

@@ -1,10 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pshared/provider/pfe/provider.dart'; import 'package:pshared/api/requests/login_data.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/provider/account.dart';
import 'package:pshared/provider/locale.dart';
import 'package:pweb/app/router/pages.dart'; import 'package:pweb/app/router/pages.dart';
import 'package:pweb/pages/signup/form/content.dart'; import 'package:pweb/pages/signup/form/content.dart';
@@ -31,7 +36,7 @@ class SignUpFormState extends State<SignUpForm> {
VoidCallback onSignUp, VoidCallback onSignUp,
void Function(Object e) onError, void Function(Object e) onError,
) async { ) async {
final pfeProvider = Provider.of<PfeProvider>(context, listen: false); final provider = Provider.of<AccountProvider>(context, listen: false);
setState(() { setState(() {
_autoValidateMode = true; _autoValidateMode = true;
@@ -42,20 +47,34 @@ class SignUpFormState extends State<SignUpForm> {
} }
try { try {
// final account = await pfeProvider.signUp( final orgDescription = controllers.description.text.trim();
// companyName: controllers.companyName.text.trim(), final locs = AppLocalizations.of(context)!;
// description: controllers.description.text.trim().isEmpty final locale = context.read<LocaleProvider>().locale;
// ? null final timezone = await FlutterTimezone.getLocalTimezone(locale.toString());
// : controllers.description.text.trim(), await provider.signup(
// firstName: controllers.firstName.text.trim(), account: AccountData.build(
// lastName: controllers.lastName.text.trim(), login: LoginData.build(
// email: controllers.email.text.trim(), login: controllers.email.text.trim(),
// password: controllers.password.text, password: controllers.password.text,
// ); locale: locale.toLanguageTag(),
),
name: controllers.password.text,
lastName: controllers.lastName.text.trim(),
),
organization: newDescribable(
name: controllers.companyName.text.trim(),
description: orgDescription.isEmpty ? null : orgDescription,
),
timezone: timezone.identifier,
ownerRole: newDescribable(
name: locs.ownerRole,
description: locs.ownerRoleDescription,
),
);
onSignUp(); onSignUp();
return 'ok'; return 'ok';
} catch (e) { } catch (e) {
onError(pfeProvider.error ?? e); onError(provider.error ?? e);
} }
return null; return null;
} }

View File

@@ -1 +1 @@
2.0.856 2.0.857