New code verification service
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/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-21 16:41:41 +01:00
parent ef5b3dc1a7
commit e1e4c580e8
72 changed files with 1660 additions and 454 deletions

View File

@@ -0,0 +1,26 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/api/responses/account.dart';
import 'package:pshared/api/responses/token.dart';
part 'login_pending.g.dart';
@JsonSerializable(explicitToJson: true)
class PendingLoginResponse {
final AccountResponse account;
final TokenData pendingToken;
final String destination;
final int ttlSeconds;
const PendingLoginResponse({
required this.account,
required this.pendingToken,
required this.destination,
required this.ttlSeconds,
});
factory PendingLoginResponse.fromJson(Map<String, dynamic> json) => _$PendingLoginResponseFromJson(json);
Map<String, dynamic> toJson() => _$PendingLoginResponseToJson(this);
}

View File

@@ -0,0 +1,17 @@
import 'package:pshared/models/account/account.dart';
import 'package:pshared/models/auth/pending_login.dart';
class LoginOutcome {
final Account? account;
final PendingLogin? pending;
const LoginOutcome._({this.account, this.pending});
factory LoginOutcome.completed(Account account) => LoginOutcome._(account: account);
factory LoginOutcome.pending(PendingLogin pending) => LoginOutcome._(pending: pending);
bool get isPending => pending != null;
bool get isCompleted => account != null;
}

View File

@@ -0,0 +1,33 @@
import 'package:pshared/api/responses/login_pending.dart';
import 'package:pshared/api/responses/token.dart';
import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/models/account/account.dart';
import 'package:pshared/models/session_identifier.dart';
class PendingLogin {
final Account account;
final TokenData pendingToken;
final String destination;
final int ttlSeconds;
final SessionIdentifier session;
const PendingLogin({
required this.account,
required this.pendingToken,
required this.destination,
required this.ttlSeconds,
required this.session,
});
factory PendingLogin.fromResponse(
PendingLoginResponse response, {
required SessionIdentifier session,
}) => PendingLogin(
account: response.account.account.toDomain(),
pendingToken: response.pendingToken,
destination: response.destination,
ttlSeconds: response.ttlSeconds,
session: session,
);
}

View File

@@ -0,0 +1,18 @@
import 'package:json_annotation/json_annotation.dart';
part 'session_identifier.g.dart';
@JsonSerializable()
class SessionIdentifier {
final String clientId;
final String deviceId;
const SessionIdentifier({
required this.clientId,
required this.deviceId,
});
factory SessionIdentifier.fromJson(Map<String, dynamic> json) => _$SessionIdentifierFromJson(json);
Map<String, dynamic> toJson() => _$SessionIdentifierToJson(this);
}

View File

