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
|
||||
---
|
||||
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
|
||||
|
|
@ -3,6 +3,7 @@ alwaysApply: true
|
|||
---
|
||||
Use yx_state and yx_scope,
|
||||
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.
|
||||
When you finish with another step:
|
||||
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 int shareDailyLimit;
|
||||
|
||||
static const String defaultBackendUrl = 'http://localhost:8443';
|
||||
static const String defaultBackendUrl = 'https://api.mnemo-cards.online';
|
||||
static const int defaultShareDailyLimit = 1;
|
||||
|
||||
factory BotConfig.fromArgs(
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -38,6 +38,6 @@ _flutter.buildConfig = {"engineRevision":"d3d45dcf251823c1769909cd43698d126db38d
|
|||
|
||||
_flutter.loader.load({
|
||||
serviceWorkerSettings: {
|
||||
serviceWorkerVersion: "1977728131"
|
||||
serviceWorkerVersion: "2889484362"
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class HttpRepositoryV2 {
|
|||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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: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/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
|
||||
///
|
||||
|
|
@ -24,12 +22,6 @@ class AuthPage extends StatefulWidget {
|
|||
class _AuthPageState extends State<AuthPage> {
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
final _telegramCodeController = TextEditingController();
|
||||
TelegramAuthCodeStatus? _webCodeStatus;
|
||||
Timer? _codeStatusTimer;
|
||||
Timer? _countdownTimer;
|
||||
DateTime? _codeExpiryTime;
|
||||
bool _autoLoginAttempted = false;
|
||||
|
||||
Future<void> _loginWithGoogle(BuildContext context) async {
|
||||
setState(() {
|
||||
|
|
@ -93,454 +85,15 @@ class _AuthPageState extends State<AuthPage> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _loginWithTelegram(BuildContext context) async {
|
||||
final code = _telegramCodeController.text.trim();
|
||||
|
||||
if (code.isEmpty) {
|
||||
setState(() {
|
||||
_errorMessage = 'Введите код авторизации';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
Future<void> _onTelegramLoginSuccess(BuildContext context) async {
|
||||
// Get router before async operations to avoid context issues
|
||||
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');
|
||||
} 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
|
||||
void dispose() {
|
||||
_codeStatusTimer?.cancel();
|
||||
_countdownTimer?.cancel();
|
||||
_telegramCodeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -643,35 +196,9 @@ class _AuthPageState extends State<AuthPage> {
|
|||
),
|
||||
],
|
||||
// Login buttons
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
SignInWithGoogleButton(
|
||||
onPressed: () => _loginWithGoogle(context),
|
||||
isLoading: _isLoading,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Divider
|
||||
|
|
@ -703,154 +230,10 @@ class _AuthPageState extends State<AuthPage> {
|
|||
),
|
||||
const SizedBox(height: 24),
|
||||
// Telegram auth code input
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Введите код из 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,
|
||||
),
|
||||
),
|
||||
SignInWithTelegram(
|
||||
onLoginSuccess: () => _onTelegramLoginSuccess(context),
|
||||
onError: (error) => setState(() => _errorMessage = error),
|
||||
isLoading: _isLoading,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -533,54 +533,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -55,17 +55,7 @@ server {
|
|||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
# CORS headers for API
|
||||
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;
|
||||
}
|
||||
# CORS headers are handled by the backend server, nginx only proxies
|
||||
|
||||
# Proxy to HTTP backend (SSL termination in nginx)
|
||||
location / {
|
||||
|
|
|
|||
|
|
@ -169,22 +169,11 @@ EOF
|
|||
# API Backend specific settings
|
||||
client_max_body_size 100M;
|
||||
|
||||
# CORS headers for API
|
||||
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;
|
||||
# CORS headers are handled by the backend server, nginx only proxies
|
||||
|
||||
# Handle preflight OPTIONS requests
|
||||
if (\$request_method = OPTIONS) {
|
||||
return 204;
|
||||
}
|
||||
|
||||
# Proxy to backend server (HTTPS for API, HTTP for others)
|
||||
# Proxy to backend server (HTTP for API - backend runs in dual mode)
|
||||
location / {
|
||||
proxy_pass https://127.0.0.1:$upstream_port;
|
||||
proxy_ssl_verify off; # Skip SSL verification for local connection
|
||||
proxy_pass http://127.0.0.1:$upstream_port;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# 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
|
||||
|
||||
server {
|
||||
|
|
@ -28,22 +28,11 @@ server {
|
|||
# API Backend specific settings
|
||||
client_max_body_size 100M;
|
||||
|
||||
# CORS headers for API
|
||||
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;
|
||||
# CORS headers are handled by the backend server, nginx only proxies
|
||||
|
||||
# Handle preflight OPTIONS requests
|
||||
if ($request_method = OPTIONS) {
|
||||
return 204;
|
||||
}
|
||||
|
||||
# Proxy to backend server (HTTPS for API, HTTP for others)
|
||||
# Proxy to backend server (HTTP for API - backend runs in dual mode)
|
||||
location / {
|
||||
proxy_pass https://127.0.0.1:8081;
|
||||
proxy_ssl_verify off; # Skip SSL verification for local connection
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# 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
|
||||
|
||||
server {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# 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
|
||||
|
||||
server {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# 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
|
||||
|
||||
server {
|
||||
|
|
|
|||
Loading…
Reference in a new issue