Chepuhagram/lib/presentation/screens/key_recovery_screen.dart

310 lines
11 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/presentation/screens/contacts_screen.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../logic/auth_provider.dart';
import '../../domain/services/crypto_service.dart';
import '../../domain/services/api_service.dart';
import 'account_setup_screen.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../core/constants.dart';
class KeyRecoveryScreen extends StatefulWidget {
const KeyRecoveryScreen({super.key});
@override
State<KeyRecoveryScreen> createState() => _KeyRecoveryScreenState();
}
class _KeyRecoveryScreenState extends State<KeyRecoveryScreen> {
bool _isLoading = false;
String? _errorMessage;
final _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
Future<void> _startFresh() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final authProvider = context.read<AuthProvider>();
// Удаляем все сообщения пользователя
try {
final api = ApiService();
await api.deleteAllMessages();
} catch (e) {
print('Ошибка при удалении сообщений: $e');
// Продолжаем даже если удаление сообщений не удалось
}
// Удаляем старые ключи и создаем новые
await authProvider.resetKeys();
// Переходим на экран настройки для создания новых ключей
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const AccountSetupScreen()),
);
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = 'Ошибка: ${e.toString()}';
_isLoading = false;
});
}
}
}
Future<void> _recoverKeys() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final authProvider = context.read<AuthProvider>();
final apiService = ApiService();
final cryptoService = CryptoService();
// Получаем токен
final token = await apiService.getAccessToken();
if (token == null) throw Exception('Не авторизован');
// Скачиваем зашифрованный приватный ключ с сервера
final response = await http.get(
Uri.parse('${AppConstants.baseUrl}/users/me'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode != 200) {
throw Exception('Не удалось получить данные пользователя');
}
final data = jsonDecode(utf8.decode(response.bodyBytes)) as Map;
final encryptedPrivateKey = data['encrypted_private_key'];
if (encryptedPrivateKey == null || encryptedPrivateKey.isEmpty) {
throw Exception('Зашифрованный ключ не найден на сервере');
}
// Расшифровываем приватный ключ
final decryptedPrivateKey = await cryptoService.decryptPrivateKey(
encryptedPrivateKey,
_passwordController.text,
);
// Сохраняем расшифрованный ключ локально
await cryptoService.savePrivateKey(decryptedPrivateKey);
// Обновляем статус в AuthProvider
await authProvider.tryAutoLogin();
if (mounted) {
// Возвращаемся на главный экран
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const ContactsScreen()),
);
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = 'Ошибка восстановления: ${e.toString().replaceAll('Exception: ', '')}';
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Восстановление ключей'),
centerTitle: true,
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 32),
Icon(
Icons.security_outlined,
size: 80,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 24),
Text(
'Восстановление ключей шифрования',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Text(
'Вы переустановили приложение или используете новый девайс. У вас есть два варианта:',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
// Вариант 1: Начать с чистого листа
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.restart_alt_outlined,
color: Theme.of(context).colorScheme.primary,
size: 28,
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Начать с чистого листа',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
Text(
'Создаются новые ключи шифрования. Старые сообщения не будут расшифрованы.',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _startFresh,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Продолжить'),
),
),
],
),
),
),
const SizedBox(height: 24),
// Вариант 2: Восстановить из облака
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.cloud_download_outlined,
color: Theme.of(context).colorScheme.primary,
size: 28,
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Восстановить из облака',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
Text(
'Введите мастер-пароль для восстановления ключей шифрования',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Мастер-пароль',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Введите мастер-пароль';
}
return null;
},
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _recoverKeys,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Восстановить'),
),
),
],
),
),
),
),
const SizedBox(height: 24),
// Сообщение об ошибке
if (_errorMessage != null)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_errorMessage!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
],
),
),
);
}
@override
void dispose() {
_passwordController.dispose();
super.dispose();
}
}