From 27376ca54d244b13d425b5c6c0b31fddc01a1231 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 30 Sep 2025 00:39:21 +0300 Subject: [PATCH] feat(auth): Issue-691 --- ENV_SETUP.md | 9 + lib/core/env_config.dart | 8 + .../auth/presentation/pages/auth_screen.dart | 67 +++++--- lib/features/auth/sdk/social_auth_facade.dart | 158 +++++++++++++----- lib/main.dart | 2 + 5 files changed, 184 insertions(+), 60 deletions(-) diff --git a/ENV_SETUP.md b/ENV_SETUP.md index 0873fb1c..092a3bd9 100644 --- a/ENV_SETUP.md +++ b/ENV_SETUP.md @@ -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` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле. diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart index a3404aeb..446bc65b 100644 --- a/lib/core/env_config.dart +++ b/lib/core/env_config.dart @@ -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'] ?? ''; } diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 970ea8d3..5f9496ac 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -388,35 +388,64 @@ class _SocialButton extends StatelessWidget { } } -// ignore: unused_element void _onVkPressed(BuildContext context) async { - final accessToken = await SocialAuthFacade.signInWithVk(context: context); - if (accessToken == null) return; - final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); - if (isAuthenticated) { - if (AppRouter.navigatorKey.currentContext != null) { - // Reuse success flow - // ignore: use_build_context_synchronously - await _reconnectAfterAuth(); + print('[VK] VK button pressed'); + try { + final accessToken = await SocialAuthFacade.signInWithVk(context: context); + print('[VK] Got access token: ${accessToken != null ? 'success' : 'null'}'); + if (accessToken == null) { + print('[VK] No access token received'); + return; } + final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); + print('[VK] Authentication result: $isAuthenticated'); + if (isAuthenticated) { + if (AppRouter.navigatorKey.currentContext != null) { + // Reuse success flow + // ignore: use_build_context_synchronously + await _reconnectAfterAuth(); + } + } + } catch (e) { + print('[VK] Error during VK auth: $e'); } } void _onOkPressed() async { - final accessToken = await SocialAuthFacade.signInWithOk(); - if (accessToken == null) return; - final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); - if (isAuthenticated) { - await _reconnectAfterAuth(); + print('[OK] OK button pressed'); + try { + final accessToken = await SocialAuthFacade.signInWithOk(); + print('[OK] Got access token: ${accessToken != null ? 'success' : 'null'}'); + if (accessToken == null) { + print('[OK] No access token received'); + return; + } + final isAuthenticated = await sl().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 { - final accessToken = await SocialAuthFacade.signInWithMailRu(); - if (accessToken == null) return; - final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); - if (isAuthenticated) { - await _reconnectAfterAuth(); + print('[MailRu] MailRu button pressed'); + try { + final accessToken = await SocialAuthFacade.signInWithMailRu(); + print('[MailRu] Got access token: ${accessToken != null ? 'success' : 'null'}'); + if (accessToken == null) { + print('[MailRu] No access token received'); + return; + } + final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); + print('[MailRu] Authentication result: $isAuthenticated'); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } + } catch (e) { + print('[MailRu] Error during MailRu auth: $e'); } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index e5761498..90d07e95 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -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 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 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 _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?> _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 = {}..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>( context: context, barrierDismissible: false, builder: (_) { + const borderRadius = BorderRadius.all(Radius.circular(16)); return Dialog( insetPadding: const EdgeInsets.all(16), - child: SizedBox(height: 560, child: WebViewWidget(controller: controller)), + shape: const RoundedRectangleBorder(borderRadius: borderRadius), + elevation: 0, + child: ClipRRect( + borderRadius: borderRadius, + child: SizedBox(height: 560, child: WebViewWidget(controller: controller)), + ), ); }, ); diff --git a/lib/main.dart b/lib/main.dart index d716f3fa..e0492219 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,6 +31,8 @@ Future 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();