Add account UI and secure session storage
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
abstract interface class SessionTokenStore {
|
||||
Future<String?> read();
|
||||
|
||||
Future<void> write(String token);
|
||||
|
||||
Future<void> delete();
|
||||
}
|
||||
|
||||
class SecureSessionTokenStore implements SessionTokenStore {
|
||||
SecureSessionTokenStore({FlutterSecureStorage? storage})
|
||||
: _storage = storage ?? const FlutterSecureStorage();
|
||||
|
||||
static const _key = 'sovereignai.account.access_token';
|
||||
|
||||
final FlutterSecureStorage _storage;
|
||||
|
||||
@override
|
||||
Future<String?> read() => _storage.read(key: _key);
|
||||
|
||||
@override
|
||||
Future<void> write(String token) => _storage.write(key: _key, value: token);
|
||||
|
||||
@override
|
||||
Future<void> delete() => _storage.delete(key: _key);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:http_parser/http_parser.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../auth/session_token_store.dart';
|
||||
import '../../features/chat/presentation/cubit/chat_state.dart';
|
||||
|
||||
const _uuid = Uuid();
|
||||
@@ -18,13 +19,25 @@ class AgentRunResult {
|
||||
}
|
||||
|
||||
class ApiRepository {
|
||||
ApiRepository({required String baseUrl}) : _baseUrl = baseUrl;
|
||||
ApiRepository({required String baseUrl, SessionTokenStore? sessionTokenStore})
|
||||
: _baseUrl = baseUrl,
|
||||
_sessionTokenStore = sessionTokenStore ?? SecureSessionTokenStore();
|
||||
|
||||
String _baseUrl;
|
||||
final SessionTokenStore _sessionTokenStore;
|
||||
String? _accessToken;
|
||||
Future<String>? _loadingLocalSession;
|
||||
Future<void>? _restoringSession;
|
||||
Map<String, dynamic>? _currentUser;
|
||||
bool _sessionRestored = false;
|
||||
http.Client? _activeRequestClient;
|
||||
String get baseUrl => _baseUrl;
|
||||
Map<String, dynamic>? get currentUser => _currentUser;
|
||||
String? get accountEmail {
|
||||
final user = _currentUser;
|
||||
if (user == null || user['mode'] != 'account') return null;
|
||||
return user['email'] as String?;
|
||||
}
|
||||
|
||||
void cancelActiveRequest() {
|
||||
_activeRequestClient?.close();
|
||||
@@ -37,6 +50,44 @@ class ApiRepository {
|
||||
_baseUrl = value.replaceFirst(RegExp(r'/+$'), '');
|
||||
_accessToken = null;
|
||||
_loadingLocalSession = null;
|
||||
_currentUser = null;
|
||||
_sessionRestored = false;
|
||||
_restoringSession = null;
|
||||
}
|
||||
|
||||
Future<void> initializeSession() async {
|
||||
if (_sessionRestored) return;
|
||||
final pending = _restoringSession;
|
||||
if (pending != null) return pending;
|
||||
final restoring = _restoreAccountSession();
|
||||
_restoringSession = restoring;
|
||||
try {
|
||||
await restoring;
|
||||
} finally {
|
||||
_restoringSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _restoreAccountSession() async {
|
||||
try {
|
||||
final token = await _sessionTokenStore.read();
|
||||
if (token == null || token.isEmpty) return;
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/v1/auth/me'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
await _sessionTokenStore.delete();
|
||||
return;
|
||||
}
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
_accessToken = token;
|
||||
_currentUser = data['user'] as Map<String, dynamic>?;
|
||||
} catch (_) {
|
||||
// Local single-user mode remains available if secure storage is absent.
|
||||
} finally {
|
||||
_sessionRestored = true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getModelName() async {
|
||||
@@ -64,6 +115,7 @@ class ApiRepository {
|
||||
}
|
||||
|
||||
Future<String> _localSessionToken() async {
|
||||
await initializeSession();
|
||||
final cached = _accessToken;
|
||||
if (cached != null) return cached;
|
||||
final existing = _loadingLocalSession;
|
||||
@@ -86,6 +138,7 @@ class ApiRepository {
|
||||
_checkStatus(response);
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
_accessToken = data['access_token'] as String;
|
||||
_currentUser = data['user'] as Map<String, dynamic>?;
|
||||
return _accessToken!;
|
||||
}
|
||||
|
||||
@@ -116,20 +169,56 @@ class ApiRepository {
|
||||
);
|
||||
_checkStatus(response);
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
_accessToken = data['access_token'] as String;
|
||||
final token = data['access_token'] as String;
|
||||
try {
|
||||
await _sessionTokenStore.write(token);
|
||||
} catch (error) {
|
||||
try {
|
||||
await http.post(
|
||||
Uri.parse('$_baseUrl/v1/auth/logout'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
} catch (_) {
|
||||
// The short-lived server session expires automatically if revocation fails.
|
||||
}
|
||||
throw Exception(
|
||||
'تعذر حفظ جلسة الحساب في التخزين الآمن على هذا الجهاز: $error',
|
||||
);
|
||||
}
|
||||
_accessToken = token;
|
||||
_currentUser = data['user'] as Map<String, dynamic>?;
|
||||
_sessionRestored = true;
|
||||
return data;
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await initializeSession();
|
||||
final token = _accessToken;
|
||||
if (token == null) return;
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/v1/auth/logout'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
_checkStatus(response);
|
||||
_accessToken = null;
|
||||
_loadingLocalSession = null;
|
||||
Object? logoutError;
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/v1/auth/logout'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
_checkStatus(response);
|
||||
} catch (error) {
|
||||
logoutError = error;
|
||||
} finally {
|
||||
try {
|
||||
await _sessionTokenStore.delete();
|
||||
} finally {
|
||||
_accessToken = null;
|
||||
_currentUser = null;
|
||||
_loadingLocalSession = null;
|
||||
_sessionRestored = true;
|
||||
}
|
||||
}
|
||||
if (logoutError != null) {
|
||||
throw Exception(
|
||||
'أُزيل الرمز من الجهاز، لكن تعذر تأكيد إلغاء جلسة الخادم: $logoutError',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _checkStatus(http.Response response) {
|
||||
|
||||
Reference in New Issue
Block a user