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
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:
26
frontend/pshared/lib/api/responses/login_pending.dart
Normal file
26
frontend/pshared/lib/api/responses/login_pending.dart
Normal 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);
|
||||
}
|
||||
17
frontend/pshared/lib/models/auth/login_outcome.dart
Normal file
17
frontend/pshared/lib/models/auth/login_outcome.dart
Normal 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;
|
||||
}
|
||||
33
frontend/pshared/lib/models/auth/pending_login.dart
Normal file
33
frontend/pshared/lib/models/auth/pending_login.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
18
frontend/pshared/lib/models/session_identifier.dart
Normal file
18
frontend/pshared/lib/models/session_identifier.dart
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import 'package:pweb/providers/two_factor.dart';
|
||||
import 'package:pweb/providers/upload_history.dart';
|
||||
import 'package:pweb/providers/wallets.dart';
|
||||
import 'package:pweb/services/amplitude.dart';
|
||||
import 'package:pweb/services/auth.dart';
|
||||
import 'package:pweb/services/balance.dart';
|
||||
import 'package:pweb/services/payments/payment_methods.dart';
|
||||
import 'package:pweb/services/payments/upload_history.dart';
|
||||
@@ -53,17 +52,16 @@ void main() async {
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
Provider<AuthenticationService>(
|
||||
create: (_) => AuthenticationService(),
|
||||
),
|
||||
ChangeNotifierProxyProvider<AuthenticationService, TwoFactorProvider>(
|
||||
create: (context) => TwoFactorProvider(
|
||||
context.read<AuthenticationService>(),
|
||||
),
|
||||
update: (context, authService, previous) => TwoFactorProvider(authService),
|
||||
),
|
||||
ChangeNotifierProvider(create: (_) => LocaleProvider(null)),
|
||||
ChangeNotifierProvider(create: (_) => AccountProvider()),
|
||||
ChangeNotifierProxyProvider<AccountProvider, TwoFactorProvider>(
|
||||
create: (context) => TwoFactorProvider(
|
||||
accountProvider: context.read<AccountProvider>(),
|
||||
),
|
||||
update: (context, accountProvider, previous) => TwoFactorProvider(
|
||||
accountProvider: accountProvider,
|
||||
),
|
||||
),
|
||||
ChangeNotifierProvider(create: (_) => OrganizationsProvider()),
|
||||
ChangeNotifierProvider(create: (_) => AccountProvider()),
|
||||
ChangeNotifierProvider(create: (_) => CarouselIndexProvider()),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
import 'package:pweb/providers/two_factor.dart';
|
||||
|
||||
|
||||
class ResendCodeButton extends StatelessWidget {
|
||||
@@ -12,9 +15,7 @@ class ResendCodeButton extends StatelessWidget {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
// TODO: Add resend logic
|
||||
},
|
||||
onPressed: () => context.read<TwoFactorProvider>().resendCode(),
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: const Size(0, 0),
|
||||
@@ -28,4 +29,4 @@ class ResendCodeButton extends StatelessWidget {
|
||||
child: Text(localizations.twoFactorResend),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,13 +38,16 @@ class _LoginFormState extends State<LoginForm> {
|
||||
final provider = Provider.of<AccountProvider>(context, listen: false);
|
||||
|
||||
try {
|
||||
//final account =
|
||||
await provider.login(
|
||||
final outcome = await provider.login(
|
||||
email: _usernameController.text,
|
||||
password: _passwordController.text,
|
||||
locale: context.read<LocaleProvider>().locale.languageCode,
|
||||
);
|
||||
onLogin();
|
||||
if (outcome.isPending) {
|
||||
navigateAndReplace(context, Pages.sfactor);
|
||||
} else {
|
||||
onLogin();
|
||||
}
|
||||
return 'ok';
|
||||
} catch (e) {
|
||||
onError(provider.error ?? e);
|
||||
@@ -92,7 +95,7 @@ class _LoginFormState extends State<LoginForm> {
|
||||
onSignUp: () => navigate(context, Pages.signup),
|
||||
login: () => _login(
|
||||
context,
|
||||
() => navigateAndReplace(context, Pages.sfactor),
|
||||
() => navigateAndReplace(context, Pages.dashboard),
|
||||
(e) => postNotifyUserOfErrorX(
|
||||
context: context,
|
||||
errorSituation: AppLocalizations.of(context)!.errorLogin,
|
||||
|
||||
@@ -1,38 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'package:pweb/services/auth.dart';
|
||||
|
||||
import 'package:pshared/models/auth/pending_login.dart';
|
||||
import 'package:pshared/provider/account.dart';
|
||||
import 'package:pshared/service/account.dart';
|
||||
|
||||
class TwoFactorProvider extends ChangeNotifier {
|
||||
final AuthenticationService _authService;
|
||||
static final _logger = Logger('provider.two_factor');
|
||||
final AccountProvider _accountProvider;
|
||||
|
||||
TwoFactorProvider(this._authService);
|
||||
TwoFactorProvider({required AccountProvider accountProvider}) : _accountProvider = accountProvider;
|
||||
|
||||
bool _isSubmitting = false;
|
||||
bool _hasError = false;
|
||||
bool _verificationSuccess = false;
|
||||
String? _errorMessage;
|
||||
|
||||
bool get isSubmitting => _isSubmitting;
|
||||
bool get hasError => _hasError;
|
||||
bool get verificationSuccess => _verificationSuccess;
|
||||
String? get errorMessage => _errorMessage;
|
||||
PendingLogin? get pendingLogin => _accountProvider.pendingLogin;
|
||||
|
||||
|
||||
Future<void> submitCode(String code) async {
|
||||
_isSubmitting = true;
|
||||
_hasError = false;
|
||||
_errorMessage = null;
|
||||
_verificationSuccess = false;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final success = await _authService.verifyTwoFactorCode(code);
|
||||
if (success) {
|
||||
_verificationSuccess = true;
|
||||
final pending = _accountProvider.pendingLogin;
|
||||
if (pending == null) {
|
||||
throw Exception('No pending login available');
|
||||
}
|
||||
final account = await AccountService.confirmLoginCode(
|
||||
pending: pending,
|
||||
code: code,
|
||||
);
|
||||
_accountProvider.completePendingLogin(account);
|
||||
_verificationSuccess = true;
|
||||
} catch (e) {
|
||||
_hasError = true;
|
||||
_errorMessage = e.toString();
|
||||
_logger.warning('Failed to verify code', e);
|
||||
} finally {
|
||||
_isSubmitting = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resendCode() async {
|
||||
final pending = _accountProvider.pendingLogin;
|
||||
if (pending == null) {
|
||||
_logger.warning('No pending login to resend code for');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await AccountService.resendLoginCode(pending);
|
||||
} catch (e) {
|
||||
_logger.warning('Failed to resend login code', e);
|
||||
_hasError = true;
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
class AuthenticationService {
|
||||
Future<bool> verifyTwoFactorCode(String code) async {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
if (code == '000000') {
|
||||
return true;
|
||||
} else {
|
||||
throw Exception('Wrong Code'); //TODO Localize
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import Foundation
|
||||
import amplitude_flutter
|
||||
import file_selector_macos
|
||||
import flutter_timezone
|
||||
import path_provider_foundation
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
@@ -18,7 +17,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AmplitudeFlutterPlugin.register(with: registry.registrar(forPlugin: "AmplitudeFlutterPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
|
||||
Reference in New Issue
Block a user