@@ -7,6 +7,8 @@ import 'package:pshared/api/requests/signup.dart';
import 'package:pshared/api/requests/login_data.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/models/account/account.dart';
import 'package:pshared/models/auth/login_outcome.dart';
import 'package:pshared/models/auth/pending_login.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/storable.dart';
import 'package:pshared/provider/locale.dart';
@@ -23,8 +25,10 @@ class AccountProvider extends ChangeNotifier {
Resource<Account?> _resource = Resource(data: null);
Resource<Account?> get resource => _resource;
late LocaleProvider _localeProvider;
PendingLogin? _pendingLogin;
Account? get account => _resource.data;
PendingLogin? get pendingLogin => _pendingLogin;
bool get isLoggedIn => account != null;
bool get isLoading => _resource.isLoading;
Object? get error => _resource.error;
@@ -57,27 +61,38 @@ class AccountProvider extends ChangeNotifier {
void _pickupLocale(String locale) => _localeProvider.setLocale(Locale(locale));
Future<Account> login({
Future<LoginOutcome> login({
required String email,
required String password,
required String locale,
}) async {
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
final acc = await AccountService.login(LoginData.build(
final outcome = await AccountService.login(LoginData.build(
login: email,
password: password,
locale: locale,
));
_setResource(Resource(data: acc, isLoading: false));
_pickupLocale(acc.locale);
return acc;
if (outcome.account != null) {
_setResource(Resource(data: outcome.account, isLoading: false));
_pickupLocale(outcome.account!.locale);
} else {
_pendingLogin = outcome.pending;
_setResource(_resource.copyWith(isLoading: false));
}
return outcome;
} catch (e) {
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
rethrow;
}
}
void completePendingLogin(Account account) {
_pendingLogin = null;
_setResource(Resource(data: account, isLoading: false, error: null));
_pickupLocale(account.locale);
}
Future<bool> isAuthorizationStored() async => AuthorizationService.isAuthorizationStored();
Future<Account?> restore() async {

View File

@@ -8,9 +8,13 @@ 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/api/responses/login.dart';
import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/models/account/account.dart';
import 'package:pshared/models/auth/login_outcome.dart';
import 'package:pshared/models/auth/pending_login.dart';
import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/authorization/storage.dart';
import 'package:pshared/service/files.dart';
import 'package:pshared/service/services.dart';
import 'package:pshared/utils/http/requests.dart';
@@ -20,11 +24,46 @@ class AccountService {
static final _logger = Logger('service.account');
static const String _objectType = Services.account;
static Future<Account> login(LoginData login) async {
static Future<LoginOutcome> login(LoginData login) async {
_logger.fine('Logging in');
return AuthorizationService.login(_objectType, login);
}
static Future<void> resendLoginCode(PendingLogin pending, {String? destination}) async {
await getPOSTResponse(
_objectType,
'confirmations/resend',
{
'target': 'login',
if (destination != null) 'destination': destination,
},
authToken: pending.pendingToken.token,
);
}
static Future<Account> confirmLoginCode({
required PendingLogin pending,
required String code,
String? destination,
}) async {
final response = await getPOSTResponse(
_objectType,
'confirmations/verify',
{
'target': 'login',
'code': code,
if (destination != null) 'destination': destination,
'sessionIdentifier': pending.session.toJson(),
},
authToken: pending.pendingToken.token,
);
final loginResponse = LoginResponse.fromJson(response);
await AuthorizationStorage.updateToken(loginResponse.accessToken);
await AuthorizationStorage.updateRefreshToken(loginResponse.refreshToken);
return loginResponse.account.toDomain();
}
static Future<Account> restore() async {
return AuthorizationService.restore();
}

View File

@@ -5,9 +5,13 @@ 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/api/responses/login_pending.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/models/auth/login_outcome.dart';
import 'package:pshared/models/auth/pending_login.dart';
import 'package:pshared/models/session_identifier.dart';
import 'package:pshared/service/authorization/circuit_breaker.dart';
import 'package:pshared/service/authorization/retry_helper.dart';
import 'package:pshared/service/authorization/storage.dart';
@@ -22,7 +26,7 @@ import 'package:pshared/utils/http/requests.dart' as httpr;
class AuthorizationService {
static final _logger = Logger('service.authorization.auth_service');
static Future<Account> login(String service, LoginData login) async {
static Future<LoginOutcome> 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(
@@ -31,7 +35,17 @@ class AuthorizationService {
LoginRequest(login: login, deviceId: deviceId, clientId: Constants.clientId).toJson(),
);
return (await _completeLogin(response)).account.toDomain();
if (response.containsKey('refreshToken')) {
return LoginOutcome.completed((await completeLogin(response)).account.toDomain());
}
if (response.containsKey('pendingToken')) {
final pending = PendingLogin.fromResponse(
PendingLoginResponse.fromJson(response),
session: SessionIdentifier(clientId: Constants.clientId, deviceId: deviceId),
);
return LoginOutcome.pending(pending);
}
throw AuthenticationFailedException('Unexpected login response', Exception(response.toString()));
}
static Future<void> _updateAccessToken(AccountResponse response) async {
@@ -49,6 +63,8 @@ class AuthorizationService {
return lr;
}
static Future<LoginResponse> completeLogin(Map<String, dynamic> response) => _completeLogin(response);
static Future<Account> restore() async {
return (await TokenService.refreshAccessToken()).account.toDomain();
}