+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
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:
BIN
frontend/pshared/lib/.DS_Store
vendored
Normal file
BIN
frontend/pshared/lib/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
frontend/pshared/lib/api/.DS_Store
vendored
Normal file
BIN
frontend/pshared/lib/api/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -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';
|
||||
}
|
||||
}
|
||||
BIN
frontend/pshared/lib/api/requests/.DS_Store
vendored
Normal file
BIN
frontend/pshared/lib/api/requests/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -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,
|
||||
});
|
||||
|
||||
76
frontend/pshared/lib/api/requests/login_data.dart
Normal file
76
frontend/pshared/lib/api/requests/login_data.dart
Normal 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);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'change_password.g.dart';
|
||||
part 'change.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
20
frontend/pshared/lib/api/requests/password/forgot.dart
Normal file
20
frontend/pshared/lib/api/requests/password/forgot.dart
Normal 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);
|
||||
}
|
||||
20
frontend/pshared/lib/api/requests/password/reset.dart
Normal file
20
frontend/pshared/lib/api/requests/password/reset.dart
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
12
frontend/pshared/lib/data/dto/date_time.dart
Normal file
12
frontend/pshared/lib/data/dto/date_time.dart
Normal 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();
|
||||
}
|
||||
18
frontend/pshared/lib/data/dto/describable.dart
Normal file
18
frontend/pshared/lib/data/dto/describable.dart
Normal 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);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
23
frontend/pshared/lib/data/dto/organization/bound.dart
Normal file
23
frontend/pshared/lib/data/dto/organization/bound.dart
Normal 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);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
26
frontend/pshared/lib/data/dto/permissions/bound.dart
Normal file
26
frontend/pshared/lib/data/dto/permissions/bound.dart
Normal 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);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
16
frontend/pshared/lib/data/dto/reference.dart
Normal file
16
frontend/pshared/lib/data/dto/reference.dart
Normal 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);
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
26
frontend/pshared/lib/data/dto/storable/describable.dart
Normal file
26
frontend/pshared/lib/data/dto/storable/describable.dart
Normal 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);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
17
frontend/pshared/lib/data/mapper/describable.dart
Normal file
17
frontend/pshared/lib/data/mapper/describable.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
18
frontend/pshared/lib/data/mapper/organization/bound.dart
Normal file
18
frontend/pshared/lib/data/mapper/organization/bound.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
11
frontend/pshared/lib/data/mapper/reference.dart
Normal file
11
frontend/pshared/lib/data/mapper/reference.dart
Normal 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);
|
||||
}
|
||||
@@ -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
BIN
frontend/pshared/lib/models/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
39
frontend/pshared/lib/models/describable.dart
Normal file
39
frontend/pshared/lib/models/describable.dart
Normal 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,
|
||||
);
|
||||
18
frontend/pshared/lib/models/organization/bound.dart
Normal file
18
frontend/pshared/lib/models/organization/bound.dart
Normal 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);
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
32
frontend/pshared/lib/models/permissions/bound.dart
Normal file
32
frontend/pshared/lib/models/permissions/bound.dart
Normal 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,
|
||||
);
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:pshared/models/permission_bound.dart';
|
||||
import 'package:pshared/models/permissions/bound.dart';
|
||||
import 'package:pshared/models/storable.dart';
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:pshared/models/describable.dart';
|
||||
import 'package:pshared/models/resources.dart';
|
||||
import 'package:pshared/models/storable.dart';
|
||||
import 'package:pshared/models/storable/describable.dart';
|
||||
|
||||
|
||||
class PolicyDescription implements Storable {
|
||||
class PolicyDescription implements StorableDescribable {
|
||||
final Storable storable;
|
||||
final Describable describable;
|
||||
final List<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,
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
22
frontend/pshared/lib/models/reference.dart
Normal file
22
frontend/pshared/lib/models/reference.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
BIN
frontend/pshared/lib/models/storable/.DS_Store
vendored
Normal file
BIN
frontend/pshared/lib/models/storable/.DS_Store
vendored
Normal file
Binary file not shown.
6
frontend/pshared/lib/models/storable/describable.dart
Normal file
6
frontend/pshared/lib/models/storable/describable.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'package:pshared/models/describable.dart';
|
||||
import 'package:pshared/models/storable.dart';
|
||||
|
||||
|
||||
abstract class StorableDescribable implements Storable, Describable {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
87
frontend/pshared/lib/service/authorization/retry_helper.dart
Normal file
87
frontend/pshared/lib/service/authorization/retry_helper.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
103
frontend/pshared/lib/service/authorization/token_mutex.dart
Normal file
103
frontend/pshared/lib/service/authorization/token_mutex.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user