Initial Commit

This commit is contained in:
ivan2282
2025-11-15 20:06:40 +03:00
commit 205d11df0d
233 changed files with 52572 additions and 0 deletions

437
lib/token_auth_screen.dart Normal file
View File

@@ -0,0 +1,437 @@
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_service.dart';
import 'package:gwid/home_screen.dart';
import 'package:gwid/proxy_service.dart';
import 'package:gwid/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'],
);
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 файл для восстановления сессии.',
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)),
],
),
),
);
}
}