Files
Client/lib/model/account_data.dart
T
2026-05-05 21:44:23 +02:00

113 lines
4.0 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../state/app/modules/account/bloc/account_bloc.dart';
import '../state/app/modules/account/bloc/account_state.dart';
class AccountData {
static const _usernameField = 'username';
static const _passwordField = 'password';
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
static final AccountData _instance = AccountData._construct();
Completer<void> _populated = Completer();
factory AccountData() => _instance;
AccountData._construct() {
_migrateAndLoad();
}
String? _username;
String? _password;
String getUsername() {
if (_username == null) throw Exception('Username not initialized');
return _username!;
}
String getPassword() {
if (_password == null) throw Exception('Password not initialized');
return _password!;
}
String getUserSecret() =>
sha512.convert(utf8.encode('${getUsername()}:${getPassword()}')).toString();
Future<String> getDeviceId() async => sha512
.convert(utf8.encode('${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}'))
.toString();
Future<void> setData(String username, String password) async {
await _secureStorage.write(key: _usernameField, value: username);
await _secureStorage.write(key: _passwordField, value: password);
_username = username;
_password = password;
if (!_populated.isCompleted) _populated.complete();
}
Future<void> removeData({BuildContext? context}) async {
_populated = Completer();
if (context != null) {
context.read<AccountBloc>().setStatus(AccountStatus.loggedOut);
}
_username = null;
_password = null;
await _secureStorage.delete(key: _usernameField);
await _secureStorage.delete(key: _passwordField);
}
Future<void> _migrateAndLoad() async {
await _migrateFromLegacyStorage();
_username = await _secureStorage.read(key: _usernameField);
_password = await _secureStorage.read(key: _passwordField);
if (!_populated.isCompleted) _populated.complete();
}
// Move credentials from the old SharedPreferences plain-text storage into the
// platform's secure keystore. Run once per install and clear the legacy keys.
Future<void> _migrateFromLegacyStorage() async {
final prefs = await SharedPreferences.getInstance();
final legacyUsername = prefs.getString(_usernameField);
final legacyPassword = prefs.getString(_passwordField);
if (legacyUsername == null || legacyPassword == null) return;
final hasSecure = (await _secureStorage.read(key: _usernameField)) != null;
if (!hasSecure) {
await _secureStorage.write(key: _usernameField, value: legacyUsername);
await _secureStorage.write(key: _passwordField, value: legacyPassword);
}
await prefs.remove(_usernameField);
await prefs.remove(_passwordField);
}
Future<bool> waitForPopulation() async {
await _populated.future;
return isPopulated();
}
bool isPopulated() => _username != null && _password != null;
/// Returns the value for an HTTP `Authorization` header using HTTP Basic.
/// Prefer this over embedding credentials in URLs — error logs and crash
/// reports often capture the URL but not headers.
String getBasicAuthHeader() {
if (!isPopulated()) throw Exception('AccountData (e.g. username or password) is not initialized!');
return 'Basic ${base64Encode(utf8.encode('$_username:$_password'))}';
}
/// Convenience wrapper around [getBasicAuthHeader] returning a single-entry
/// header map ready to merge into HTTP request headers.
Map<String, String> authHeaders() => {'Authorization': getBasicAuthHeader()};
}