Chepuhagram/lib/logic/auth_provider.dart

567 lines
17 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/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 {
_isLoading = true;
notifyListeners();
await ApiService().logoutCurrentUser();
// 1. Закрываем WebSocket connection
SocketService().disconnect();
final mode = await _storage.read(key: 'theme_mode');
final color = await _storage.read(key: 'accent_color');
await _storage.deleteAll();
CryptoService.clearCache();
final context = navigatorKey.currentContext;
if (context != null) {
try {
Provider.of<ContactProvider>(context, listen: false).clearCache();
} catch (e) {
print("Error clearing contact provider cache: $e");
}
}
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
await LocalDbService().clearDatabase();
_currentUserId = null;
_username = null;
_firstName = null;
_lastName = null;
_phone = null;
_email = null;
_about = null;
_avatarPath = null;
_avatarUrl = null;
if (mode != null) {
await _storage.write(key: 'theme_mode', value: mode);
}
if (color != null) {
await _storage.write(key: 'accent_color', value: color);
}
// 3. Уничтожаем локальную базу данных переписки (Drift)
// Поскольку у вас Drift открывает SqfliteQueryExecutor (local_db_service.dart),
// самый надежный способ — физически удалить файл базы данных чепухаграма.
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");
}
_currentUserId = null;
_isLoading = false;
notifyListeners();
navigatorKey.currentState?.pushAndRemoveUntil(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const SplashScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
),
(route) => false,
);
}
Future<void> fetchSessions() async {
_isLoading = true;
_error = null;
notifyListeners();
try {
final data = await _apiService.getActiveSessions();
print('Fetched sessions: $data');
_sessions = data.map((json) => Session.fromJson(json)).toList();
} catch (e) {
_error = 'Произошла ошибка при загрузке сессий: $e';
print(_error);
} finally {
_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<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;
}
}
}