feat(auth): Issue-691

This commit is contained in:
mso7nueh
2025-09-30 00:39:21 +03:00
parent 01f1c39450
commit 27376ca54d
5 changed files with 184 additions and 60 deletions

View File

@@ -54,6 +54,15 @@ cp env.example .env
- `GOOGLE_CHECK_URL` - URL для проверки соединения (Google)
- `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex)
### VK ID Configuration
- `VK_CLIENT_ID` - ID приложения VK (числовой)
- **Примечание**: VK_CLIENT_ID - это числовой "ID приложения" из консоли VK ID, НЕ "Защищённый ключ" или "Сервисный ключ доступа"
### Yandex ID Configuration
- `YANDEX_CLIENT_ID` - ID приложения Yandex OAuth
- `YANDEX_CLIENT_SECRET` - секретный ключ приложения Yandex OAuth
- `YANDEX_REDIRECT_URI` - URI для перенаправления (должен совпадать с настройками в Yandex OAuth приложении)
## Безопасность
⚠️ **ВАЖНО**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле.

View File

@@ -43,4 +43,12 @@ class EnvConfig {
// Connection Check URLs
static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? '';
static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? '';
// VK ID Configuration
static String get vkClientId => dotenv.env['VK_CLIENT_ID'] ?? '';
// Yandex ID Configuration
static String get yandexClientId => dotenv.env['YANDEX_CLIENT_ID'] ?? '';
static String get yandexClientSecret => dotenv.env['YANDEX_CLIENT_SECRET'] ?? '';
static String get yandexRedirectUri => dotenv.env['YANDEX_REDIRECT_URI'] ?? '';
}

View File

@@ -388,11 +388,17 @@ class _SocialButton extends StatelessWidget {
}
}
// ignore: unused_element
void _onVkPressed(BuildContext context) async {
print('[VK] VK button pressed');
try {
final accessToken = await SocialAuthFacade.signInWithVk(context: context);
if (accessToken == null) return;
print('[VK] Got access token: ${accessToken != null ? 'success' : 'null'}');
if (accessToken == null) {
print('[VK] No access token received');
return;
}
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'vk', accessToken: accessToken);
print('[VK] Authentication result: $isAuthenticated');
if (isAuthenticated) {
if (AppRouter.navigatorKey.currentContext != null) {
// Reuse success flow
@@ -400,24 +406,47 @@ void _onVkPressed(BuildContext context) async {
await _reconnectAfterAuth();
}
}
} catch (e) {
print('[VK] Error during VK auth: $e');
}
}
void _onOkPressed() async {
print('[OK] OK button pressed');
try {
final accessToken = await SocialAuthFacade.signInWithOk();
if (accessToken == null) return;
print('[OK] Got access token: ${accessToken != null ? 'success' : 'null'}');
if (accessToken == null) {
print('[OK] No access token received');
return;
}
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'ok', accessToken: accessToken);
print('[OK] Authentication result: $isAuthenticated');
if (isAuthenticated) {
await _reconnectAfterAuth();
}
} catch (e) {
print('[OK] Error during OK auth: $e');
}
}
void _onMailRuPressed() async {
print('[MailRu] MailRu button pressed');
try {
final accessToken = await SocialAuthFacade.signInWithMailRu();
if (accessToken == null) return;
print('[MailRu] Got access token: ${accessToken != null ? 'success' : 'null'}');
if (accessToken == null) {
print('[MailRu] No access token received');
return;
}
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'mailru', accessToken: accessToken);
print('[MailRu] Authentication result: $isAuthenticated');
if (isAuthenticated) {
await _reconnectAfterAuth();
}
} catch (e) {
print('[MailRu] Error during MailRu auth: $e');
}
}
void _onYandexPressed() async {

View File

@@ -1,10 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_login_yandex/flutter_login_yandex.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:vk_id/vk_id.dart';
import 'package:webview_flutter/webview_flutter.dart';
@@ -29,26 +28,52 @@ class SocialAuthFacade {
}
static Future<VkAuthResult?> signInWithVkAndFetchProfile({required BuildContext context}) async {
final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID'];
if (clientIdStr == null || clientIdStr.isEmpty) {
final clientIdStr = EnvConfig.vkClientId.trim();
print('[VK] VK_CLIENT_ID from EnvConfig: $clientIdStr');
if (clientIdStr.isEmpty) {
print('[VK] VK_CLIENT_ID is not set. Configure it in .env');
return null;
}
try {
print('[VK] Creating VkIDController with clientId: $clientIdStr');
dynamic controller = VkIDController(clID: int.parse(clientIdStr));
final authUri = controller.generateAuthorizeLinkWithCodeVerifier();
print('[VK] Generated auth URI: $authUri');
// Extract URL from MapEntry
String authUrl;
if (authUri is MapEntry) {
authUrl = authUri.value.toString();
} else {
authUrl = authUri.toString();
}
print('[VK] Extracted auth URL: $authUrl');
final redirectScheme = 'vk$clientIdStr://';
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme);
if (redirectParams == null) return null;
print('[VK] Redirect scheme: $redirectScheme');
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme);
print('[VK] WebView returned params: $redirectParams');
if (redirectParams == null) {
print('[VK] No redirect params received');
return null;
}
final code = redirectParams['code'];
final deviceId = redirectParams['device_id'];
if (code == null || deviceId == null) return null;
print('[VK] Code: $code, DeviceId: $deviceId');
if (code == null || deviceId == null) {
print('[VK] Missing code or deviceId');
return null;
}
final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId);
final accessToken = _extractAccessToken(tokenResponse);
if (accessToken == null) return null;
return VkAuthResult(accessToken: accessToken);
} catch (_) {
// Instead of exchanging the code on-device (the sdk method is not available),
// return the authorization code so the backend can exchange it securely.
print('[VK] Returning authorization code to backend for exchange');
return VkAuthResult(accessToken: code);
} catch (e) {
print('[VK] Error in signInWithVkAndFetchProfile: $e');
return null;
}
}
@@ -66,10 +91,14 @@ class SocialAuthFacade {
}
static Future<String?> signInWithYandex() async {
final clientId = dotenv.maybeGet('YANDEX_CLIENT_ID')?.trim() ?? Platform.environment['YANDEX_CLIENT_ID'];
final redirectUri = dotenv.maybeGet('YANDEX_REDIRECT_URI')?.trim() ?? Platform.environment['YANDEX_REDIRECT_URI'];
final clientId = EnvConfig.yandexClientId.trim();
final redirectUri = EnvConfig.yandexRedirectUri.trim();
if (clientId == null || clientId.isEmpty || redirectUri == null || redirectUri.isEmpty) {
print('[Yandex] Client ID: $clientId');
print('[Yandex] Redirect URI: $redirectUri');
if (clientId.isEmpty || redirectUri.isEmpty) {
print('[Yandex] Missing YANDEX_CLIENT_ID or YANDEX_REDIRECT_URI in .env');
return null;
}
try {
@@ -83,73 +112,120 @@ class SocialAuthFacade {
}
static Future<String?> _signInWithVkIdProvider({required BuildContext context, required String provider}) async {
final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID'];
if (clientIdStr == null || clientIdStr.isEmpty) {
final clientIdStr = EnvConfig.vkClientId.trim();
if (clientIdStr.isEmpty) {
return null;
}
try {
print('[VK] Provider flow start: $provider, clientId: $clientIdStr');
dynamic controller = VkIDController(clID: int.parse(clientIdStr));
final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider);
// Try different approaches for provider parameter
dynamic authUri;
try {
// First try with provider as string
authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider);
print('[VK] [$provider] Generated auth URI with string provider: $authUri');
} catch (e) {
print('[VK] [$provider] Failed with string provider, trying null: $e');
try {
// Try with null provider
authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: null);
print('[VK] [$provider] Generated auth URI with null provider: $authUri');
} catch (e2) {
print('[VK] [$provider] Failed with null provider, trying without provider: $e2');
// Try without provider parameter
authUri = controller.generateAuthorizeLinkWithCodeVerifier();
print('[VK] [$provider] Generated auth URI without provider: $authUri');
}
}
// Extract URL from MapEntry
String authUrl;
if (authUri is MapEntry) {
authUrl = authUri.value.toString();
} else {
authUrl = authUri.toString();
}
print('[VK] [$provider] Extracted auth URL: $authUrl');
final redirectScheme = 'vk$clientIdStr://';
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme);
print('[VK] [$provider] Redirect scheme: $redirectScheme');
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme);
print('[VK] [$provider] WebView returned params: $redirectParams');
if (redirectParams == null) return null;
final code = redirectParams['code'];
final deviceId = redirectParams['device_id'];
print('[VK] [$provider] Code: $code, DeviceId: $deviceId');
if (code == null || deviceId == null) return null;
final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId, provider: provider);
final accessToken = _extractAccessToken(tokenResponse);
return accessToken;
} catch (_) {
// Return the authorization code to be exchanged by the backend for provider flow
return code;
} catch (e) {
print('[VK] [$provider] Error in provider flow: $e');
return null;
}
}
static String? _extractAccessToken(dynamic tokenResponse) {
if (tokenResponse == null) return null;
if (tokenResponse is Map) {
return (tokenResponse['access_token'] as String?) ?? (tokenResponse['token'] as String?);
}
try {
final token = (tokenResponse as dynamic).accessToken as String?;
if (token != null && token.isNotEmpty) return token;
} catch (_) {}
try {
final token = (tokenResponse as dynamic).token as String?;
if (token != null && token.isNotEmpty) return token;
} catch (_) {}
return null;
}
// Token extraction is no longer needed because we return the authorization code
// to the backend for exchange.
static Future<Map<String, String>?> _presentOAuthWebView({
required BuildContext context,
required String initialUrl,
required String redirectSchemePrefix,
}) async {
print('[VK] WebView: Loading URL: $initialUrl');
print('[VK] WebView: Redirect scheme: $redirectSchemePrefix');
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (request) {
final url = request.url;
print('[VK] WebView: Navigation to: $url');
if (url.startsWith(redirectSchemePrefix)) {
print('[VK] WebView: Redirect detected, parsing params');
final uri = Uri.parse(url);
final params = <String, String>{}..addAll(uri.queryParameters);
print('[VK] WebView: Parsed params: $params');
Navigator.of(context, rootNavigator: true).pop(params);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
onPageStarted: (url) {
print('[VK] WebView: Page started loading: $url');
},
onPageFinished: (url) {
print('[VK] WebView: Page finished loading: $url');
},
onWebResourceError: (error) {
print('[VK] WebView: Error: ${error.description}');
},
),
)
..loadRequest(Uri.parse(initialUrl));
);
try {
await controller.loadRequest(Uri.parse(initialUrl));
} catch (e) {
print('[VK] WebView: Failed to load URL: $e');
return null;
}
return showDialog<Map<String, String>>(
context: context,
barrierDismissible: false,
builder: (_) {
const borderRadius = BorderRadius.all(Radius.circular(16));
return Dialog(
insetPadding: const EdgeInsets.all(16),
shape: const RoundedRectangleBorder(borderRadius: borderRadius),
elevation: 0,
child: ClipRRect(
borderRadius: borderRadius,
child: SizedBox(height: 560, child: WebViewWidget(controller: controller)),
),
);
},
);

View File

@@ -31,6 +31,8 @@ Future<void> main() async {
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await dotenv.load(fileName: ".env");
print('[ENV] Loaded .env file');
print('[ENV] VK_CLIENT_ID: ${dotenv.maybeGet('VK_CLIENT_ID')}');
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await FirebaseMessaging.instance.requestPermission();