replaced account data singleton with a session model

This commit is contained in:
2026-09-27 14:49:38 +02:00
parent 79b33ab248
commit 5e65c4671a
60 changed files with 1019 additions and 679 deletions
+2 -2
View File
@@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import '../../model/account_data.dart';
import '../../session/session_manager.dart';
import '../../theming/light_app_theme.dart';
import '../../widget/app_progress_indicator.dart';
@@ -61,7 +61,7 @@ class _AccountLoadingScreenState extends State<AccountLoadingScreen> {
),
const SizedBox(height: 8),
TextButton(
onPressed: AccountData().abandonLoad,
onPressed: SessionManager().abandonLoad,
style: TextButton.styleFrom(foregroundColor: Colors.white),
child: const Text('Zur Anmeldung'),
),
+17
View File
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../background/widget_background_task.dart';
import '../../session/session_lifecycle.dart';
import '../../state/app/modules/account/bloc/account_bloc.dart';
import '../../state/app/modules/account/bloc/account_state.dart';
import '../../state/app/modules/settings/bloc/settings_cubit.dart';
@@ -11,6 +12,7 @@ import '../../storage/dev_tools_settings.dart';
import '../../storage/settings.dart' as model;
import '../../theming/light_app_theme.dart';
import '../../utils/haptics.dart';
import '../../widget/info_dialog.dart';
import '../pages/settings/widgets/endpoint_picker.dart';
import 'login_controller.dart';
import 'post_login_splash.dart';
@@ -34,6 +36,21 @@ class _LoginState extends State<Login> with SingleTickerProviderStateMixin {
value: 1,
);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _showSignOutNotice());
}
/// An involuntary sign-out (expired token, rotated password) otherwise just
/// drops the user here without a word.
void _showSignOutNotice() {
final notice = SessionLifecycle.signOutNotice.value;
if (notice == null || !mounted) return;
SessionLifecycle.signOutNotice.value = null;
InfoDialog.show(context, notice, title: 'Erneut anmelden');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
+28 -18
View File
@@ -10,8 +10,9 @@ 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 '../../model/session_wipe.dart';
import '../../session/session.dart';
import '../../session/session_manager.dart';
import '../../widget_data/widget_sync.dart';
/// Outcome of a login attempt.
@@ -56,25 +57,18 @@ class LoginController extends ChangeNotifier {
// 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);
await _discardPreviousAccount();
await SessionManager().signIn(
CredentialSession(username: user, password: 'demo', isDemo: true),
);
_loading = false;
notifyListeners();
return LoginResult.success;
}
var signedIn = false;
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();
await _discardPreviousAccount();
// AuthLogin = Credential-Probe + Token-Create in einem Call.
// 401 hier heißt: falsches Passwort.
await AuthLogin().run(
@@ -82,7 +76,10 @@ class LoginController extends ChangeNotifier {
password: password,
tokenName: await DeviceTokenName.resolve(),
);
await AccountData().setData(user, password);
await SessionManager().signIn(
CredentialSession(username: user, password: password),
);
signedIn = true;
// 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.
@@ -92,7 +89,9 @@ class LoginController extends ChangeNotifier {
return ncReady ? LoginResult.success : LoginResult.nextcloudLoginRequired;
} catch (e) {
log(e.toString());
await AccountData().removeData();
// Only the account signed in by this attempt; while adding an account
// the active one is parked and restored on cancel.
if (signedIn) await SessionManager().signOut();
await const MarianumConnectTokenStorage().clear();
final isWrongCredentials = e is AuthException && e.statusCode == 401;
_errorMessage = isWrongCredentials
@@ -105,6 +104,17 @@ class LoginController extends ChangeNotifier {
}
}
/// Vorherigen Token verwerfen, bevor ein neuer angefordert wird, und den
/// Widget-Snapshot löschen — sonst blitzt nach einem Account-Wechsel kurz
/// der Stundenplan des vorigen Users auf dem Home-Bildschirm. Die Session
/// selbst überschreibt signIn vollständig; beim Hinzufügen eines Kontos
/// liegt der Token des aktiven Kontos schon in dessen Tresor.
Future<void> _discardPreviousAccount() async {
await const MarianumConnectTokenStorage().clear();
await WidgetSync.clear();
await WidgetSync.triggerUpdate();
}
/// 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
@@ -112,7 +122,7 @@ class LoginController extends ChangeNotifier {
Future<bool> _prepareNextcloudAppPassword() async {
try {
final appPassword = await GetAppPassword().run();
await AccountData().setAppPassword(appPassword);
await SessionManager().setAppPassword(appPassword);
return true;
} on AuthException {
return false;
@@ -131,7 +141,7 @@ class LoginController extends ChangeNotifier {
} on Object catch (e) {
log('Login rollback: MC logout failed: $e');
}
await AccountData().removeData();
await SessionManager().signOut();
await const MarianumConnectTokenStorage().clear();
_errorMessage =
'Die Anmeldung wurde abgebrochen — dein Konto erfordert die Bestätigung im Browser.';
@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
import '../../api/errors/error_mapper.dart';
import '../../api/marianumcloud/app_password/delete_app_password.dart';
import '../../api/marianumcloud/login_flow/login_flow_api.dart';
import '../../model/account_data.dart';
import '../../session/session_manager.dart';
import '../../utils/url_opener.dart';
import '../../widget/app_progress_indicator.dart';
@@ -19,7 +19,7 @@ enum _FlowStep { primary, talk }
/// Runs the Nextcloud Login Flow v2: opens the browser login, polls until the
/// user confirmed it there (2FA happens inside the browser) and adopts the
/// returned app password via [AccountData.setLoginFlow]. A second, skippable
/// returned app password via [SessionManager.setLoginFlow]. A second, skippable
/// pass mints the Talk app password so flow accounts keep BOTH push
/// subscriptions (see PushRegistrationType). Pops `true` once the primary
/// credential was adopted, `false`/`null` when the user backs out before that.
@@ -104,7 +104,7 @@ class _NextcloudLoginFlowPageState extends State<NextcloudLoginFlowPage>
final credentials = await _api.poll(flow);
if (credentials == null || _finished || !mounted) return;
if (!LoginFlowApi.loginNameMatches(
expected: AccountData().getUsername(),
expected: SessionManager().requireNextcloud().username,
actual: credentials.loginName,
)) {
_timer?.cancel();
@@ -120,7 +120,7 @@ class _NextcloudLoginFlowPageState extends State<NextcloudLoginFlowPage>
}
switch (_step) {
case _FlowStep.primary:
await AccountData().setLoginFlow(credentials.appPassword);
await SessionManager().setLoginFlow(credentials.appPassword);
if (!mounted) return;
// Zweite Freigabe direkt anstoßen: die Browser-Session besteht
// bereits, es fehlt nur noch der Grant-Tipp.
@@ -129,7 +129,7 @@ class _NextcloudLoginFlowPageState extends State<NextcloudLoginFlowPage>
case _FlowStep.talk:
_finished = true;
_timer?.cancel();
await AccountData().setAppPasswordTalk(credentials.appPassword);
await SessionManager().setAppPasswordTalk(credentials.appPassword);
if (!mounted) return;
Navigator.of(context).pop(true);
}