import 'dart:developer'; 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 '../../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 { bool _loading = false; String? _errorMessage; String? _errorDetails; bool get loading => _loading; String? get errorMessage => _errorMessage; String? get errorDetails => _errorDetails; Future submit(String username, String password) async { if (_loading) return LoginResult.failure; _loading = true; _errorMessage = null; _errorDetails = null; notifyListeners(); final user = username.trim().toLowerCase(); // Demo login: the prefix enters local demo mode, password ignored, no // network (see DemoMode). if (DemoMode.matches(user)) { await AccountData().removeData(); await const MarianumConnectTokenStorage().clear(); await WidgetSync.clear(); await WidgetSync.triggerUpdate(); await AccountData().setDemo(user); _loading = false; notifyListeners(); return LoginResult.success; } try { await AccountData().removeData(); // Vorherigen Token revoken bevor wir einen neuen anfordern — ein altes // Account hätte sonst noch einen aktiven Token in api_tokens. await const MarianumConnectTokenStorage().clear(); // Widget-Snapshot löschen, sonst blitzt nach Account-Wechsel kurz der // Stundenplan des vorigen Users auf dem Home-Bildschirm. await WidgetSync.clear(); await WidgetSync.triggerUpdate(); // AuthLogin = Credential-Probe + Token-Create in einem Call. // 401 hier heißt: falsches Passwort. await AuthLogin().run( username: user, password: password, tokenName: await DeviceTokenName.resolve(), ); await AccountData().setData(user, password); // 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 ncReady ? LoginResult.success : LoginResult.nextcloudLoginRequired; } catch (e) { log(e.toString()); await AccountData().removeData(); await const MarianumConnectTokenStorage().clear(); final isWrongCredentials = e is AuthException && e.statusCode == 401; _errorMessage = isWrongCredentials ? 'Benutzername oder Passwort falsch.' : errorToUserMessage(e); _errorDetails = errorToTechnicalDetails(e); _loading = false; notifyListeners(); 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 _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 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(); } }