added support for 2fa login with browser flow
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -6,13 +5,29 @@ import 'package:flutter/foundation.dart';
|
||||
import '../../api/demo/demo_mode.dart';
|
||||
import '../../api/errors/auth_exception.dart';
|
||||
import '../../api/errors/error_mapper.dart';
|
||||
import '../../api/marianumcloud/app_password/get_app_password.dart';
|
||||
import '../../api/marianumconnect/auth/device_token_name.dart';
|
||||
import '../../api/marianumconnect/auth/token_storage.dart';
|
||||
import '../../api/marianumconnect/queries/auth_login/auth_login.dart';
|
||||
import '../../api/marianumconnect/queries/auth_logout/auth_logout.dart';
|
||||
import '../../model/account_data.dart';
|
||||
import '../../push/push_registration.dart';
|
||||
import '../../widget_data/widget_sync.dart';
|
||||
|
||||
/// Outcome of a login attempt.
|
||||
enum LoginResult {
|
||||
/// Fully logged in — the view transitions to `loggedIn`.
|
||||
success,
|
||||
|
||||
/// Credentials rejected or a transport problem; the error is exposed via
|
||||
/// [LoginController.errorMessage].
|
||||
failure,
|
||||
|
||||
/// MarianumConnect accepted the credentials, but Nextcloud rejects them
|
||||
/// (two-factor authentication active or diverging password). The view must
|
||||
/// complete the Nextcloud Login Flow v2 in the browser before proceeding.
|
||||
nextcloudLoginRequired,
|
||||
}
|
||||
|
||||
/// Owns the login flow's transient state (loading, last error) so it can be
|
||||
/// driven from a thin Stateful view and unit-tested without a widget tree.
|
||||
class LoginController extends ChangeNotifier {
|
||||
@@ -24,10 +39,8 @@ class LoginController extends ChangeNotifier {
|
||||
String? get errorMessage => _errorMessage;
|
||||
String? get errorDetails => _errorDetails;
|
||||
|
||||
/// Returns `true` when the credential probe succeeded. The view should
|
||||
/// then transition the AccountBloc to `loggedIn`.
|
||||
Future<bool> submit(String username, String password) async {
|
||||
if (_loading) return false;
|
||||
Future<LoginResult> submit(String username, String password) async {
|
||||
if (_loading) return LoginResult.failure;
|
||||
_loading = true;
|
||||
_errorMessage = null;
|
||||
_errorDetails = null;
|
||||
@@ -45,7 +58,7 @@ class LoginController extends ChangeNotifier {
|
||||
await AccountData().setDemo(user);
|
||||
_loading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
return LoginResult.success;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -65,13 +78,13 @@ class LoginController extends ChangeNotifier {
|
||||
tokenName: await DeviceTokenName.resolve(),
|
||||
);
|
||||
await AccountData().setData(user, password);
|
||||
// Mint the Nextcloud app password now so it's ready for the push
|
||||
// registration and subsequent NC calls. Non-blocking: on failure push
|
||||
// stays off and retries on the next start.
|
||||
unawaited(PushRegistration().ensureAppPassword());
|
||||
// Mint the Nextcloud app password now — it doubles as the Nextcloud
|
||||
// credential probe: a rejection means 2FA is active (or the NC password
|
||||
// diverges) and the login must finish interactively in the browser.
|
||||
final ncReady = await _prepareNextcloudAppPassword();
|
||||
_loading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
return ncReady ? LoginResult.success : LoginResult.nextcloudLoginRequired;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
await AccountData().removeData();
|
||||
@@ -83,7 +96,41 @@ class LoginController extends ChangeNotifier {
|
||||
_errorDetails = errorToTechnicalDetails(e);
|
||||
_loading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
return LoginResult.failure;
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to mint the Nextcloud app password with the just-verified password.
|
||||
/// `false` = Nextcloud rejected the credentials → Login Flow v2 required.
|
||||
/// Transport/server problems stay non-blocking (like the previous
|
||||
/// fire-and-forget mint): the mint retries with the push registration.
|
||||
Future<bool> _prepareNextcloudAppPassword() async {
|
||||
try {
|
||||
final appPassword = await GetAppPassword().run();
|
||||
await AccountData().setAppPassword(appPassword);
|
||||
return true;
|
||||
} on AuthException {
|
||||
return false;
|
||||
} on Object catch (e) {
|
||||
log('Nextcloud app password mint failed (non-blocking): $e');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rolls the half-finished login back after the user cancelled the
|
||||
/// Nextcloud browser login: revoke the fresh MarianumConnect token and wipe
|
||||
/// the stored credentials, then surface why the login did not complete.
|
||||
Future<void> abortNextcloudLogin() async {
|
||||
try {
|
||||
await AuthLogout().run();
|
||||
} on Object catch (e) {
|
||||
log('Login rollback: MC logout failed: $e');
|
||||
}
|
||||
await AccountData().removeData();
|
||||
await const MarianumConnectTokenStorage().clear();
|
||||
_errorMessage =
|
||||
'Die Anmeldung wurde abgebrochen — dein Konto erfordert die Bestätigung im Browser.';
|
||||
_errorDetails = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user