oauth
This commit is contained in:
parent
5eed9dc2f1
commit
a5560944a5
16 changed files with 27 additions and 722 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
---
|
---
|
||||||
alwaysApply: true
|
alwaysApply: true
|
||||||
---
|
---
|
||||||
mnemo_cards_web_v2 is a web app. It should only run on web! Not mobile or macos. And tests should also only run on web!
|
mnemo_cards_web_v2 is a web app. It should only run on web! Not mobile or macos. And tests should also only run on web! It runs on a remote server.
|
||||||
|
Telegram bot also runs on a web server
|
||||||
Актуальные доступы лежат в .access
|
Актуальные доступы лежат в .access
|
||||||
|
|
@ -3,6 +3,7 @@ alwaysApply: true
|
||||||
---
|
---
|
||||||
Use yx_state and yx_scope,
|
Use yx_state and yx_scope,
|
||||||
Follow clean architecture techique,
|
Follow clean architecture techique,
|
||||||
|
Modularize and split into files,
|
||||||
You can use ../mnemo_cards app as a design and product features reference, but NEVER copy its architecture.
|
You can use ../mnemo_cards app as a design and product features reference, but NEVER copy its architecture.
|
||||||
When you finish with another step:
|
When you finish with another step:
|
||||||
Write unit tests for all the functionalities
|
Write unit tests for all the functionalities
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -23,7 +23,7 @@ class BotConfig {
|
||||||
final String backendUrl;
|
final String backendUrl;
|
||||||
final int shareDailyLimit;
|
final int shareDailyLimit;
|
||||||
|
|
||||||
static const String defaultBackendUrl = 'http://localhost:8443';
|
static const String defaultBackendUrl = 'https://api.mnemo-cards.online';
|
||||||
static const int defaultShareDailyLimit = 1;
|
static const int defaultShareDailyLimit = 1;
|
||||||
|
|
||||||
factory BotConfig.fromArgs(
|
factory BotConfig.fromArgs(
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -38,6 +38,6 @@ _flutter.buildConfig = {"engineRevision":"d3d45dcf251823c1769909cd43698d126db38d
|
||||||
|
|
||||||
_flutter.loader.load({
|
_flutter.loader.load({
|
||||||
serviceWorkerSettings: {
|
serviceWorkerSettings: {
|
||||||
serviceWorkerVersion: "1977728131"
|
serviceWorkerVersion: "2889484362"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ class HttpRepositoryV2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check dynamic paths (e.g., /auth/telegram/code-status/<code>)
|
// Check dynamic paths (e.g., /auth/telegram/code-status/<code>)
|
||||||
if (normalizedPath.startsWith('/auth/telegram/code-status/')) {
|
if (normalizedPath.startsWith('/auth/telegram/code-status/') || normalizedPath.startsWith('/auth/oauth/')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
import 'package:yx_scope_flutter/yx_scope_flutter.dart';
|
import 'package:yx_scope_flutter/yx_scope_flutter.dart';
|
||||||
|
|
||||||
import 'package:mnemo_cards_web_v2/di/app_scope/app_scope_container.dart';
|
import 'package:mnemo_cards_web_v2/di/app_scope/app_scope_container.dart';
|
||||||
import 'package:mnemo_cards_web_v2/domain/config/api_config_v2.dart';
|
import 'package:mnemo_cards_web_v2/domain/config/api_config_v2.dart';
|
||||||
import 'package:mnemo_cards_web_v2/domain/models/telegram_auth_code_status.dart';
|
import 'package:mnemo_cards_web_v2/presentation/pages/auth/sign_in_with_google_button.dart';
|
||||||
|
import 'package:mnemo_cards_web_v2/presentation/pages/auth/sign_in_with_telegram.dart';
|
||||||
|
|
||||||
/// Authentication page
|
/// Authentication page
|
||||||
///
|
///
|
||||||
|
|
@ -24,12 +22,6 @@ class AuthPage extends StatefulWidget {
|
||||||
class _AuthPageState extends State<AuthPage> {
|
class _AuthPageState extends State<AuthPage> {
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
final _telegramCodeController = TextEditingController();
|
|
||||||
TelegramAuthCodeStatus? _webCodeStatus;
|
|
||||||
Timer? _codeStatusTimer;
|
|
||||||
Timer? _countdownTimer;
|
|
||||||
DateTime? _codeExpiryTime;
|
|
||||||
bool _autoLoginAttempted = false;
|
|
||||||
|
|
||||||
Future<void> _loginWithGoogle(BuildContext context) async {
|
Future<void> _loginWithGoogle(BuildContext context) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -93,454 +85,15 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loginWithTelegram(BuildContext context) async {
|
Future<void> _onTelegramLoginSuccess(BuildContext context) async {
|
||||||
final code = _telegramCodeController.text.trim();
|
|
||||||
|
|
||||||
if (code.isEmpty) {
|
|
||||||
setState(() {
|
|
||||||
_errorMessage = 'Введите код авторизации';
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isLoading = true;
|
|
||||||
_errorMessage = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get router before async operations to avoid context issues
|
// Get router before async operations to avoid context issues
|
||||||
final router = GoRouter.of(context);
|
final router = GoRouter.of(context);
|
||||||
|
|
||||||
try {
|
|
||||||
final appScope = ScopeProvider.of<AppScopeContainer>(
|
|
||||||
context,
|
|
||||||
listen: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (appScope == null) {
|
|
||||||
throw Exception('AppScope not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login via Telegram with code
|
|
||||||
final user = await appScope.authService.loginWithTelegram(code);
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
if (user != null) {
|
|
||||||
log('Telegram login successful: ${user.email}', name: 'AuthPage');
|
|
||||||
|
|
||||||
_stopCodePolling();
|
|
||||||
_countdownTimer?.cancel();
|
|
||||||
_countdownTimer = null;
|
|
||||||
setState(() {
|
|
||||||
_webCodeStatus = _webCodeStatus?.copyWith(
|
|
||||||
state: TelegramAuthCodeState.used,
|
|
||||||
remainingSeconds: 0,
|
|
||||||
isUsed: true,
|
|
||||||
isClaimed: true,
|
|
||||||
expiresAt: DateTime.now(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear code field
|
|
||||||
_telegramCodeController.clear();
|
|
||||||
|
|
||||||
// Create UserScope if it doesn't exist
|
|
||||||
if (appScope.userScopeHolder.scope == null) {
|
|
||||||
await appScope.userScopeHolder.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update user state
|
|
||||||
appScope.userScopeHolder.scope!.userStateManager.setUser(user);
|
|
||||||
|
|
||||||
// Notify that UserScope has changed
|
|
||||||
appScope.notifyUserScopeChanged();
|
|
||||||
|
|
||||||
// Notify router about auth change
|
|
||||||
appScope.userScopeHolder.notifyAuthChanged();
|
|
||||||
|
|
||||||
// Navigate to home
|
|
||||||
router.go('/home');
|
router.go('/home');
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_errorMessage = 'Неверный код авторизации';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
log('Error in Telegram login', error: e, stackTrace: s, name: 'AuthPage');
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_errorMessage = 'Ошибка авторизации: ${e.toString()}';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() => _isLoading = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _generateTelegramCode(BuildContext context) async {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = true;
|
|
||||||
_errorMessage = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
final appScope = ScopeProvider.of<AppScopeContainer>(
|
|
||||||
context,
|
|
||||||
listen: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (appScope == null) {
|
|
||||||
throw Exception('AppScope not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
final status = await appScope.authService.createTelegramWebCode();
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
_autoLoginAttempted = false;
|
|
||||||
_startCountdownTimer(status);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_webCodeStatus = status;
|
|
||||||
_telegramCodeController.text = status.code;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Copy code to clipboard for convenience
|
|
||||||
await Clipboard.setData(ClipboardData(text: status.code));
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
_startCodePolling(context, status.code);
|
|
||||||
// Automatically open Telegram bot with the code
|
|
||||||
await _openTelegramBotWithCode(status.code);
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
log(
|
|
||||||
'Error generating Telegram code',
|
|
||||||
error: e,
|
|
||||||
stackTrace: s,
|
|
||||||
name: 'AuthPage',
|
|
||||||
);
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_errorMessage = 'Не удалось получить код. Попробуйте ещё раз.';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() => _isLoading = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startCodePolling(BuildContext context, String code) {
|
|
||||||
_stopCodePolling();
|
|
||||||
// Trigger immediate status check
|
|
||||||
unawaited(_refreshCodeStatus(context, code));
|
|
||||||
_codeStatusTimer = Timer.periodic(
|
|
||||||
const Duration(seconds: 3),
|
|
||||||
(_) => _refreshCodeStatus(context, code),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _stopCodePolling() {
|
|
||||||
_codeStatusTimer?.cancel();
|
|
||||||
_codeStatusTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _refreshCodeStatus(BuildContext context, String code) async {
|
|
||||||
try {
|
|
||||||
final appScope = ScopeProvider.of<AppScopeContainer>(
|
|
||||||
context,
|
|
||||||
listen: false,
|
|
||||||
);
|
|
||||||
if (appScope == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final status = await appScope.authService.getTelegramCodeStatus(code);
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
_startCountdownTimer(status);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_webCodeStatus = status;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (status.isExpired) {
|
|
||||||
_stopCodePolling();
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_errorMessage = 'Срок действия кода истёк. Сгенерируйте новый.';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (status.isConsumed) {
|
|
||||||
_stopCodePolling();
|
|
||||||
} else if (status.isReadyForLogin && !_autoLoginAttempted && mounted) {
|
|
||||||
_autoLoginAttempted = true;
|
|
||||||
await _loginWithTelegram(context);
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
log(
|
|
||||||
'Failed to refresh Telegram code status',
|
|
||||||
error: e,
|
|
||||||
stackTrace: s,
|
|
||||||
name: 'AuthPage',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startCountdownTimer(TelegramAuthCodeStatus status) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final expiresAt =
|
|
||||||
status.expiresAt ?? now.add(Duration(seconds: status.remainingSeconds));
|
|
||||||
_codeExpiryTime = expiresAt;
|
|
||||||
|
|
||||||
_countdownTimer?.cancel();
|
|
||||||
|
|
||||||
if (status.isExpired || status.isConsumed) {
|
|
||||||
_countdownTimer = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
||||||
if (!mounted) {
|
|
||||||
_countdownTimer?.cancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_codeExpiryTime != null && DateTime.now().isAfter(_codeExpiryTime!)) {
|
|
||||||
_countdownTimer?.cancel();
|
|
||||||
setState(() {});
|
|
||||||
} else {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatRemaining() {
|
|
||||||
final expiresAt = _codeExpiryTime;
|
|
||||||
if (expiresAt == null) {
|
|
||||||
return '00:00';
|
|
||||||
}
|
|
||||||
final remaining = expiresAt.difference(DateTime.now());
|
|
||||||
if (remaining.isNegative) {
|
|
||||||
return '00:00';
|
|
||||||
}
|
|
||||||
final minutes = remaining.inMinutes;
|
|
||||||
final seconds = remaining.inSeconds % 60;
|
|
||||||
return '${minutes.toString().padLeft(2, '0')}:'
|
|
||||||
'${seconds.toString().padLeft(2, '0')}';
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTelegramStatusCard(BuildContext context) {
|
|
||||||
final status = _webCodeStatus!;
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
IconData icon;
|
|
||||||
String statusLabel;
|
|
||||||
Color iconColor;
|
|
||||||
|
|
||||||
switch (status.state) {
|
|
||||||
case TelegramAuthCodeState.claimed:
|
|
||||||
icon = Icons.verified;
|
|
||||||
statusLabel = 'Код подтверждён';
|
|
||||||
iconColor = theme.colorScheme.secondary;
|
|
||||||
break;
|
|
||||||
case TelegramAuthCodeState.used:
|
|
||||||
icon = Icons.done_all;
|
|
||||||
statusLabel = 'Код использован';
|
|
||||||
iconColor = theme.colorScheme.secondary;
|
|
||||||
break;
|
|
||||||
case TelegramAuthCodeState.expired:
|
|
||||||
icon = Icons.timer_off;
|
|
||||||
statusLabel = 'Код истёк';
|
|
||||||
iconColor = Colors.red;
|
|
||||||
break;
|
|
||||||
case TelegramAuthCodeState.pending:
|
|
||||||
icon = Icons.hourglass_top;
|
|
||||||
statusLabel = 'Ожидаем отправку кода в боте';
|
|
||||||
iconColor = theme.colorScheme.onSurface;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
final isActive = !status.isExpired && !status.isConsumed;
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
color: iconColor,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
statusLabel,
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: theme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
SelectableText(
|
|
||||||
'Код: ${status.code}',
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: theme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
isActive
|
|
||||||
? 'Осталось времени: ${_formatRemaining()}'
|
|
||||||
: 'Код больше не активен',
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (status.isReadyForLogin)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Text(
|
|
||||||
'Код отправлен. Нажмите «Войти» для завершения.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else if (status.isExpired)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Text(
|
|
||||||
'Получите новый код и повторите попытку.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else if (status.isConsumed)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Text(
|
|
||||||
'Код уже использован для входа.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Text(
|
|
||||||
'Бот должен открыться автоматически. Отправьте код в боте, чтобы продолжить.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open Telegram bot with a specific code in the start parameter
|
|
||||||
Future<void> _openTelegramBotWithCode(String code) async {
|
|
||||||
if (mounted && _errorMessage != null) {
|
|
||||||
setState(() {
|
|
||||||
_errorMessage = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format: https://t.me/bot_username?start=code
|
|
||||||
// Bot extracts 6-digit code from payload using regex (\d{6})$
|
|
||||||
final deepLink = Uri.parse(
|
|
||||||
ApiConfigV2.telegramBotDeepLink(code),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final launched = await launchUrl(
|
|
||||||
deepLink,
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
);
|
|
||||||
if (!launched) {
|
|
||||||
throw Exception('Could not launch Telegram');
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
log(
|
|
||||||
'Error opening Telegram bot',
|
|
||||||
error: e,
|
|
||||||
stackTrace: s,
|
|
||||||
name: 'AuthPage',
|
|
||||||
);
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_errorMessage =
|
|
||||||
'Не удалось открыть Telegram. Попробуйте вручную: ${deepLink.toString()}';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open Telegram bot without code (base link)
|
|
||||||
Future<void> _openTelegramBot() async {
|
|
||||||
if (mounted && _errorMessage != null) {
|
|
||||||
setState(() {
|
|
||||||
_errorMessage = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final deepLink = Uri.parse(ApiConfigV2.telegramBotDeepLinkBase);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final launched = await launchUrl(
|
|
||||||
deepLink,
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
);
|
|
||||||
if (!launched) {
|
|
||||||
throw Exception('Could not launch Telegram');
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
log(
|
|
||||||
'Error opening Telegram bot',
|
|
||||||
error: e,
|
|
||||||
stackTrace: s,
|
|
||||||
name: 'AuthPage',
|
|
||||||
);
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_errorMessage =
|
|
||||||
'Не удалось открыть Telegram. Попробуйте вручную: ${deepLink.toString()}';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_codeStatusTimer?.cancel();
|
|
||||||
_countdownTimer?.cancel();
|
|
||||||
_telegramCodeController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -643,35 +196,9 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
// Login buttons
|
// Login buttons
|
||||||
SizedBox(
|
SignInWithGoogleButton(
|
||||||
width: double.infinity,
|
onPressed: () => _loginWithGoogle(context),
|
||||||
height: 56,
|
isLoading: _isLoading,
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: _isLoading
|
|
||||||
? null
|
|
||||||
: () => _loginWithGoogle(context),
|
|
||||||
icon: _isLoading
|
|
||||||
? SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: theme.colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.g_mobiledata, size: 28),
|
|
||||||
label: Text(
|
|
||||||
_isLoading ? 'Signing in...' : 'Sign in with Google',
|
|
||||||
style: theme.textTheme.labelLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// Divider
|
// Divider
|
||||||
|
|
@ -703,154 +230,10 @@ class _AuthPageState extends State<AuthPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// Telegram auth code input
|
// Telegram auth code input
|
||||||
Column(
|
SignInWithTelegram(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
onLoginSuccess: () => _onTelegramLoginSuccess(context),
|
||||||
children: [
|
onError: (error) => setState(() => _errorMessage = error),
|
||||||
Text(
|
isLoading: _isLoading,
|
||||||
'Введите код из Telegram бота',
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: theme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
TextField(
|
|
||||||
controller: _telegramCodeController,
|
|
||||||
enabled: !_isLoading,
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: theme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: '123456',
|
|
||||||
hintStyle: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: theme.hintColor,
|
|
||||||
),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: theme.dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: theme.dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
filled: true,
|
|
||||||
fillColor: theme.cardColor,
|
|
||||||
prefixIcon: Icon(
|
|
||||||
Icons.code,
|
|
||||||
color: theme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
counterText: '',
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
textInputAction: TextInputAction.done,
|
|
||||||
maxLength: 6,
|
|
||||||
onSubmitted: (_) => _loginWithTelegram(context),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: _isLoading
|
|
||||||
? null
|
|
||||||
: () => _generateTelegramCode(context),
|
|
||||||
icon: const Icon(Icons.bolt, size: 20),
|
|
||||||
label: Text(
|
|
||||||
_isLoading ? 'Создание...' : 'Получить код',
|
|
||||||
style: theme.textTheme.labelLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: _isLoading ? null : _openTelegramBot,
|
|
||||||
icon: const Icon(Icons.telegram, size: 20),
|
|
||||||
label: Text(
|
|
||||||
'Открыть бота',
|
|
||||||
style: theme.textTheme.labelLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
side: BorderSide(
|
|
||||||
color: theme.dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (_webCodeStatus != null) ...[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildTelegramStatusCard(context),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 56,
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: _isLoading
|
|
||||||
? null
|
|
||||||
: () => _loginWithTelegram(context),
|
|
||||||
icon: const Icon(Icons.telegram, size: 24),
|
|
||||||
label: Text(
|
|
||||||
'Войти с кодом Telegram',
|
|
||||||
style: theme.textTheme.labelLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
TextButton(
|
|
||||||
onPressed: _isLoading ? null : () => context.go('/home'),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 24,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'Continue as guest',
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -533,54 +533,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.9.3"
|
version: "5.9.3"
|
||||||
flutter_secure_storage:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage
|
|
||||||
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "9.2.4"
|
|
||||||
flutter_secure_storage_linux:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage_linux
|
|
||||||
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.3"
|
|
||||||
flutter_secure_storage_macos:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage_macos
|
|
||||||
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.3"
|
|
||||||
flutter_secure_storage_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage_platform_interface
|
|
||||||
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.2"
|
|
||||||
flutter_secure_storage_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage_web
|
|
||||||
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.1"
|
|
||||||
flutter_secure_storage_windows:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage_windows
|
|
||||||
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.2"
|
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -55,17 +55,7 @@ server {
|
||||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
|
||||||
# CORS headers for API
|
# CORS headers are handled by the backend server, nginx only proxies
|
||||||
add_header Access-Control-Allow-Origin "*" always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS" always;
|
|
||||||
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization, user_token, request_token, app_version" always;
|
|
||||||
add_header Access-Control-Expose-Headers "Authorization" always;
|
|
||||||
add_header Access-Control-Max-Age "86400" always;
|
|
||||||
|
|
||||||
# Handle preflight OPTIONS requests
|
|
||||||
if ($request_method = OPTIONS) {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Proxy to HTTP backend (SSL termination in nginx)
|
# Proxy to HTTP backend (SSL termination in nginx)
|
||||||
location / {
|
location / {
|
||||||
|
|
|
||||||
|
|
@ -169,22 +169,11 @@ EOF
|
||||||
# API Backend specific settings
|
# API Backend specific settings
|
||||||
client_max_body_size 100M;
|
client_max_body_size 100M;
|
||||||
|
|
||||||
# CORS headers for API
|
# CORS headers are handled by the backend server, nginx only proxies
|
||||||
add_header Access-Control-Allow-Origin "*" always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS" always;
|
|
||||||
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization, user_token, request_token, app_version" always;
|
|
||||||
add_header Access-Control-Expose-Headers "Authorization" always;
|
|
||||||
add_header Access-Control-Max-Age "86400" always;
|
|
||||||
|
|
||||||
# Handle preflight OPTIONS requests
|
# Proxy to backend server (HTTP for API - backend runs in dual mode)
|
||||||
if (\$request_method = OPTIONS) {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Proxy to backend server (HTTPS for API, HTTP for others)
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass https://127.0.0.1:$upstream_port;
|
proxy_pass http://127.0.0.1:$upstream_port;
|
||||||
proxy_ssl_verify off; # Skip SSL verification for local connection
|
|
||||||
proxy_set_header Host \$host;
|
proxy_set_header Host \$host;
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Nginx configuration for api.mnemo-cards.online
|
# Nginx configuration for api.mnemo-cards.online
|
||||||
# Generated by generate-nginx-configs.sh on Thu Nov 27 22:14:27 MSK 2025
|
# Generated by generate-nginx-configs.sh on Thu Nov 27 22:41:03 MSK 2025
|
||||||
# Service: api
|
# Service: api
|
||||||
|
|
||||||
server {
|
server {
|
||||||
|
|
@ -28,22 +28,11 @@ server {
|
||||||
# API Backend specific settings
|
# API Backend specific settings
|
||||||
client_max_body_size 100M;
|
client_max_body_size 100M;
|
||||||
|
|
||||||
# CORS headers for API
|
# CORS headers are handled by the backend server, nginx only proxies
|
||||||
add_header Access-Control-Allow-Origin "*" always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS" always;
|
|
||||||
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization, user_token, request_token, app_version" always;
|
|
||||||
add_header Access-Control-Expose-Headers "Authorization" always;
|
|
||||||
add_header Access-Control-Max-Age "86400" always;
|
|
||||||
|
|
||||||
# Handle preflight OPTIONS requests
|
# Proxy to backend server (HTTP for API - backend runs in dual mode)
|
||||||
if ($request_method = OPTIONS) {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Proxy to backend server (HTTPS for API, HTTP for others)
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass https://127.0.0.1:8081;
|
proxy_pass http://127.0.0.1:8081;
|
||||||
proxy_ssl_verify off; # Skip SSL verification for local connection
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Nginx configuration for code.mnemo-cards.online
|
# Nginx configuration for code.mnemo-cards.online
|
||||||
# Generated by generate-nginx-configs.sh on Thu Nov 27 22:14:27 MSK 2025
|
# Generated by generate-nginx-configs.sh on Thu Nov 27 22:41:03 MSK 2025
|
||||||
# Service: forgejo
|
# Service: forgejo
|
||||||
|
|
||||||
server {
|
server {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Nginx configuration for mnemo-cards.online
|
# Nginx configuration for mnemo-cards.online
|
||||||
# Generated by generate-nginx-configs.sh on Thu Nov 27 22:14:27 MSK 2025
|
# Generated by generate-nginx-configs.sh on Thu Nov 27 22:41:03 MSK 2025
|
||||||
# Service: webapp
|
# Service: webapp
|
||||||
|
|
||||||
server {
|
server {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Nginx configuration for vscode.mnemo-cards.online
|
# Nginx configuration for vscode.mnemo-cards.online
|
||||||
# Generated by generate-nginx-configs.sh on Thu Nov 27 22:14:27 MSK 2025
|
# Generated by generate-nginx-configs.sh on Thu Nov 27 22:41:03 MSK 2025
|
||||||
# Service: vscode
|
# Service: vscode
|
||||||
|
|
||||||
server {
|
server {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue