补充
This commit is contained in:
@@ -2,12 +2,10 @@
|
||||
/// 闲言APP — 登录页面
|
||||
/// 创建时间: 2026-04-28
|
||||
/// 更新时间: 2026-06-05
|
||||
/// 作用: 用户登录界面,支持密码/验证码/Token/老用户/二维码/OAuth社交登录
|
||||
/// 上次更新: 新增Apple/Google/GitHub OAuth社交登录入口
|
||||
/// 作用: 用户登录界面,支持密码/验证码/Token/老用户/二维码登录
|
||||
/// 上次更新: 移除OAuth社交登录(未正式使用)
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
@@ -17,7 +15,6 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../core/router/app_routes.dart';
|
||||
import '../../../core/network/api_exception.dart';
|
||||
import '../../../core/services/device/device_info_service.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
@@ -31,7 +28,6 @@ import '../config/register_config.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../services/email_service.dart';
|
||||
import '../services/oauth_service.dart';
|
||||
import 'login_form_sections.dart';
|
||||
import 'register_section.dart';
|
||||
|
||||
@@ -527,60 +523,6 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
// TODO: [OAuth社交登录] 待各平台OAuth配置完成后启用,详见 docs/OAUTH_INTEGRATION_GUIDE.md
|
||||
// 第二行分隔线:社交登录
|
||||
// Row(
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: Divider(color: ext.textHint.withValues(alpha: 0.15)),
|
||||
// ),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm),
|
||||
// child: Text(
|
||||
// auth.otherLoginMethods,
|
||||
// style: AppTypography.caption2.copyWith(
|
||||
// color: ext.textHint,
|
||||
// fontSize: 10,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: Divider(color: ext.textHint.withValues(alpha: 0.15)),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: AppSpacing.md),
|
||||
// // 第三行:Apple/Google/GitHub
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// // Apple登录(仅iOS/macOS)
|
||||
// if (Platform.isIOS || Platform.isMacOS) ...[
|
||||
// _buildOAuthButton(
|
||||
// ext,
|
||||
// icon: '',
|
||||
// label: 'Apple',
|
||||
// onTap: () => _handleOAuthLogin(OAuthPlatform.apple),
|
||||
// ),
|
||||
// const SizedBox(width: AppSpacing.lg),
|
||||
// ],
|
||||
// // Google登录
|
||||
// _buildOAuthButton(
|
||||
// ext,
|
||||
// icon: '',
|
||||
// label: 'Google',
|
||||
// onTap: () => _handleOAuthLogin(OAuthPlatform.google),
|
||||
// ),
|
||||
// const SizedBox(width: AppSpacing.lg),
|
||||
// // GitHub登录
|
||||
// _buildOAuthButton(
|
||||
// ext,
|
||||
// icon: '',
|
||||
// label: 'GitHub',
|
||||
// onTap: () => _handleOAuthLogin(OAuthPlatform.github),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -654,96 +596,6 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
||||
);
|
||||
}
|
||||
|
||||
/// OAuth社交登录按钮
|
||||
Widget _buildOAuthButton(
|
||||
AppThemeExtension ext, {
|
||||
required String icon,
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: ext.bgCard,
|
||||
border: Border.all(
|
||||
color: ext.textHint.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(icon, style: const TextStyle(fontSize: 22)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 处理OAuth社交登录
|
||||
Future<void> _handleOAuthLogin(OAuthPlatform platform) async {
|
||||
final deviceModel = DeviceInfoService.cachedDeviceModel;
|
||||
final deviceName = DeviceInfoService.cachedDeviceName;
|
||||
|
||||
OAuthLoginResult result;
|
||||
|
||||
switch (platform) {
|
||||
case OAuthPlatform.apple:
|
||||
result = await OAuthService.loginWithApple(
|
||||
deviceName: deviceName,
|
||||
deviceModel: deviceModel,
|
||||
platformType: Platform.isIOS ? 'ios' : 'mac',
|
||||
);
|
||||
case OAuthPlatform.google:
|
||||
result = await OAuthService.loginWithGoogle(
|
||||
deviceName: deviceName,
|
||||
deviceModel: deviceModel,
|
||||
platformType: Platform.isIOS ? 'ios' : 'android',
|
||||
);
|
||||
case OAuthPlatform.github:
|
||||
result = await OAuthService.loginWithGithub(
|
||||
deviceName: deviceName,
|
||||
deviceModel: deviceModel,
|
||||
platformType: Platform.isIOS ? 'ios' : 'android',
|
||||
);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (result.success && result.token != null) {
|
||||
// 登录成功,通过token登录到本地
|
||||
try {
|
||||
await AuthService.tokenLogin(result.token!);
|
||||
ref.invalidate(authProvider);
|
||||
if (mounted) {
|
||||
final t = ref.read(translationsProvider);
|
||||
AppToast.showSuccess(t.auth.loginSuccess);
|
||||
_navigateAfterLogin();
|
||||
}
|
||||
} on ApiException catch (e) {
|
||||
if (mounted) AppToast.showError(e.message);
|
||||
} catch (e) {
|
||||
if (mounted) AppToast.showError('登录失败: $e');
|
||||
}
|
||||
} else if (result.error != null) {
|
||||
if (result.error != '用户取消') {
|
||||
AppToast.showError(result.error!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAgreement(AppThemeExtension ext, TAuth auth) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: AppSpacing.md),
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
/// ============================================================
|
||||
/// 文件: oauth_service.dart
|
||||
/// 创建时间: 2026-06-05
|
||||
/// 更新时间: 2026-06-05
|
||||
/// 名称: OAuth社交登录服务
|
||||
/// 作用: 处理Apple/Google/GitHub第三方登录
|
||||
/// 上次更新: 新增OAuth社交登录服务
|
||||
/// ============================================================
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_web_auth/flutter_web_auth.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
import '../../../core/utils/platform/platform_utils.dart' as pu;
|
||||
|
||||
/// OAuth平台枚举
|
||||
enum OAuthPlatform {
|
||||
apple,
|
||||
google,
|
||||
github;
|
||||
|
||||
/// 平台标识值
|
||||
String get value => switch (this) {
|
||||
OAuthPlatform.apple => 'apple',
|
||||
OAuthPlatform.google => 'google',
|
||||
OAuthPlatform.github => 'github',
|
||||
};
|
||||
|
||||
/// 显示名称
|
||||
String get displayName => switch (this) {
|
||||
OAuthPlatform.apple => 'Apple',
|
||||
OAuthPlatform.google => 'Google',
|
||||
OAuthPlatform.github => 'GitHub',
|
||||
};
|
||||
|
||||
/// 图标emoji
|
||||
String get icon => switch (this) {
|
||||
OAuthPlatform.apple => '',
|
||||
OAuthPlatform.google => '',
|
||||
OAuthPlatform.github => '',
|
||||
};
|
||||
}
|
||||
|
||||
/// OAuth登录结果
|
||||
class OAuthLoginResult {
|
||||
const OAuthLoginResult({
|
||||
required this.success,
|
||||
this.token,
|
||||
this.userinfo,
|
||||
this.isNewUser = false,
|
||||
this.bindPlatform,
|
||||
this.error,
|
||||
});
|
||||
|
||||
final bool success;
|
||||
final String? token;
|
||||
final Map<String, dynamic>? userinfo;
|
||||
final bool isNewUser;
|
||||
final String? bindPlatform;
|
||||
final String? error;
|
||||
|
||||
/// 从服务端响应JSON构造
|
||||
factory OAuthLoginResult.fromJson(Map<String, dynamic> json) {
|
||||
final data = json['data'];
|
||||
final Map<String, dynamic>? dataMap =
|
||||
data is Map<String, dynamic> ? data : null;
|
||||
return OAuthLoginResult(
|
||||
success: json['code'] == 1,
|
||||
token: dataMap?['token'] as String?,
|
||||
userinfo: dataMap?['userinfo'] != null
|
||||
? Map<String, dynamic>.from(dataMap!['userinfo'] as Map)
|
||||
: null,
|
||||
isNewUser: dataMap?['is_new_user'] as bool? ?? false,
|
||||
bindPlatform: dataMap?['bind_platform'] as String?,
|
||||
error: json['code'] != 1 ? json['msg'] as String? : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// OAuth社交登录服务
|
||||
class OAuthService {
|
||||
OAuthService._();
|
||||
|
||||
static const String _tag = 'OAuthService';
|
||||
|
||||
/// ApiClient单例的Dio实例
|
||||
static final Dio _dio = ApiClient.instance.dio;
|
||||
|
||||
// ============================================================
|
||||
// Apple登录
|
||||
// ============================================================
|
||||
|
||||
/// Apple登录
|
||||
///
|
||||
/// 仅支持iOS和macOS平台,通过Sign in with Apple获取id_token后发送到服务端验证。
|
||||
static Future<OAuthLoginResult> loginWithApple({
|
||||
String? deviceName,
|
||||
String? deviceModel,
|
||||
String? platformType,
|
||||
String? deviceId,
|
||||
}) async {
|
||||
try {
|
||||
if (!pu.isIOS && !pu.isMacOS) {
|
||||
return const OAuthLoginResult(
|
||||
success: false,
|
||||
error: 'Apple登录仅支持iOS和macOS',
|
||||
);
|
||||
}
|
||||
|
||||
final credential = await SignInWithApple.getAppleIDCredential(
|
||||
scopes: [
|
||||
AppleIDAuthorizationScopes.email,
|
||||
AppleIDAuthorizationScopes.fullName,
|
||||
],
|
||||
webAuthenticationOptions: pu.isIOS
|
||||
? null
|
||||
: WebAuthenticationOptions(
|
||||
clientId: 'apps.xy.xianyan.service',
|
||||
redirectUri:
|
||||
Uri.parse('https://tools.wktyl.com/oauth/callback'),
|
||||
),
|
||||
);
|
||||
|
||||
Log.i('$_tag: Apple登录获取id_token成功');
|
||||
|
||||
return _loginToServer(
|
||||
platform: OAuthPlatform.apple,
|
||||
idToken: credential.identityToken,
|
||||
authorizationCode: credential.authorizationCode,
|
||||
deviceName: deviceName,
|
||||
deviceModel: deviceModel,
|
||||
platformType: platformType,
|
||||
deviceId: deviceId,
|
||||
);
|
||||
} on SignInWithAppleAuthorizationException catch (e) {
|
||||
if (e.code == AuthorizationErrorCode.canceled) {
|
||||
return const OAuthLoginResult(success: false, error: '用户取消');
|
||||
}
|
||||
Log.e('$_tag: Apple登录失败: $e');
|
||||
return OAuthLoginResult(
|
||||
success: false,
|
||||
error: 'Apple登录失败: ${e.message}',
|
||||
);
|
||||
} on PlatformException catch (e) {
|
||||
Log.e('$_tag: Apple登录平台异常: $e');
|
||||
return const OAuthLoginResult(success: false, error: 'Apple登录失败');
|
||||
} catch (e) {
|
||||
Log.e('$_tag: Apple登录异常: $e');
|
||||
return const OAuthLoginResult(success: false, error: 'Apple登录失败');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Google登录
|
||||
// ============================================================
|
||||
|
||||
/// Google登录
|
||||
///
|
||||
/// 通过Google Sign In获取serverAuthCode后发送到服务端验证。
|
||||
static Future<OAuthLoginResult> loginWithGoogle({
|
||||
String? deviceName,
|
||||
String? deviceModel,
|
||||
String? platformType,
|
||||
String? deviceId,
|
||||
}) async {
|
||||
try {
|
||||
final googleSignIn = GoogleSignIn(scopes: ['email', 'profile']);
|
||||
|
||||
final account = await googleSignIn.signIn();
|
||||
if (account == null) {
|
||||
return const OAuthLoginResult(success: false, error: '用户取消');
|
||||
}
|
||||
|
||||
final auth = await account.authentication;
|
||||
final serverAuthCode = auth.serverAuthCode;
|
||||
|
||||
if (serverAuthCode == null) {
|
||||
Log.w('$_tag: Google serverAuthCode为空,尝试使用idToken');
|
||||
return const OAuthLoginResult(
|
||||
success: false,
|
||||
error: 'Google授权码获取失败,请重试',
|
||||
);
|
||||
}
|
||||
|
||||
Log.i('$_tag: Google登录获取serverAuthCode成功');
|
||||
|
||||
return _loginToServer(
|
||||
platform: OAuthPlatform.google,
|
||||
code: serverAuthCode,
|
||||
deviceName: deviceName,
|
||||
deviceModel: deviceModel,
|
||||
platformType: platformType,
|
||||
deviceId: deviceId,
|
||||
);
|
||||
} on PlatformException catch (e) {
|
||||
if (e.code == 'sign_in_canceled') {
|
||||
return const OAuthLoginResult(success: false, error: '用户取消');
|
||||
}
|
||||
Log.e('$_tag: Google登录平台异常: $e');
|
||||
return const OAuthLoginResult(success: false, error: 'Google登录失败');
|
||||
} catch (e) {
|
||||
Log.e('$_tag: Google登录异常: $e');
|
||||
return const OAuthLoginResult(success: false, error: 'Google登录失败');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GitHub登录
|
||||
// ============================================================
|
||||
|
||||
/// GitHub登录(通过浏览器OAuth)
|
||||
///
|
||||
/// 先从服务端获取OAuth配置,再通过浏览器完成授权流程,最后将授权码发送到服务端。
|
||||
static Future<OAuthLoginResult> loginWithGithub({
|
||||
String? deviceName,
|
||||
String? deviceModel,
|
||||
String? platformType,
|
||||
String? deviceId,
|
||||
}) async {
|
||||
try {
|
||||
// 从服务端获取GitHub OAuth配置
|
||||
final configResp = await _dio.get<Map<String, dynamic>>(
|
||||
'/api/oauth/config',
|
||||
queryParameters: {'platform': 'github'},
|
||||
);
|
||||
final configData = configResp.data;
|
||||
if (configData == null ||
|
||||
configData['code'] != 1 ||
|
||||
(configData['data'] as Map<String, dynamic>?)?['configured'] !=
|
||||
true) {
|
||||
return const OAuthLoginResult(
|
||||
success: false,
|
||||
error: 'GitHub登录暂未配置',
|
||||
);
|
||||
}
|
||||
|
||||
final dataMap = configData['data'] as Map<String, dynamic>;
|
||||
final authorizeUrl = dataMap['authorize_url'] as String;
|
||||
|
||||
// 通过浏览器完成OAuth
|
||||
final result = await FlutterWebAuth.authenticate(
|
||||
url: authorizeUrl,
|
||||
callbackUrlScheme: 'xianyan',
|
||||
);
|
||||
|
||||
// 从回调URL中提取code
|
||||
final callbackUri = Uri.parse(result);
|
||||
final code = callbackUri.queryParameters['code'];
|
||||
if (code == null || code.isEmpty) {
|
||||
return const OAuthLoginResult(
|
||||
success: false,
|
||||
error: 'GitHub授权码获取失败',
|
||||
);
|
||||
}
|
||||
|
||||
Log.i('$_tag: GitHub登录获取授权码成功');
|
||||
|
||||
return _loginToServer(
|
||||
platform: OAuthPlatform.github,
|
||||
code: code,
|
||||
deviceName: deviceName,
|
||||
deviceModel: deviceModel,
|
||||
platformType: platformType,
|
||||
deviceId: deviceId,
|
||||
);
|
||||
} on PlatformException catch (e) {
|
||||
if (e.code == 'CANCELED') {
|
||||
return const OAuthLoginResult(success: false, error: '用户取消');
|
||||
}
|
||||
Log.e('$_tag: GitHub登录平台异常: $e');
|
||||
return const OAuthLoginResult(success: false, error: 'GitHub登录失败');
|
||||
} catch (e) {
|
||||
Log.e('$_tag: GitHub登录异常: $e');
|
||||
return const OAuthLoginResult(success: false, error: 'GitHub登录失败');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 绑定/解绑社交账号
|
||||
// ============================================================
|
||||
|
||||
/// 绑定社交账号
|
||||
static Future<Map<String, dynamic>> bind({
|
||||
required OAuthPlatform platform,
|
||||
String? code,
|
||||
String? idToken,
|
||||
}) async {
|
||||
final resp = await _dio.post<Map<String, dynamic>>(
|
||||
'/api/oauth/bind',
|
||||
data: {
|
||||
'platform': platform.value,
|
||||
if (code != null) 'code': code,
|
||||
if (idToken != null) 'id_token': idToken,
|
||||
},
|
||||
);
|
||||
return resp.data ?? {};
|
||||
}
|
||||
|
||||
/// 解绑社交账号
|
||||
static Future<Map<String, dynamic>> unbind({
|
||||
required OAuthPlatform platform,
|
||||
}) async {
|
||||
final resp = await _dio.post<Map<String, dynamic>>(
|
||||
'/api/oauth/unbind',
|
||||
data: {'platform': platform.value},
|
||||
);
|
||||
return resp.data ?? {};
|
||||
}
|
||||
|
||||
/// 获取已绑定列表
|
||||
static Future<List<Map<String, dynamic>>> getBoundList() async {
|
||||
final resp = await _dio.get<Map<String, dynamic>>('/api/oauth/bound');
|
||||
final data = resp.data;
|
||||
if (data != null && data['code'] == 1) {
|
||||
final innerData = data['data'];
|
||||
if (innerData is Map<String, dynamic> &&
|
||||
innerData['bindings'] != null) {
|
||||
return List<Map<String, dynamic>>.from(
|
||||
innerData['bindings'] as List,
|
||||
);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 内部方法
|
||||
// ============================================================
|
||||
|
||||
/// 向服务端发送登录请求
|
||||
static Future<OAuthLoginResult> _loginToServer({
|
||||
required OAuthPlatform platform,
|
||||
String? code,
|
||||
String? idToken,
|
||||
String? authorizationCode,
|
||||
String? deviceName,
|
||||
String? deviceModel,
|
||||
String? platformType,
|
||||
String? deviceId,
|
||||
}) async {
|
||||
try {
|
||||
final resp = await _dio.post<Map<String, dynamic>>(
|
||||
'/api/oauth/login',
|
||||
data: {
|
||||
'platform': platform.value,
|
||||
if (code != null) 'code': code,
|
||||
if (idToken != null) 'id_token': idToken,
|
||||
if (authorizationCode != null) 'code': authorizationCode,
|
||||
if (deviceName != null) 'device_name': deviceName,
|
||||
if (deviceModel != null) 'device_model': deviceModel,
|
||||
if (platformType != null) 'platform_type': platformType,
|
||||
if (deviceId != null) 'device_id': deviceId,
|
||||
},
|
||||
);
|
||||
|
||||
final result = OAuthLoginResult.fromJson(resp.data ?? {});
|
||||
Log.i(
|
||||
'$_tag: ${platform.displayName}登录${result.success ? "成功" : "失败"}',
|
||||
);
|
||||
return result;
|
||||
} on DioException catch (e) {
|
||||
Log.e('$_tag: 服务端登录请求失败: $e');
|
||||
return const OAuthLoginResult(
|
||||
success: false,
|
||||
error: '网络请求失败,请检查网络连接',
|
||||
);
|
||||
} catch (e) {
|
||||
Log.e('$_tag: 服务端登录异常: $e');
|
||||
return const OAuthLoginResult(success: false, error: '登录失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user