650 lines
20 KiB
Dart
650 lines
20 KiB
Dart
import 'package:chepuhagram/data/datasources/local_db_service.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
import 'dart:convert';
|
||
import 'dart:io';
|
||
import '/core/constants.dart';
|
||
import 'package:http/http.dart' as http;
|
||
import 'package:chepuhagram/domain/services/api_service.dart';
|
||
import 'package:chepuhagram/data/datasources/ws_client.dart';
|
||
import 'package:chepuhagram/domain/services/crypto_service.dart';
|
||
import 'package:chepuhagram/logic/contact_provider.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:path/path.dart' as p;
|
||
import 'package:chepuhagram/data/models/session_model.dart';
|
||
import 'package:chepuhagram/presentation/screens/splash_screen.dart';
|
||
import 'package:chepuhagram/presentation/screens/login_screen.dart';
|
||
import 'package:chepuhagram/main.dart';
|
||
|
||
class AuthProvider extends ChangeNotifier {
|
||
bool _isLoading = false;
|
||
bool get isLoading => _isLoading;
|
||
|
||
String? _error;
|
||
String? get error => _error;
|
||
|
||
int? _currentUserId;
|
||
int? get currentUserId => _currentUserId;
|
||
|
||
String? _username;
|
||
String? get username => _username;
|
||
|
||
String? _firstName;
|
||
String? get firstName => _firstName;
|
||
|
||
String? _lastName;
|
||
String? get lastName => _lastName;
|
||
|
||
String? _phone;
|
||
String? get phone => _phone;
|
||
|
||
String? _email;
|
||
String? get email => _email;
|
||
|
||
String? _about;
|
||
String? get about => _about;
|
||
|
||
String? _avatarPath;
|
||
String? get avatarPath => _avatarPath;
|
||
|
||
String? _avatarUrl;
|
||
String? get avatarUrl => _avatarUrl;
|
||
|
||
// Privacy settings
|
||
bool? _showEmail;
|
||
bool? get showEmail => _showEmail;
|
||
|
||
bool? _showPhone;
|
||
bool? get showPhone => _showPhone;
|
||
|
||
bool? _showAvatar;
|
||
bool? get showAvatar => _showAvatar;
|
||
|
||
bool? _showAbout;
|
||
bool? get showAbout => _showAbout;
|
||
|
||
bool? _showUsername;
|
||
bool? get showUsername => _showUsername;
|
||
|
||
List<Session> _sessions = [];
|
||
List<Session> get sessions => _sessions;
|
||
|
||
String get displayName {
|
||
final full = '${_firstName ?? ''} ${_lastName ?? ''}'.trim();
|
||
if (full.isNotEmpty) return full;
|
||
if ((_username ?? '').isNotEmpty) return _username!;
|
||
return 'User';
|
||
}
|
||
|
||
// Флаги для определения пути пользователя
|
||
bool _needsSetup = false;
|
||
bool get needsSetup => _needsSetup;
|
||
|
||
bool _needsKeyRecovery = false;
|
||
bool get needsKeyRecovery => _needsKeyRecovery;
|
||
|
||
bool _hasPublicKeyOnServer = false;
|
||
bool get hasPublicKeyOnServer => _hasPublicKeyOnServer;
|
||
|
||
final _storage = const FlutterSecureStorage();
|
||
FlutterSecureStorage get storage => _storage;
|
||
|
||
final _client = http.Client();
|
||
final ApiService _apiService = ApiService();
|
||
final SocketService _socketService = SocketService();
|
||
final CryptoService _cryptoService = CryptoService();
|
||
|
||
void clearError() {
|
||
_error = null;
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> initRealtime() async {
|
||
try {
|
||
await _socketService.connect(_apiService);
|
||
_socketService.messages.listen((message) async {
|
||
if (message['type'] == 'session_terminated') {
|
||
print('Сессия завершена');
|
||
await logout();
|
||
}
|
||
});
|
||
} catch (e) {
|
||
throw Exception(e);
|
||
}
|
||
}
|
||
|
||
Future<void> logout() async {
|
||
if (_isLoading) return;
|
||
_isLoading = true;
|
||
notifyListeners();
|
||
|
||
// 1. Отзываем токен на сервере
|
||
try {
|
||
await ApiService().logoutCurrentUser();
|
||
} catch (e) {
|
||
print("Сервер не ответил на logout: $e");
|
||
}
|
||
|
||
// 2. Отключаем WebSocket connection
|
||
try {
|
||
SocketService().disconnect();
|
||
} catch (e) {
|
||
print("Ошибка при отключении сокета: $e");
|
||
}
|
||
|
||
String? mode;
|
||
String? color;
|
||
try {
|
||
mode = await _storage.read(key: 'theme_mode');
|
||
color = await _storage.read(key: 'accent_color');
|
||
} catch (e) {
|
||
print("Ошибка чтения настроек темы: $e");
|
||
}
|
||
|
||
try {
|
||
await _storage.deleteAll();
|
||
} catch (e) {
|
||
print("Ошибка очистки SecureStorage: $e");
|
||
// Пытаемся удалить хотя бы авторизационные токены
|
||
try {
|
||
await _storage.delete(key: 'access_token');
|
||
await _storage.delete(key: 'refresh_token');
|
||
await _storage.delete(key: 'private_key');
|
||
} catch (ex) {
|
||
print("Ошибка удаления отдельных токенов: $ex");
|
||
}
|
||
}
|
||
|
||
try {
|
||
CryptoService.clearCache();
|
||
} catch (e) {
|
||
print("Ошибка очистки кэша криптографии: $e");
|
||
}
|
||
|
||
final context = navigatorKey.currentContext;
|
||
if (context != null) {
|
||
try {
|
||
Provider.of<ContactProvider>(context, listen: false).clearCache();
|
||
} catch (e) {
|
||
print("Error clearing contact provider cache: $e");
|
||
}
|
||
}
|
||
|
||
try {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.clear();
|
||
} catch (e) {
|
||
print("Ошибка очистки SharedPreferences: $e");
|
||
}
|
||
|
||
try {
|
||
await LocalDbService().clearDatabase();
|
||
} catch (e) {
|
||
print("Ошибка очистки локальной базы данных: $e");
|
||
}
|
||
|
||
_currentUserId = null;
|
||
_username = null;
|
||
_firstName = null;
|
||
_lastName = null;
|
||
_phone = null;
|
||
_email = null;
|
||
_about = null;
|
||
_avatarPath = null;
|
||
_avatarUrl = null;
|
||
|
||
try {
|
||
if (mode != null) {
|
||
await _storage.write(key: 'theme_mode', value: mode);
|
||
}
|
||
if (color != null) {
|
||
await _storage.write(key: 'accent_color', value: color);
|
||
}
|
||
} catch (e) {
|
||
print("Ошибка записи настроек темы: $e");
|
||
}
|
||
|
||
// 3. Уничтожаем локальную базу данных переписки (Drift)
|
||
try {
|
||
final dbFolder = await databaseFactory.getDatabasesPath();
|
||
final dbFile = File(p.join(dbFolder, 'chat_app.db'));
|
||
if (await dbFile.exists()) {
|
||
await dbFile.delete();
|
||
print("БАЗА ДАННЫХ УСПЕШНО УНИЧТОЖЕНА В ЦЕЛЯХ БЕЗОПАСНОСТИ");
|
||
}
|
||
final journalFile = File(p.join(dbFolder, 'chat_app.db-journal'));
|
||
if (await journalFile.exists()) {
|
||
await journalFile.delete();
|
||
}
|
||
final walFile = File(p.join(dbFolder, 'chat_app.db-wal'));
|
||
if (await walFile.exists()) {
|
||
await walFile.delete();
|
||
}
|
||
final shmFile = File(p.join(dbFolder, 'chat_app.db-shm'));
|
||
if (await shmFile.exists()) {
|
||
await shmFile.delete();
|
||
}
|
||
} catch (e) {
|
||
print("Ошибка удаления файлов БД: $e");
|
||
}
|
||
|
||
_isLoading = false;
|
||
notifyListeners();
|
||
|
||
// Перенаправляем напрямую на LoginScreen во избежание циклического автологина
|
||
navigatorKey.currentState?.pushAndRemoveUntil(
|
||
PageRouteBuilder(
|
||
pageBuilder: (context, animation, secondaryAnimation) =>
|
||
const LoginScreen(),
|
||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||
return FadeTransition(opacity: animation, child: child);
|
||
},
|
||
),
|
||
(route) => false,
|
||
);
|
||
}
|
||
|
||
Future<void> fetchSessions({bool silent = false}) async {
|
||
if (!silent) {
|
||
_isLoading = true;
|
||
_error = null;
|
||
notifyListeners();
|
||
}
|
||
try {
|
||
final data = await _apiService.getActiveSessions();
|
||
print('Fetched sessions: $data');
|
||
_sessions = data.map((json) => Session.fromJson(json)).toList();
|
||
_error = null;
|
||
} catch (e) {
|
||
if (!silent) {
|
||
_error = 'Произошла ошибка при загрузке сессий: $e';
|
||
}
|
||
print('Ошибка при загрузке сессий: $e');
|
||
} finally {
|
||
if (!silent) {
|
||
_isLoading = false;
|
||
}
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
Future<bool> revokeSession(int sessionId) async {
|
||
try {
|
||
final success = await _apiService.revokeSession(sessionId);
|
||
if (success) {
|
||
await fetchSessions();
|
||
return true;
|
||
} else {
|
||
_error = 'Не удалось завершить сессию.';
|
||
notifyListeners();
|
||
return false;
|
||
}
|
||
} catch (e) {
|
||
_error = 'Произошла ошибка при завершении сессии: $e';
|
||
notifyListeners();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
Future<bool> revokeAllOtherSessions() async {
|
||
try {
|
||
final success = await _apiService.clearOtherSessions();
|
||
if (success) {
|
||
await fetchSessions();
|
||
return true;
|
||
} else {
|
||
_error = 'Не удалось завершить другие сессии.';
|
||
notifyListeners();
|
||
return false;
|
||
}
|
||
} catch (e) {
|
||
_error = 'Произошла ошибка при завершении других сессий: $e';
|
||
notifyListeners();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
void closeRealtime() {
|
||
_socketService.disconnect();
|
||
}
|
||
|
||
SocketService get socketService => _socketService;
|
||
|
||
Future<bool> login(
|
||
String username,
|
||
String password, {
|
||
String? totpCode,
|
||
}) async {
|
||
_isLoading = true;
|
||
notifyListeners();
|
||
|
||
try {
|
||
final deviceName = await _apiService.getDeviceName();
|
||
|
||
final body = {
|
||
'username': username,
|
||
'password': password,
|
||
'device_name': deviceName,
|
||
};
|
||
if (totpCode != null) {
|
||
body['totp_code'] = totpCode;
|
||
}
|
||
final response = await _client.post(
|
||
Uri.parse('${AppConstants.baseUrl}/auth/login'),
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Device-Name': deviceName,
|
||
},
|
||
body: jsonEncode(body),
|
||
);
|
||
|
||
final decodedResponse =
|
||
jsonDecode(utf8.decode(response.bodyBytes)) as Map;
|
||
|
||
if (response.statusCode == 200) {
|
||
await _storage.write(
|
||
key: 'access_token',
|
||
value: decodedResponse['access_token'],
|
||
);
|
||
await _storage.write(
|
||
key: 'refresh_token',
|
||
value: decodedResponse['refresh_token'],
|
||
);
|
||
await _storage.write(
|
||
key: 'user_id',
|
||
value: decodedResponse['user_id'].toString(),
|
||
);
|
||
_currentUserId = decodedResponse['user_id'];
|
||
|
||
// Проверяем статус аккаунта (нужна ли настройка или восстановление)
|
||
await _checkAccountStatus();
|
||
|
||
_isLoading = false;
|
||
notifyListeners();
|
||
return true;
|
||
} else {
|
||
_isLoading = false;
|
||
notifyListeners();
|
||
final error = decodedResponse['detail'] ?? 'Ошибка запроса';
|
||
throw Exception(error);
|
||
}
|
||
} catch (e) {
|
||
_isLoading = false;
|
||
notifyListeners();
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<void> loginWithQrData({
|
||
required String accessToken,
|
||
required String refreshToken,
|
||
required String userId,
|
||
required String privateKey,
|
||
}) async {
|
||
_isLoading = true;
|
||
notifyListeners();
|
||
|
||
try {
|
||
await _storage.write(key: 'access_token', value: accessToken);
|
||
await _storage.write(key: 'refresh_token', value: refreshToken);
|
||
await _storage.write(key: 'user_id', value: userId);
|
||
await _storage.write(key: 'private_key', value: privateKey);
|
||
|
||
_currentUserId = int.tryParse(userId);
|
||
|
||
await _checkAccountStatus();
|
||
} catch (e) {
|
||
print("Ошибка авторизации по QR: $e");
|
||
rethrow;
|
||
} finally {
|
||
_isLoading = false;
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
Future<bool> tryAutoLogin() async {
|
||
String? token;
|
||
try {
|
||
token = await _apiService.getAccessToken(forceRefresh: true);
|
||
} catch (e) {
|
||
throw Exception('$e+_aup_tal_1');
|
||
}
|
||
if (token == null) return false;
|
||
|
||
// Загружаем currentUserId из хранилища
|
||
/*final userIdStr = await _storage.read(key: 'user_id');
|
||
if (userIdStr != null) {
|
||
_currentUserId = int.tryParse(userIdStr);
|
||
}
|
||
|
||
try {
|
||
final response = await _client
|
||
.get(
|
||
Uri.parse('${AppConstants.baseUrl}/users/me'),
|
||
headers: {'Authorization': 'Bearer $token'},
|
||
)
|
||
.timeout(const Duration(seconds: 5));
|
||
|
||
if (response.statusCode == 200) {
|
||
// Проверяем статус аккаунта для определения дальнейшего пути
|
||
await _checkAccountStatus();
|
||
return true;
|
||
} else if (response.statusCode == 401) {
|
||
bool isUpdated = await _apiService.refreshToken();
|
||
if (isUpdated) {
|
||
// После обновления токена проверяем статус
|
||
await _checkAccountStatus();
|
||
}
|
||
return isUpdated;
|
||
} else {
|
||
return false;
|
||
}
|
||
} catch (e) {
|
||
// Если сервер недоступен, позволяем offline mode
|
||
return true;
|
||
}*/
|
||
return true;
|
||
}
|
||
|
||
Future<bool> setupAccount(
|
||
String firstName,
|
||
String? lastName,
|
||
String masterPassword,
|
||
) async {
|
||
notifyListeners();
|
||
|
||
try {
|
||
final token = await _apiService.getAccessToken();
|
||
|
||
// Генерируем ключи и шифруем приватный
|
||
final keys = await _cryptoService.initAccountSecurity(masterPassword);
|
||
|
||
final response = await _client.post(
|
||
Uri.parse('${AppConstants.baseUrl}/auth/setup-account'),
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': 'Bearer $token',
|
||
},
|
||
body: jsonEncode({
|
||
'first_name': firstName,
|
||
'last_name': lastName,
|
||
'public_key': keys['public_key'],
|
||
'encrypted_private_key': keys['encrypted_private_key'],
|
||
}),
|
||
);
|
||
|
||
if (response.statusCode == 200) {
|
||
_needsSetup = false;
|
||
notifyListeners();
|
||
return true;
|
||
} else {
|
||
print("Ошибка настройки профиля: ${response.body}");
|
||
return false;
|
||
}
|
||
} catch (e) {
|
||
print("Ошибка сети: $e");
|
||
return false;
|
||
} finally {
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
// Приватный метод для проверки статуса аккаунта
|
||
Future<void> _checkAccountStatus() async {
|
||
try {
|
||
final token = await _apiService.getAccessToken();
|
||
final response = await _client.get(
|
||
Uri.parse('${AppConstants.baseUrl}/users/me'),
|
||
headers: {'Authorization': 'Bearer $token'},
|
||
);
|
||
|
||
if (response.statusCode == 200) {
|
||
final data = jsonDecode(utf8.decode(response.bodyBytes)) as Map;
|
||
|
||
_currentUserId = data['id'] as int?;
|
||
_username = data['username']?.toString();
|
||
_firstName = data['first_name']?.toString();
|
||
_lastName = data['last_name']?.toString();
|
||
_phone = data['phone']?.toString();
|
||
_email = data['email']?.toString();
|
||
_about = data['about']?.toString();
|
||
final avatarFileId = data['avatar_file_id']?.toString();
|
||
_avatarUrl = avatarFileId != null
|
||
? '${AppConstants.baseUrl}/media/$avatarFileId'
|
||
: null;
|
||
|
||
// Загружаем локальные настройки
|
||
_avatarPath = await _storage.read(key: 'avatar_path');
|
||
|
||
// Проверяем наличие публичного ключа на сервере
|
||
_hasPublicKeyOnServer =
|
||
data['public_key'] != null && data['public_key'].isNotEmpty;
|
||
|
||
// Проверяем наличие приватного ключа локально
|
||
final hasLocalPrivateKey =
|
||
await _storage.read(key: 'private_key') != null;
|
||
|
||
if (!_hasPublicKeyOnServer) {
|
||
// Путь А: Первая настройка - нужно создать ключи и профиль
|
||
_needsSetup = true;
|
||
_needsKeyRecovery = false;
|
||
} else if (!hasLocalPrivateKey) {
|
||
// Путь В: Переустановка - ключ на сервере, но его нет локально
|
||
_needsKeyRecovery = true;
|
||
_needsSetup = false;
|
||
} else {
|
||
// Путь Б: Нормальный вход - все в порядке
|
||
_needsSetup = false;
|
||
_needsKeyRecovery = false;
|
||
}
|
||
}
|
||
|
||
// Загружаем настройки конфиденциальности
|
||
try {
|
||
final privacyData = await _apiService.getPrivacySettings();
|
||
_showEmail = privacyData['show_email'] as bool?;
|
||
_showPhone = privacyData['show_phone'] as bool?;
|
||
_showAvatar = privacyData['show_avatar'] as bool?;
|
||
_showAbout = privacyData['show_about'] as bool?;
|
||
_showUsername = privacyData['show_username'] as bool?;
|
||
} catch (e) {
|
||
print("Ошибка загрузки настроек конфиденциальности: $e");
|
||
// Устанавливаем значения по умолчанию
|
||
_showEmail = true;
|
||
_showPhone = true;
|
||
_showAvatar = true;
|
||
_showAbout = true;
|
||
_showUsername = true;
|
||
}
|
||
} catch (e) {
|
||
print("Ошибка проверки статуса: $e");
|
||
_needsSetup = false;
|
||
_needsKeyRecovery = false;
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> refreshMe() async {
|
||
await _checkAccountStatus();
|
||
}
|
||
|
||
// Метод для начала с чистого листа (новые ключи)
|
||
Future<void> resetKeys() async {
|
||
try {
|
||
await ApiService().clearOtherSessions();
|
||
} catch (e) {
|
||
print("Error clearing other sessions on key reset: $e");
|
||
}
|
||
await _storage.delete(key: 'private_key');
|
||
try {
|
||
final allKeys = await _storage.readAll();
|
||
for (final key in allKeys.keys) {
|
||
if (key.startsWith('contact_shared_key_') || key.startsWith('contact_public_key_')) {
|
||
await _storage.delete(key: key);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
print("Error clearing cached contact keys in secure storage: $e");
|
||
}
|
||
CryptoService.clearCache();
|
||
final context = navigatorKey.currentContext;
|
||
if (context != null) {
|
||
try {
|
||
Provider.of<ContactProvider>(context, listen: false).clearCache();
|
||
} catch (_) {}
|
||
}
|
||
_needsKeyRecovery = false;
|
||
notifyListeners();
|
||
}
|
||
|
||
void updateAvatarPath(String? path) {
|
||
_avatarPath = path;
|
||
if (path != null) {
|
||
_storage.write(key: 'avatar_path', value: path);
|
||
} else {
|
||
_storage.delete(key: 'avatar_path');
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<bool> updateAvatar(String path) async {
|
||
try {
|
||
final bytes = await File(path).readAsBytes();
|
||
final fileId = await _apiService.uploadFile(bytes, purpose: 'avatar');
|
||
if (fileId != null) {
|
||
final success = await _apiService.updateAvatar(fileId);
|
||
if (success) {
|
||
updateAvatarPath(path);
|
||
await refreshMe(); // Обновить данные профиля, включая avatarUrl
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
} catch (e) {
|
||
print('Ошибка обновления аватарки: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
Future<bool> updateAvatarForUser(String path, int userId) async {
|
||
try {
|
||
final bytes = await File(path).readAsBytes();
|
||
final fileId = await _apiService.uploadFile(bytes, purpose: 'avatar');
|
||
if (fileId != null) {
|
||
final success = await _apiService.updateAvatarForUser(fileId, userId);
|
||
if (success) {
|
||
updateAvatarPath(path);
|
||
await refreshMe(); // Обновить данные профиля, включая avatarUrl
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
} catch (e) {
|
||
print('Ошибка обновления аватарки: $e');
|
||
return false;
|
||
}
|
||
}
|
||
}
|