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

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

View File

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

View File

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

View File

@@ -8,9 +8,9 @@ replace github.com/tech/sendico/chain/gateway => ../chain/gateway
require (
github.com/aws/aws-sdk-go-v2 v1.39.6
github.com/aws/aws-sdk-go-v2/config v1.31.18
github.com/aws/aws-sdk-go-v2/credentials v1.18.22
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
github.com/aws/aws-sdk-go-v2/config v1.31.20
github.com/aws/aws-sdk-go-v2/credentials v1.18.24
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/go-chi/jwtauth/v5 v5.3.3
@@ -49,9 +49,9 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 // indirect
github.com/aws/smithy-go v1.23.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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:pshared/api/requests/login_data.dart';
part 'login.g.dart';
@JsonSerializable(explicitToJson: true)
class LoginRequest {
final String login;
final String password;
final String locale;
final LoginData login;
final String clientId;
final String deviceId;
const LoginRequest({
required this.login,
required this.password,
required this.locale,
required this.clientId,
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';
part 'change_password.g.dart';
part 'change.g.dart';
@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:pshared/api/requests/login_data.dart';
import 'package:pshared/data/dto/describable.dart';
import 'package:pshared/data/mapper/describable.dart';
import 'package:pshared/models/describable.dart';
part 'signup.g.dart';
@JsonSerializable(explicitToJson: true)
class SignupRequest {
final SignupAccount account;
final DescribableRequest organization;
final AccountData account;
final DescribableDTO organization;
final String organizationTimeZone;
final DescribableRequest anonymousUser;
final DescribableRequest ownerRole;
final DescribableRequest anonymousRole;
final DescribableDTO ownerRole;
const SignupRequest({
required this.account,
required this.organization,
required this.organizationTimeZone,
required this.anonymousUser,
required this.ownerRole,
required this.anonymousRole,
});
factory SignupRequest.build({
required String name,
required String login,
required String password,
required String locale,
required String organizationName,
required AccountData account,
required Describable organization,
required String organizationTimeZone,
}) =>
SignupRequest(
account: SignupAccount(
name: name,
login: login,
password: password,
locale: locale,
),
organization: DescribableRequest(name: organizationName),
organizationTimeZone: organizationTimeZone,
anonymousUser: const DescribableRequest(name: 'Anonymous'),
ownerRole: const DescribableRequest(name: 'Owner'),
anonymousRole: const DescribableRequest(name: 'Anonymous'),
);
required Describable ownerRole,
}) => SignupRequest(
account: account,
organization: organization.toDTO(),
organizationTimeZone: organizationTimeZone,
ownerRole: ownerRole.toDTO(),
);
factory SignupRequest.fromJson(Map<String, dynamic> json) =>
_$SignupRequestFromJson(json);
factory SignupRequest.fromJson(Map<String, dynamic> json) => _$SignupRequestFromJson(json);
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:pshared/data/dto/account/base.dart';
import 'package:pshared/data/dto/date_time.dart';
part 'account.g.dart';
@@ -8,14 +9,17 @@ part 'account.g.dart';
@JsonSerializable()
class AccountDTO extends AccountBaseDTO {
final String login;
final String locale;
const AccountDTO({
required super.id,
required super.createdAt,
required super.updatedAt,
required super.name,
required super.lastName,
required super.description,
required super.avatarUrl,
required super.locale,
required this.locale,
required this.login,
});

View File

@@ -1,6 +1,7 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/storable.dart';
import 'package:pshared/data/dto/date_time.dart';
part 'base.g.dart';
@@ -8,7 +9,8 @@ part 'base.g.dart';
@JsonSerializable()
class AccountBaseDTO extends StorableDTO {
final String name;
final String locale;
final String lastName;
final String? description;
final String? avatarUrl;
const AccountBaseDTO({
@@ -16,8 +18,9 @@ class AccountBaseDTO extends StorableDTO {
required super.createdAt,
required super.updatedAt,
required this.name,
required this.description,
required this.avatarUrl,
required this.locale,
required this.lastName,
});
factory AccountBaseDTO.fromJson(Map<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:pshared/data/dto/storable.dart';
import 'package:pshared/data/dto/date_time.dart';
import 'package:pshared/data/dto/permissions/bound.dart';
part 'organization.g.dart';
@JsonSerializable()
class OrganizationDTO extends StorableDTO {
class OrganizationDTO extends PermissionBoundDTO {
final String name;
final String? description;
final String timeZone;
final String? logoUrl;
final String tenantRef;
const OrganizationDTO({
required super.id,
required super.createdAt,
required super.updatedAt,
required super.permissionRef,
required super.organizationRef,
required this.name,
required this.tenantRef,
this.description,
required this.timeZone,
this.logoUrl,
});

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:pshared/data/dto/describable.dart';
part 'description.g.dart';
@JsonSerializable()
class OrganizationDescriptionDTO {
final DescribableDTO description;
final String? logoUrl;
const OrganizationDescriptionDTO({
required this.description,
this.logoUrl,
});

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

View File

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

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:pshared/data/dto/date_time.dart';
part 'storable.g.dart';
@JsonSerializable()
class StorableDTO {
final String id;
@UtcIso8601Converter()
final DateTime createdAt;
@UtcIso8601Converter()
final DateTime updatedAt;
const StorableDTO({

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

View File

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

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

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/mapper/describable.dart';
import 'package:pshared/models/organization/description.dart';
extension OrganizationDescriptionMapper on OrganizationDescription {
OrganizationDescriptionDTO toDTO() => OrganizationDescriptionDTO(
description: description.toDTO(),
logoUrl: logoUrl,
);
}
@@ -11,5 +13,6 @@ extension OrganizationDescriptionMapper on OrganizationDescription {
extension AccountDescriptionDTOMapper on OrganizationDescriptionDTO {
OrganizationDescription toDomain() => OrganizationDescription(
logoUrl: logoUrl,
description: description.toDomain(),
);
}

View File

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

View File

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

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 {
StorableDTO toDTO() => StorableDTO(id: id, createdAt: createdAt, updatedAt: updatedAt);
StorableDTO toDTO() => StorableDTO(id: id, createdAt: createdAt.toUtc(), updatedAt: updatedAt.toUtc());
}
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/describable.dart';
@immutable
class Account extends AccountBase {
final String login;
final String locale;
const Account({
required super.storable,
required super.describable,
required super.avatarUrl,
required super.lastName,
required this.login,
required super.locale,
required super.name,
required this.locale,
});
factory Account.fromBase(AccountBase accountBase, String login) => Account(
storable: accountBase.storable,
avatarUrl: accountBase.avatarUrl,
locale: accountBase.locale,
name: accountBase.name,
login: login,
);
@override
Account copyWith({
Describable? describable,
String? lastName,
String? Function()? avatarUrl,
String? name,
String? locale,
}) {
final updatedBase = super.copyWith(
avatarUrl: avatarUrl,
name: name,
locale: locale,
);
return Account.fromBase(updatedBase, login);
}
}) => Account(
storable: storable,
describable: describableCopyWithOther(this.describable, describable),
lastName: lastName ?? this.lastName,
avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl,
login: login,
locale: locale ?? this.locale,
);
}

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

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 {
final Describable description;
final String? logoUrl;
const OrganizationDescription({
required this.description,
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';
class Organization implements Storable {
class Organization implements PermissionBoundStorableDescribable {
final Storable storable;
final PermissionBound permissionBound;
final Describable describable;
@override
String get id => storable.id;
@@ -10,25 +15,39 @@ class Organization implements Storable {
DateTime get createdAt => storable.createdAt;
@override
DateTime get updatedAt => storable.updatedAt;
@override
String get organizationRef => permissionBound.organizationRef;
@override
String get permissionRef => permissionBound.permissionRef;
@override
String get name => describable.name;
@override
String? get description => describable.description;
final String timeZone;
final String? logoUrl;
final String tenantRef;
const Organization({
required this.storable,
required this.describable,
required this.timeZone,
required this.permissionBound,
required this.tenantRef,
this.logoUrl,
});
Organization copyWith({
String? name,
String? Function()? description,
Describable? describable,
String? timeZone,
String? Function()? logoUrl,
}) => Organization(
storable: storable, // Same Storable, same id
describable: describableCopyWithOther(this.describable, describable),
timeZone: timeZone ?? this.timeZone,
logoUrl: logoUrl != null ? logoUrl() : this.logoUrl,
permissionBound: permissionBound,
tenantRef: tenantRef,
);
}

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';

View File

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

View File

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

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

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:pshared/models/permission_bound_storable.dart';
import 'package:pshared/provider/exception.dart';
import 'package:pshared/models/permissions/bound/storable.dart';
import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/template.dart';
import 'package:pshared/utils/exception.dart';
List<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/responses/account.dart';
import 'package:pshared/api/requests/change_password.dart';
import 'package:pshared/api/requests/login_data.dart';
import 'package:pshared/api/requests/password/change.dart';
import 'package:pshared/api/requests/password/forgot.dart';
import 'package:pshared/api/requests/password/reset.dart';
import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/models/account/account.dart';
import 'package:pshared/service/authorization/service.dart';
@@ -17,9 +20,9 @@ class AccountService {
static final _logger = Logger('service.account');
static const String _objectType = Services.account;
static Future<Account> login(String email, String password, String locale) async {
static Future<Account> login(LoginData login) async {
_logger.fine('Logging in');
return AuthorizationService.login(_objectType, email, password, locale);
return AuthorizationService.login(_objectType, login);
}
static Future<Account> restore() async {
@@ -27,6 +30,7 @@ class AccountService {
}
static Future<void> signup(SignupRequest request) async {
// Use regular HTTP for public signup endpoint (no auth needed)
await getPOSTResponse(_objectType, 'signup', request.toJson());
}
@@ -42,9 +46,20 @@ class AccountService {
static Future<Account> update(Account account) async {
_logger.fine('Patching account ${account.id}');
// Use AuthorizationService for authenticated operations
return _getAccount(AuthorizationService.getPUTResponse(_objectType, '', account.toDTO().toJson()));
}
static Future<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 {
_logger.fine('Changing password');
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:pshared/api/errors/upload_failed.dart';
import 'package:pshared/api/errors/authorization_failed.dart';
import 'package:pshared/api/requests/login.dart';
import 'package:pshared/api/requests/login_data.dart';
import 'package:pshared/api/responses/account.dart';
import 'package:pshared/api/responses/login.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/config/web.dart';
import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/models/account/account.dart';
import 'package:pshared/service/authorization/circuit_breaker.dart';
import 'package:pshared/service/authorization/retry_helper.dart';
import 'package:pshared/service/authorization/storage.dart';
import 'package:pshared/service/authorization/token.dart';
import 'package:pshared/service/device_id.dart';
import 'package:pshared/utils/exception.dart';
import 'package:pshared/utils/http/requests.dart' as httpr;
/// AuthorizationService provides centralized authorization management
/// with token refresh, retry logic, and circuit breaker patterns
class AuthorizationService {
static final _logger = Logger('service.authorization');
static final _logger = Logger('service.authorization.auth_service');
static Future<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 {
await AuthorizationStorage.updateToken(response.accessToken);
@@ -31,59 +49,86 @@ class AuthorizationService {
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 {
return (await TokenService.rotateRefreshToken()).account.toDomain();
return (await TokenService.refreshAccessToken()).account.toDomain();
}
static Future<void> logout() async {
return AuthorizationStorage.removeTokens();
}
static Future<Map<String, dynamic>> _authenticatedRequest(
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);
// Original AuthorizationService methods - keeping the interface unchanged
static Future<Map<String, dynamic>> getGETResponse(String service, String url) async {
final accessToken = await TokenService.getAccessToken();
return httpr.getGETResponse(service, url, authToken: accessToken);
final token = await TokenService.getAccessTokenSafe();
return httpr.getGETResponse(service, url, authToken: token);
}
static Future<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 {
final accessToken = await TokenService.getAccessToken();
final res = await httpr.getFileUploadResponse(service, url, fileName, fileType, mediaType, bytes, authToken: accessToken);
final token = await TokenService.getAccessTokenSafe();
final res = await httpr.getFileUploadResponse(service, url, fileName, fileType, mediaType, bytes, authToken: token);
if (res == null) {
throw ErrorUploadFailed();
throw Exception('Upload failed');
}
return res.url;
}
static Future<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));
}
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 {
_logger.fine('Getting access token');
return _getTokenData(Constants.accessTokenStorageKey);

View File

@@ -1,20 +1,25 @@
import 'package:logging/logging.dart';
import 'package:pshared/api/errors/authorization_failed.dart';
import 'package:pshared/api/errors/unauthorized.dart';
import 'package:pshared/api/requests/tokens/access_refresh.dart';
import 'package:pshared/api/requests/tokens/refresh_rotate.dart';
import 'package:pshared/api/responses/account.dart';
import 'package:pshared/api/responses/error/server.dart';
import 'package:pshared/api/responses/login.dart';
import 'package:pshared/api/responses/token.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/service/authorization/circuit_breaker.dart';
import 'package:pshared/service/authorization/retry_helper.dart';
import 'package:pshared/service/authorization/storage.dart';
import 'package:pshared/service/authorization/token_mutex.dart';
import 'package:pshared/service/device_id.dart';
import 'package:pshared/service/services.dart';
import 'package:pshared/utils/exception.dart';
import 'package:pshared/utils/http/requests.dart';
class TokenService {
static final _logger = Logger('service.authorization.token');
static const String _objectType = Services.account;
@@ -26,7 +31,11 @@ class TokenService {
static Future<String> getAccessToken() async {
TokenData token = await AuthorizationStorage.getAccessToken();
if (_isTokenExpiringSoon(token, const Duration(hours: 4))) {
token = (await _refreshAccessToken()).accessToken;
// Use mutex to prevent concurrent refresh operations
final refreshedToken = await TokenRefreshMutex().executeRefresh(() async {
return (await refreshAccessToken()).accessToken.token;
});
return refreshedToken;
}
return token.token;
}
@@ -36,13 +45,13 @@ class TokenService {
await AuthorizationStorage.updateRefreshToken(response.refreshToken);
}
static Future<AccountResponse> _refreshAccessToken() async {
static Future<AccountResponse> refreshAccessToken() async {
_logger.fine('Refreshing access token...');
final deviceId = await DeviceIdManager.getDeviceId();
final refresh = await AuthorizationStorage.getRefreshToken();
if (_isTokenExpiringSoon(refresh, const Duration(days: 7))) {
return await rotateRefreshToken();
return await _rotateRefreshToken();
}
final response = await getPOSTResponse(
@@ -60,7 +69,7 @@ class TokenService {
return accountResp;
}
static Future<LoginResponse> rotateRefreshToken() async {
static Future<LoginResponse> _rotateRefreshToken() async {
_logger.fine('Rotating refresh token...');
final refresh = await AuthorizationStorage.getRefreshToken();
@@ -82,4 +91,89 @@ class TokenService {
return loginResponse;
}
/// Enhanced method to handle unexpected 401 errors with fallback logic
static Future<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';
class SecureStorageService {
static Future<String?> get(String key) async {
final prefs = await SharedPreferences.getInstance();
@@ -18,6 +19,11 @@ class SecureStorageService {
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 {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);

View File

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

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",
"companyDescription": "Company Description",
"companyDescriptionHint": "Describe any of the fields of the Company's business",
"optional": "optional"
"optional": "optional",
"ownerRole": "Organization Owner",
"ownerRoleDescription": "This role is granted to the organizations creator, providing full administrative privileges"
}

View File

@@ -422,5 +422,8 @@
"registeredStatus": "Зарегистрирован",
"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/locale.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/pfe/provider.dart';
import 'package:pweb/app/app.dart';
import 'package:pweb/app/timeago.dart';
@@ -66,7 +65,7 @@ void main() async {
ChangeNotifierProvider(create: (_) => LocaleProvider(null)),
ChangeNotifierProvider(create: (_) => AccountProvider()),
ChangeNotifierProvider(create: (_) => OrganizationsProvider()),
ChangeNotifierProvider(create: (_) => PfeProvider()),
ChangeNotifierProvider(create: (_) => AccountProvider()),
ChangeNotifierProvider(create: (_) => CarouselIndexProvider()),
ChangeNotifierProvider(

View File

@@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/pfe/provider.dart';
import 'package:pshared/provider/account.dart';
import 'package:pshared/provider/locale.dart';
import 'package:pweb/app/router/pages.dart';
import 'package:pweb/pages/login/buttons.dart';
@@ -34,17 +35,19 @@ class _LoginFormState extends State<LoginForm> {
final ValueNotifier<bool> _isPasswordAcceptable = ValueNotifier<bool>(false);
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 {
// final account = await pfeProvider.login(
// email: _usernameController.text,
// password: _passwordController.text,
// );
//final account =
await provider.login(
email: _usernameController.text,
password: _passwordController.text,
locale: context.read<LocaleProvider>().locale.languageCode,
);
onLogin();
return 'ok';
} catch (e) {
onError(pfeProvider.error == null ? e : pfeProvider.error!);
onError(provider.error ?? e);
}
return null;
}

View File

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

View File

@@ -1 +1 @@
2.0.856
2.0.857