import 'package:flutter/material.dart'; import 'package:local_auth/local_auth.dart'; import 'package:flutter/services.dart'; import 'package:chepuhagram/domain/services/api_service.dart'; import 'package:chepuhagram/domain/services/crypto_service.dart'; import 'dart:convert'; class SecuritySettingsScreen extends StatefulWidget { const SecuritySettingsScreen({super.key}); @override State createState() => _SecuritySettingsScreenState(); } class _SecuritySettingsScreenState extends State { final _passwordFormKey = GlobalKey(); final _encryptionFormKey = 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; bool _isTotpEnabled = false; String? _totpSecret; String? _totpQrCode; @override void initState() { super.initState(); _checkBiometricSupport(); _loadTotpStatus(); } @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 _loadTotpStatus() async { try { final api = ApiService(); final userData = await api.getMe(); if (!mounted) return; setState(() { _isTotpEnabled = userData['totp_enabled'] ?? false; }); } catch (e) { if (!mounted) return; setState(() => _isTotpEnabled = false); } } Future _authenticateBiometric() async { try { return await _localAuth.authenticate( localizedReason: 'Подтвердите личность для изменения крипто-пароля', options: const AuthenticationOptions( biometricOnly: false, useErrorDialogs: true, stickyAuth: false, ), ); } catch (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('Основной пароль успешно обновлен'), behavior: SnackBarBehavior.floating, ), ); _currentPasswordController.clear(); _newPasswordController.clear(); _confirmPasswordController.clear(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString().replaceAll('Exception: ', '')), behavior: SnackBarBehavior.floating, ), ); } finally { if (mounted) 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('Крипто-пароль успешно изменен'), behavior: SnackBarBehavior.floating, ), ); _currentEncryptPasswordController.clear(); _newEncryptPasswordController.clear(); _confirmEncryptPasswordController.clear(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString().replaceAll('Exception: ', '')), behavior: SnackBarBehavior.floating, ), ); } finally { if (mounted) setState(() => _isSavingEncryption = false); } } Future _setupTotp() async { if (_isTotpEnabled) { _showTotpOptionsDialog(); } else { setState(() => _isSavingTotp = true); try { final api = ApiService(); final data = await api.enableTotp(); setState(() { _totpSecret = data['secret']; _totpQrCode = data['qr_code']; }); _showTotpSetupDialog(); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString().replaceAll('Exception: ', '')), behavior: SnackBarBehavior.floating, ), ); } finally { setState(() => _isSavingTotp = false); } } } void _showTotpOptionsDialog() { showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: const Text('Защита TOTP активна'), content: const Text( 'Выберите необходимое действие для управления двухфакторной аутентификацией.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Отмена'), ), ElevatedButton( onPressed: () { Navigator.pop(context); _disableTotp(); }, style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, elevation: 0, ), child: const Text( 'Отключить 2FA', style: TextStyle(color: Colors.white), ), ), ], ), ); } Future _disableTotp() async { setState(() => _isSavingTotp = true); try { final api = ApiService(); final success = await api.disableTotp(); if (success) { setState(() { _isTotpEnabled = false; _totpSecret = null; _totpQrCode = null; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Двухфакторная защита отключена'), behavior: SnackBarBehavior.floating, ), ); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString().replaceAll('Exception: ', '')), behavior: SnackBarBehavior.floating, ), ); } finally { setState(() => _isSavingTotp = false); } } void _showTotpSetupDialog() { final codeController = TextEditingController(); final colorScheme = Theme.of(context).colorScheme; showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), title: const Text('Активация TOTP 2FA'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( 'Сканируйте код приложением аутентификатора (Google Authenticator / Aegis):', style: TextStyle(fontSize: 13), ), const SizedBox(height: 16), if (_totpQrCode != null) ClipRRect( borderRadius: BorderRadius.circular(16), child: Image.memory( base64Decode(_totpQrCode!.split(',').last), width: 180, height: 180, ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), decoration: BoxDecoration( color: colorScheme.surfaceVariant.withOpacity(0.4), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Expanded( child: Text( _totpSecret ?? '', style: const TextStyle( fontFamily: 'monospace', fontSize: 12, ), overflow: TextOverflow.ellipsis, ), ), IconButton( icon: const Icon(Icons.copy_rounded, size: 16), onPressed: () { if (_totpSecret != null) { Clipboard.setData(ClipboardData(text: _totpSecret!)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Ключ скопирован в буфер'), duration: Duration(seconds: 1), ), ); } }, ), ], ), ), const SizedBox(height: 16), TextField( controller: codeController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: '6-значный одноразовый код', border: OutlineInputBorder(), ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Отмена'), ), ElevatedButton( onPressed: () async { if (codeController.text.trim().isEmpty) return; try { final success = await ApiService().verifyTotp( codeController.text.trim(), ); if (success) { Navigator.pop(context); setState(() => _isTotpEnabled = true); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Двухфакторный ключ успешно привязан'), behavior: SnackBarBehavior.floating, ), ); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString().replaceAll('Exception: ', '')), ), ); } }, child: const Text('Подтвердить'), ), ], ), ); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( backgroundColor: colorScheme.background, appBar: AppBar( title: const Text('Безопасность'), elevation: 0, backgroundColor: Colors.transparent, ), body: ListView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.all(16), children: [ // Модуль 1: Основной пароль _buildCardSection( title: 'Смена пароля аккаунта', child: Form( key: _passwordFormKey, child: Column( children: [ _buildFormInput( _currentPasswordController, 'Текущий пароль', true, ), _buildFormInput(_newPasswordController, 'Новый пароль', true), _buildFormInput( _confirmPasswordController, 'Повторите новый пароль', true, validator: (v) { if (v != _newPasswordController.text) return 'Пароли не совпадают'; return null; }, ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isSavingPassword ? null : _savePassword, style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: _isSavingPassword ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Обновить основной пароль'), ), ), ], ), ), ), const SizedBox(height: 20), // Модуль 2: Сквозное шифрование _buildCardSection( title: 'Пароль сквозного шифрования (E2EE)', child: Form( key: _encryptionFormKey, child: Column( children: [ _buildFormInput( _currentEncryptPasswordController, _isBiometricAvailable ? 'Оставьте пустым и подтвердите биометрией' : 'Текущий крипто-пароль', true, hint: _isBiometricAvailable ? 'Подтвердите биометрией' : null, validator: (v) { // Если биометрия доступна на устройстве, поле МОЖЕТ быть пустым if (_isBiometricAvailable) return null; // Если биометрии нет, то поле становится строго обязательным if (v == null || v.isEmpty) return 'Введите текущий пароль'; return null; }, ), _buildFormInput( _newEncryptPasswordController, 'Новый крипто-пароль', true, ), _buildFormInput( _confirmEncryptPasswordController, 'Повторите новый крипто-пароль', true, validator: (v) { if (v != _newEncryptPasswordController.text) return 'Пароли не совпадают'; return null; }, ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isSavingEncryption ? null : _saveEncryptionPassword, style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: _isSavingEncryption ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Обновить ключ шифрования'), ), ), ], ), ), ), const SizedBox(height: 20), // Модуль 3: Двухфакторная аутентификация Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colorScheme.surfaceVariant.withOpacity(0.2), borderRadius: BorderRadius.circular(24), border: Border.all( color: colorScheme.outlineVariant.withOpacity(0.1), ), ), child: Row( children: [ Icon( Icons.lock_clock_rounded, color: colorScheme.primary, size: 28, ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Двухфакторная защита (TOTP)', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, ), ), const SizedBox(height: 2), Text( _isTotpEnabled ? 'Статус: Активна' : 'Статус: Отключена', style: TextStyle( color: _isTotpEnabled ? Colors.green : colorScheme.outline, fontSize: 13, ), ), ], ), ), ElevatedButton( onPressed: _isSavingTotp ? null : _setupTotp, style: ElevatedButton.styleFrom( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Text(_isTotpEnabled ? 'Опции' : 'Включить'), ), ], ), ), ], ), ); } Widget _buildCardSection({required String title, required Widget child}) { final colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colorScheme.surfaceVariant.withOpacity(0.2), borderRadius: BorderRadius.circular(24), border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.1)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, letterSpacing: -0.2, ), ), const SizedBox(height: 16), child, ], ), ); } Widget _buildFormInput( TextEditingController controller, String label, bool obscure, { String? hint, String? Function(String?)? validator, }) { return Padding( padding: const EdgeInsets.only(bottom: 12.0), child: TextFormField( controller: controller, obscureText: obscure, validator: validator ?? (v) => (v == null || v.isEmpty) ? 'Обязательное поле' : null, decoration: InputDecoration( labelText: label, hintText: hint, border: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), contentPadding: const EdgeInsets.symmetric( horizontal: 14, vertical: 12, ), ), ), ); } }