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
+36 -31
View File
@@ -3,7 +3,6 @@ import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:nextcloud/notifications.dart' show generatePushTokenHash;
import 'package:package_info_plus/package_info_plus.dart';
import '../api/demo/demo_mode.dart';
import '../api/marianumcloud/app_password/delete_app_password.dart';
@@ -11,9 +10,11 @@ import '../api/marianumcloud/app_password/get_app_password.dart';
import '../api/marianumconnect/marianumconnect_endpoint.dart';
import '../api/marianumconnect/queries/push_device_register/push_device_register.dart';
import '../api/marianumconnect/queries/push_device_unregister/push_device_unregister.dart';
import '../model/account_data.dart';
import '../model/endpoint_data.dart';
import '../session/nextcloud_credentials.dart';
import '../session/session_manager.dart';
import 'nextcloud_push_api.dart';
import 'push_device_info.dart';
import 'push_keypair.dart';
import 'push_registration_store.dart';
import 'push_registration_type.dart';
@@ -48,8 +49,6 @@ class PushRegistration {
_store = store ?? const PushRegistrationStore(),
_nextcloud = nextcloud ?? NextcloudPushApi();
String get _platform => Platform.isIOS ? 'ios' : 'android';
String get _talkUserAgent =>
Platform.isIOS ? talkUserAgentIos : talkUserAgentAndroid;
@@ -63,20 +62,26 @@ class PushRegistration {
/// slash) — persisted alongside the registration to detect endpoint changes.
String get currentNcBaseUrl => 'https://${EndpointData().nextcloud().full()}';
NextcloudCredentials? get _nextcloudOrNull =>
SessionManager().current?.nextcloud;
/// Ensures the Nextcloud app password exists (idempotent, best-effort). Push
/// registration binds to it, so it must be obtained before registering.
Future<void> ensureAppPassword() async {
if (AccountData().hasAppPassword()) return;
if (AccountData().usesLoginFlow) {
final nextcloud = _nextcloudOrNull;
if (nextcloud == null || nextcloud.hasAppPassword) return;
if (nextcloud.usesLoginFlow) {
// Flow-Konten (2FA): Basic Auth mit dem echten Passwort wird abgelehnt,
// stilles Minting ist unmöglich. Reparatur nur interaktiv über
// Einstellungen → „Nextcloud neu verbinden".
log('Push: login-flow account without app password, cannot mint silently');
log(
'Push: login-flow account without app password, cannot mint silently',
);
return;
}
try {
final appPassword = await GetAppPassword().run();
await AccountData().setAppPassword(appPassword);
await SessionManager().setAppPassword(appPassword);
} on Object catch (e) {
log('Push: could not obtain app password (non-blocking): $e');
}
@@ -85,15 +90,16 @@ class PushRegistration {
/// Ensures the second app password backing the Talk registration exists
/// (each `getapppassword` call with the real password mints a fresh one).
Future<void> ensureTalkAppPassword() async {
if (AccountData().hasAppPasswordTalk()) return;
final nextcloud = _nextcloudOrNull;
if (nextcloud == null || nextcloud.hasAppPasswordTalk) return;
// Flow-Konten können still kein zweites App-Passwort münzen — das
// Talk-Passwort kommt nur aus dem zweiten Login-Flow-Durchlauf; bis dahin
// teilt sich die Talk-Registrierung das eine App-Passwort (siehe
// AccountData.getTalkBasicAuthHeader).
if (AccountData().usesLoginFlow) return;
// NextcloudCredentials.talkBasicAuthHeader).
if (nextcloud.usesLoginFlow) return;
try {
final appPassword = await GetAppPassword().run();
await AccountData().setAppPasswordTalk(appPassword);
await SessionManager().setAppPasswordTalk(appPassword);
} on Object catch (e) {
log('Push: could not obtain talk app password (non-blocking): $e');
}
@@ -107,7 +113,8 @@ class PushRegistration {
/// fire-and-forget (and simply ignore the result).
Future<bool> register() async {
if (DemoMode.active) return false;
final epoch = AccountData().sessionEpoch;
if (_nextcloudOrNull == null) return false;
final epoch = SessionManager().sessionEpoch;
final String? fcmToken;
try {
fcmToken = await FirebaseMessaging.instance.getToken();
@@ -135,16 +142,13 @@ class PushRegistration {
return false;
}
String? appVersion;
try {
appVersion = (await PackageInfo.fromPlatform()).version;
} on Object {
appVersion = null;
}
final appVersion = await pushAppVersion();
// Re-read: the ensure* calls above may have swapped the credentials.
final nextcloud = SessionManager().requireNextcloud();
final types = registrationTypesFor(
usesLoginFlow: AccountData().usesLoginFlow,
hasTalkAppPassword: AccountData().hasAppPasswordTalk(),
usesLoginFlow: nextcloud.usesLoginFlow,
hasTalkAppPassword: nextcloud.hasAppPasswordTalk,
);
if (!types.contains(PushRegistrationType.general)) {
await _recordAttempt(
@@ -183,7 +187,7 @@ class PushRegistration {
devicePublicKeyPem: pems.publicKeyPem,
proxyServer: proxyServer,
authorizationHeader: isTalk
? AccountData().getTalkBasicAuthHeader()
? SessionManager().requireNextcloud().talkBasicAuthHeader
: null,
userAgent: isTalk ? _talkUserAgent : null,
);
@@ -191,7 +195,7 @@ class PushRegistration {
// Signed out while registering: persisting or announcing this
// registration would keep delivering the previous account's pushes,
// and logoutCleanup already ran so nothing would unregister it.
if (!AccountData().isCurrentSession(epoch)) return false;
if (!SessionManager().isCurrentSession(epoch)) return false;
await _store.save(
type: type,
@@ -207,7 +211,7 @@ class PushRegistration {
deviceIdentifierSignature: registration.signature,
userPublicKey: registration.publicKey,
pushToken: fcmToken,
platform: _platform,
platform: pushPlatform,
registrationType: type.wireName,
appVersion: appVersion,
);
@@ -256,7 +260,7 @@ class PushRegistration {
try {
final endpoint = EndpointData().nextcloud();
await _store.saveNativeAuthContext(
username: AccountData().getUsername(),
username: SessionManager().requireNextcloud().username,
baseUrl: 'https://${endpoint.full()}',
);
} on Object catch (e) {
@@ -274,7 +278,7 @@ class PushRegistration {
// session token — each registration with its own app password.
await _nextcloud.unregister(
authorizationHeader: type == PushRegistrationType.talk
? AccountData().getTalkBasicAuthHeader()
? SessionManager().requireNextcloud().talkBasicAuthHeader
: null,
);
} on Object catch (e) {
@@ -428,7 +432,7 @@ class PushRegistration {
/// is called with), then clear them locally. Ordered so the proxy stops
/// pushing before credentials are gone.
Future<void> logoutCleanup() async {
if (DemoMode.active) return;
if (DemoMode.active || _nextcloudOrNull == null) return;
await unregister();
try {
await DeleteAppPassword().run();
@@ -436,15 +440,16 @@ class PushRegistration {
log('Push: delete app password failed: $e');
}
try {
if (AccountData().hasAppPasswordTalk()) {
final nextcloud = SessionManager().requireNextcloud();
if (nextcloud.hasAppPasswordTalk) {
await DeleteAppPassword().run(
authorizationHeader: AccountData().getTalkBasicAuthHeader(),
authorizationHeader: nextcloud.talkBasicAuthHeader,
);
}
} on Object catch (e) {
log('Push: delete talk app password failed: $e');
}
await AccountData().clearAppPassword();
await AccountData().clearAppPasswordTalk();
await SessionManager().clearAppPassword();
await SessionManager().clearAppPasswordTalk();
}
}