Files
fuckKomet/lib/screens/token_auth_screen.dart

438 lines
13 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 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/screens/home_screen.dart';
import 'package:gwid/utils/proxy_service.dart';
import 'package:gwid/utils/proxy_settings.dart';
import 'package:gwid/screens/settings/qr_scanner_screen.dart';
import 'package:gwid/screens/settings/session_spoofing_screen.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:crypto/crypto.dart' as crypto;
class TokenAuthScreen extends StatefulWidget {
const TokenAuthScreen({super.key});
@override
State<TokenAuthScreen> createState() => _TokenAuthScreenState();
}
class _TokenAuthScreenState extends State<TokenAuthScreen> {
final TextEditingController _tokenController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_tokenController.dispose();
super.dispose();
}
Future<void> _processLogin({
required String token,
Map<String, dynamic>? spoofData,
ProxySettings? proxySettings,
}) async {
if (!mounted) return;
setState(() => _isLoading = true);
final messenger = ScaffoldMessenger.of(context);
try {
if (spoofData != null && spoofData.isNotEmpty) {
messenger.showSnackBar(
const SnackBar(
content: Text('Настройки анонимности из файла применены!'),
),
);
}
if (proxySettings != null) {
await ProxyService.instance.saveProxySettings(proxySettings);
messenger.showSnackBar(
const SnackBar(
content: Text('Настройки прокси из файла применены!'),
backgroundColor: Colors.blue,
),
);
}
await ApiService.instance.saveToken(token);
if (mounted) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const HomeScreen()),
(Route<dynamic> route) => false,
);
}
} catch (e) {
messenger.showSnackBar(
SnackBar(
content: Text('Ошибка входа: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
void _loginWithToken() {
final token = _tokenController.text.trim();
if (token.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Введите токен для входа')));
return;
}
_processLogin(token: token);
}
Future<void> _loadSessionFile() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json','ksession'],
);
if (result == null || result.files.single.path == null) return;
final filePath = result.files.single.path!;
setState(() => _isLoading = true);
try {
final fileContent = await File(filePath).readAsString();
Map<String, dynamic> jsonData = json.decode(fileContent);
String finalJsonPayload;
if (jsonData['encrypted'] == true) {
final password = await _showPasswordDialog();
if (password == null || password.isEmpty) {
setState(() => _isLoading = false);
return;
}
final iv = encrypt.IV.fromBase64(jsonData['iv_base64']);
final encryptedData = encrypt.Encrypted.fromBase64(
jsonData['data_base64'],
);
final keyBytes = utf8.encode(password);
final keyHash = crypto.sha256.convert(keyBytes);
final key = encrypt.Key(Uint8List.fromList(keyHash.bytes));
final encrypter = encrypt.Encrypter(
encrypt.AES(key, mode: encrypt.AESMode.cbc),
);
finalJsonPayload = encrypter.decrypt(encryptedData, iv: iv);
} else {
finalJsonPayload = fileContent;
}
final Map<String, dynamic> sessionData = json.decode(finalJsonPayload);
final String? token = sessionData['token'];
if (token == null || token.isEmpty)
throw Exception('Файл сессии не содержит токена.');
await _processLogin(
token: token,
spoofData: sessionData['spoof_data'] is Map<String, dynamic>
? sessionData['spoof_data']
: null,
proxySettings: sessionData['proxy_settings'] is Map<String, dynamic>
? ProxySettings.fromJson(sessionData['proxy_settings'])
: null,
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ошибка: $e'), backgroundColor: Colors.red),
);
setState(() => _isLoading = false);
}
}
Future<void> _processQrData(String qrData) async {
if (!mounted) return;
setState(() => _isLoading = true);
final messenger = ScaffoldMessenger.of(context);
try {
final decoded = jsonDecode(qrData) as Map<String, dynamic>;
if (decoded['type'] != 'komet_auth_v1' ||
decoded['token'] == null ||
decoded['timestamp'] == null) {
throw Exception("Неверный формат QR-кода.");
}
final int qrTimestamp = decoded['timestamp'];
final String token = decoded['token'];
final int now = DateTime.now().millisecondsSinceEpoch;
const int oneMinuteInMillis = 60 * 1000; // 60 секунд
if ((now - qrTimestamp) > oneMinuteInMillis) {
throw Exception("QR-код устарел. Пожалуйста, сгенерируйте новый.");
}
await _processLogin(token: token);
} catch (e) {
messenger.showSnackBar(
SnackBar(
content: Text('Ошибка: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
void _showQrSourceSelection() {
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Wrap(
children: <Widget>[
ListTile(
leading: const Icon(Icons.camera_alt_outlined),
title: const Text('Камера'),
onTap: () {
Navigator.of(context).pop();
_scanWithCamera();
},
),
ListTile(
leading: const Icon(Icons.photo_library_outlined),
title: const Text('Галерея'),
onTap: () {
Navigator.of(context).pop();
_scanFromGallery();
},
),
],
),
),
);
}
Future<void> _scanWithCamera() async {
final result = await Navigator.of(context).push<String>(
MaterialPageRoute(builder: (context) => const QrScannerScreen()),
);
if (result != null) await _processQrData(result);
}
Future<void> _scanFromGallery() async {
final image = await ImagePicker().pickImage(source: ImageSource.gallery);
if (image == null) return;
final controller = MobileScannerController();
final result = await controller.analyzeImage(image.path);
await controller.dispose();
if (result != null &&
result.barcodes.isNotEmpty &&
result.barcodes.first.rawValue != null) {
await _processQrData(result.barcodes.first.rawValue!);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('QR-код на изображении не найден.'),
backgroundColor: Colors.orange,
),
);
}
}
Future<String?> _showPasswordDialog() {
final passwordController = TextEditingController();
bool isPasswordVisible = false;
return showDialog<String>(
context: context,
barrierDismissible: false,
builder: (context) => StatefulBuilder(
builder: (context, setStateDialog) => AlertDialog(
title: const Text('Введите пароль'),
content: TextField(
controller: passwordController,
obscureText: !isPasswordVisible,
autofocus: true,
decoration: InputDecoration(
labelText: 'Пароль от файла сессии',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
isPasswordVisible ? Icons.visibility_off : Icons.visibility,
),
onPressed: () => setStateDialog(
() => isPasswordVisible = !isPasswordVisible,
),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(null),
child: const Text('Отмена'),
),
FilledButton(
onPressed: () =>
Navigator.of(context).pop(passwordController.text),
child: const Text('OK'),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Другие способы входа')),
body: Stack(
children: [
ListView(
padding: const EdgeInsets.all(16),
children: [
_AuthCard(
icon: Icons.qr_code_scanner_rounded,
title: 'Вход по QR-коду',
subtitle:
'Отсканируйте QR-код с другого устройства, чтобы быстро войти.',
buttonLabel: 'Сканировать QR-код',
onPressed: _showQrSourceSelection,
),
const SizedBox(height: 20),
_AuthCard(
icon: Icons.file_open_outlined,
title: 'Вход по файлу сессии',
subtitle:
'Загрузите ранее экспортированный .json или .ksession файл для восстановления сессии.',
buttonLabel: 'Загрузить файл',
onPressed: _loadSessionFile,
isOutlined: true,
),
const SizedBox(height: 20),
_AuthCard(
icon: Icons.vpn_key_outlined,
title: 'Вход по токену',
subtitle: 'Введите токен авторизации (AUTH_TOKEN) вручную.',
buttonLabel: 'Войти с токеном',
onPressed: _loginWithToken,
isOutlined: true,
child: Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: TextField(
controller: _tokenController,
decoration: const InputDecoration(
labelText: 'Токен',
border: OutlineInputBorder(),
),
),
),
),
],
),
if (_isLoading)
Container(
color: Colors.black.withOpacity(0.5),
child: const Center(child: CircularProgressIndicator()),
),
],
),
);
}
}
class _AuthCard extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final String buttonLabel;
final VoidCallback onPressed;
final bool isOutlined;
final Widget? child;
const _AuthCard({
required this.icon,
required this.title,
required this.subtitle,
required this.buttonLabel,
required this.onPressed,
this.isOutlined = false,
this.child,
});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Card(
elevation: isOutlined ? 0 : 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: isOutlined
? BorderSide(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
)
: BorderSide.none,
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(
icon,
size: 28,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 12),
Text(
title,
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Text(
subtitle,
style: textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (child != null) ...[const SizedBox(height: 20), child!],
const SizedBox(height: 20),
isOutlined
? OutlinedButton(onPressed: onPressed, child: Text(buttonLabel))
: FilledButton(onPressed: onPressed, child: Text(buttonLabel)),
],
),
),
);
}
}