import 'dart:convert'; /// Nextcloud identity of a session. Immutable; the session manager swaps in a /// new instance whenever an app password is minted or revoked. class NextcloudCredentials { final String username; /// The real account password. Invalid against Nextcloud when /// [usesLoginFlow] is set (2FA accounts), where only [appPassword] works. final String password; final String? appPassword; /// Backs the second (apptype=talk) push registration — Nextcloud binds each /// push subscription to its session token, so two registrations need two /// app passwords. final String? appPasswordTalk; /// True when the credentials came from Login Flow v2 (browser login, e.g. /// because the account has two-factor authentication). final bool usesLoginFlow; const NextcloudCredentials({ required this.username, required this.password, this.appPassword, this.appPasswordTalk, this.usesLoginFlow = false, }); bool get hasAppPassword => appPassword != null && appPassword!.isNotEmpty; bool get hasAppPasswordTalk => appPasswordTalk != null && appPasswordTalk!.isNotEmpty; /// The app password once available (minted or flow-issued), otherwise the /// real password. It survives real-password rotation and is what the push /// registration is bound to. String get secret => hasAppPassword ? appPassword! : password; /// HTTP Basic header value. Prefer headers over credentials in URLs — error /// logs and crash reports often capture the URL but not headers. String get basicAuthHeader => _basicAuth(secret); Map get authHeaders => {'Authorization': basicAuthHeader}; /// Authenticates the apptype=talk push registration (and its unregister). /// Throws when the talk password has not been minted yet; callers treat that /// as a failed talk registration and retry on the next start. String get talkBasicAuthHeader { if (hasAppPasswordTalk) return _basicAuth(appPasswordTalk!); // Login-flow account whose second (talk) flow pass was skipped: no silent // minting possible, the talk registration shares the flow credential. if (usesLoginFlow && hasAppPassword) return _basicAuth(appPassword!); throw StateError('Talk app password not available yet'); } /// Always the real password. Needed to mint the app password via /// `core/getapppassword` — an app password cannot mint another. String get realPasswordBasicAuthHeader => _basicAuth(password); NextcloudCredentials copyWith({ String? Function()? appPassword, String? Function()? appPasswordTalk, bool? usesLoginFlow, }) => NextcloudCredentials( username: username, password: password, appPassword: appPassword != null ? appPassword() : this.appPassword, appPasswordTalk: appPasswordTalk != null ? appPasswordTalk() : this.appPasswordTalk, usesLoginFlow: usesLoginFlow ?? this.usesLoginFlow, ); String _basicAuth(String secret) => 'Basic ${base64Encode(utf8.encode('$username:$secret'))}'; }