forked from eatvio/eatvio_app
feat(auth): Issue-691
This commit is contained in:
@@ -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` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле.
|
||||
|
||||
@@ -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'] ?? '';
|
||||
}
|
||||
|
||||
@@ -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<AuthCubit>().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<AuthCubit>().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<AuthCubit>().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<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 {
|
||||
final accessToken = await SocialAuthFacade.signInWithMailRu();
|
||||
if (accessToken == null) return;
|
||||
final isAuthenticated = await sl<AuthCubit>().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<AuthCubit>().getTokenBySocial(provider: 'mailru', accessToken: accessToken);
|
||||
print('[MailRu] Authentication result: $isAuthenticated');
|
||||
if (isAuthenticated) {
|
||||
await _reconnectAfterAuth();
|
||||
}
|
||||
} catch (e) {
|
||||
print('[MailRu] Error during MailRu auth: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
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)),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user