import 'package:flutter/material.dart'; import 'package:local_auth/local_auth.dart'; import 'package:chepuhagram/domain/services/api_service.dart'; import 'package:chepuhagram/domain/services/crypto_service.dart'; class SecuritySettingsScreen extends StatefulWidget { const SecuritySettingsScreen({super.key}); @override State createState() => _SecuritySettingsScreenState(); } class _SecuritySettingsScreenState extends State { final _passwordFormKey = GlobalKey(); final _encryptionFormKey = GlobalKey(); final _totpFormKey = GlobalKey(); final _currentPasswordController = TextEditingController(); final _newPasswordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); final _currentEncryptPasswordController = TextEditingController(); final _newEncryptPasswordController = TextEditingController(); final _confirmEncryptPasswordController = TextEditingController(); final LocalAuthentication _localAuth = LocalAuthentication(); bool _isBiometricAvailable = false; bool _isSavingPassword = false; bool _isSavingEncryption = false; bool _isSavingTotp = false; @override void initState() { super.initState(); _checkBiometricSupport(); } @override void dispose() { _currentPasswordController.dispose(); _newPasswordController.dispose(); _confirmPasswordController.dispose(); _currentEncryptPasswordController.dispose(); _newEncryptPasswordController.dispose(); _confirmEncryptPasswordController.dispose(); super.dispose(); } Future _checkBiometricSupport() async { try { final canCheckBiometrics = await _localAuth.canCheckBiometrics; final isSupported = await _localAuth.isDeviceSupported(); final availableBiometrics = await _localAuth.getAvailableBiometrics(); if (!mounted) return; setState(() { _isBiometricAvailable = canCheckBiometrics && isSupported && availableBiometrics.isNotEmpty; }); } catch (_) { if (!mounted) return; setState(() { _isBiometricAvailable = false; }); } } Future _authenticateBiometric() async { try { return await _localAuth.authenticate( localizedReason: 'Подтвердите личность для смены пароля шифрования', options: const AuthenticationOptions( biometricOnly: false, stickyAuth: false, useErrorDialogs: true, sensitiveTransaction: true, ), ); } catch (error) { debugPrint('Biometric authentication error: $error'); return false; } } Future _savePassword() async { if (!_passwordFormKey.currentState!.validate()) return; setState(() => _isSavingPassword = true); try { final api = ApiService(); final success = await api.changePassword( _currentPasswordController.text.trim(), _newPasswordController.text.trim(), ); if (!success) { throw Exception('Не удалось изменить пароль'); } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Пароль успешно изменён')), ); _currentPasswordController.clear(); _newPasswordController.clear(); _confirmPasswordController.clear(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))), ); } finally { if (!mounted) return; setState(() => _isSavingPassword = false); } } Future _saveEncryptionPassword() async { await _checkBiometricSupport(); if (!_encryptionFormKey.currentState!.validate()) return; setState(() => _isSavingEncryption = true); try { final newPassword = _newEncryptPasswordController.text.trim(); final currentPassword = _currentEncryptPasswordController.text.trim(); final cryptoService = CryptoService(); String privateKeyBase64; if (currentPassword.isEmpty) { if (!_isBiometricAvailable) { throw Exception('Биометрия не настроена. Введите текущий пароль.'); } final authenticated = await _authenticateBiometric(); if (!authenticated) { throw Exception('Биометрическая аутентификация не пройдена.'); } final localPrivateKey = await cryptoService.getPrivateKey(); if (localPrivateKey == null || localPrivateKey.isEmpty) { throw Exception('Локальный приватный ключ не найден.'); } privateKeyBase64 = localPrivateKey; } else { final api = ApiService(); final userData = await api.getMe(); final encryptedPrivateKey = userData['encrypted_private_key']?.toString(); if (encryptedPrivateKey == null || encryptedPrivateKey.isEmpty) { throw Exception('Зашифрованный ключ не найден на сервере.'); } privateKeyBase64 = await cryptoService.decryptPrivateKey( encryptedPrivateKey, currentPassword, ); await cryptoService.savePrivateKey(privateKeyBase64); } final updatedEncryptedPrivateKey = await cryptoService.encryptPrivateKeyWithPassword( privateKeyBase64, newPassword, ); final success = await ApiService().updateEncryptedPrivateKey(updatedEncryptedPrivateKey); if (!success) { throw Exception('Не удалось обновить пароль шифрования на сервере.'); } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Пароль шифрования успешно обновлён')), ); _currentEncryptPasswordController.clear(); _newEncryptPasswordController.clear(); _confirmEncryptPasswordController.clear(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))), ); } finally { if (!mounted) return; setState(() => _isSavingEncryption = false); } } Future _setupTotp() async { setState(() => _isSavingTotp = true); await Future.delayed(const Duration(milliseconds: 500)); if (!mounted) return; setState(() => _isSavingTotp = false); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('TOTP пока не подключён на сервере')), ); } String? _currentEncryptionPasswordValidator(String? value) { if (value == null || value.isEmpty) { if (!_isBiometricAvailable) { return 'Введите текущий пароль'; } } return null; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Безопасность')), body: ListView( padding: const EdgeInsets.all(16), children: [ const Text('Смена пароля аккаунта', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 12), Form( key: _passwordFormKey, child: Column( children: [ TextFormField( controller: _currentPasswordController, decoration: const InputDecoration(labelText: 'Текущий пароль'), obscureText: true, validator: (value) { if (value == null || value.isEmpty) return 'Введите текущий пароль'; return null; }, ), const SizedBox(height: 12), TextFormField( controller: _newPasswordController, decoration: const InputDecoration(labelText: 'Новый пароль'), obscureText: true, validator: (value) { if (value == null || value.isEmpty) return 'Введите новый пароль'; if (value.length < 6) return 'Пароль слишком короткий'; return null; }, ), const SizedBox(height: 12), TextFormField( controller: _confirmPasswordController, decoration: const InputDecoration(labelText: 'Повторите пароль'), obscureText: true, validator: (value) { if (value != _newPasswordController.text) return 'Пароли не совпадают'; return null; }, ), const SizedBox(height: 14), ElevatedButton( onPressed: _isSavingPassword ? null : _savePassword, child: _isSavingPassword ? const CircularProgressIndicator(color: Colors.white) : const Text('Сохранить пароль'), ), ], ), ), const SizedBox(height: 24), const Text('Пароль шифрования сообщений', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 12), Form( key: _encryptionFormKey, child: Column( children: [ TextFormField( controller: _currentEncryptPasswordController, decoration: InputDecoration( labelText: 'Текущий пароль шифрования', helperText: _isBiometricAvailable ? 'Оставьте поле пустым и подтвердите биометрией' : 'Требуется текущий пароль', ), obscureText: true, validator: _currentEncryptionPasswordValidator, ), const SizedBox(height: 12), TextFormField( controller: _newEncryptPasswordController, decoration: const InputDecoration(labelText: 'Новый пароль шифрования'), obscureText: true, validator: (value) { if (value == null || value.length < 6) return 'Пароль слишком короткий'; return null; }, ), const SizedBox(height: 12), TextFormField( controller: _confirmEncryptPasswordController, decoration: const InputDecoration(labelText: 'Повторите новый пароль'), obscureText: true, validator: (value) { if (value != _newEncryptPasswordController.text) return 'Пароли не совпадают'; return null; }, ), const SizedBox(height: 14), ElevatedButton( onPressed: _isSavingEncryption ? null : _saveEncryptionPassword, child: _isSavingEncryption ? const CircularProgressIndicator(color: Colors.white) : const Text('Сохранить пароль шифрования'), ), ], ), ), const SizedBox(height: 24), const Text('TOTP', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 12), const Text('Настройка одноразового кода (TOTP) пока не подключена на сервере.'), const SizedBox(height: 12), ElevatedButton( onPressed: _isSavingTotp ? null : _setupTotp, child: _isSavingTotp ? const CircularProgressIndicator(color: Colors.white) : const Text('Установить TOTP код'), ), ], ), ); } }