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 _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; 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(); print('TOTP status from getMe: ${userData['totp_enabled']}'); if (!mounted) return; setState(() { _isTotpEnabled = userData['totp_enabled'] ?? false; }); print('TOTP status set to: $_isTotpEnabled'); } catch (e) { print('Error loading TOTP status: $e'); // Ignore errors, assume TOTP is disabled if (!mounted) return; setState(() => _isTotpEnabled = 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 { if (_isTotpEnabled) { // Показываем диалог с опциями _showTotpOptionsDialog(); } else { // Enable TOTP setState(() => _isSavingTotp = true); try { final api = ApiService(); final data = await api.enableTotp(); setState(() { _totpSecret = data['secret']; _totpQrCode = data['qr_code']; }); // Show dialog to scan QR and enter code _showTotpSetupDialog(); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))), ); } finally { setState(() => _isSavingTotp = false); } } } void _showTotpOptionsDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('TOTP'), content: const Text('TOTP включён. Выберите действие:'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: const Text('Отмена'), ), TextButton( onPressed: () { Navigator.of(context).pop(); _reissueTotp(); }, child: const Text('Перевыпустить ключ'), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); _disableTotp(); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, ), child: const Text('Отключить TOTP'), ), ], ), ); } Future _reissueTotp() async { setState(() => _isSavingTotp = true); try { final api = ApiService(); final data = await api.enableTotp(); setState(() { _totpSecret = data['secret']; _totpQrCode = data['qr_code']; }); // Show dialog to scan QR and enter code _showTotpSetupDialog(isReissue: true); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))), ); } finally { setState(() => _isSavingTotp = false); } } 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('TOTP отключён')), ); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))), ); } finally { setState(() => _isSavingTotp = false); } } void _showTotpSetupDialog({bool isReissue = false}) { final codeController = TextEditingController(); showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: Text(isReissue ? 'Перевыпуск ключа TOTP' : 'Настройка TOTP'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(isReissue ? 'Отсканируйте новый QR-код в приложении аутентификатора:' : 'Отсканируйте QR-код в приложении аутентификатора:'), const SizedBox(height: 16), if (_totpQrCode != null) Builder( builder: (context) { final base64String = _totpQrCode!.split(',').last; final bytes = base64Decode(base64String); return Image.memory(bytes, width: 200, height: 200); }, ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: Text( 'Ключ: ${_totpSecret ?? ''}', style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.copy, size: 18), onPressed: () { if (_totpSecret != null) { Clipboard.setData(ClipboardData(text: _totpSecret!)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Ключ скопирован')), ); } }, tooltip: 'Скопировать ключ', ), ], ), const SizedBox(height: 16), TextField( controller: codeController, decoration: const InputDecoration( labelText: 'Введите код из приложения', helperText: 'Обычно это 6 цифр', ), keyboardType: TextInputType.number, ), ], ), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); setState(() { _totpSecret = null; _totpQrCode = null; }); }, child: const Text('Отмена'), ), ElevatedButton( onPressed: () async { final code = codeController.text.trim(); if (code.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Введите код')), ); return; } try { final api = ApiService(); final success = await api.verifyTotp(code); if (success) { Navigator.of(context).pop(); setState(() { _isTotpEnabled = true; _totpSecret = null; _totpQrCode = null; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(isReissue ? 'Ключ перевыпущен' : 'TOTP включён')), ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Неверный код')), ); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))), ); } }, child: const Text('Подтвердить'), ), ], ), ); } 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), Text(_isTotpEnabled ? 'TOTP включён' : 'TOTP отключён'), const SizedBox(height: 12), ElevatedButton( onPressed: _isSavingTotp ? null : _setupTotp, child: _isSavingTotp ? const CircularProgressIndicator(color: Colors.white) : Text(_isTotpEnabled ? 'Отключить TOTP' : 'Включить TOTP'), ), ], ), ); } }