Chepuhagram/lib/logic/auth_provider.dart

650 lines
20 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}