Skip to content

Commit 20df63e

Browse files
committed
feat(auth): 实现多平台OAuth登录支持与令牌管理优化
新增Web、Android和Windows平台的OAuth回调处理 重构令牌管理逻辑支持Cookie存储与恢复 优化用户信息解析增加容错处理 添加IP地址检测与设备UUID标准化 分离不同平台的回调页面与配置
1 parent 9da72ac commit 20df63e

20 files changed

Lines changed: 1086 additions & 271 deletions

lib/models/auth_token.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@ class AuthToken {
2121
final expiresAt = expiresIn > 0
2222
? DateTime.now().add(Duration(seconds: expiresIn))
2323
: null;
24+
final rawAccessToken = json['access_token'] as String;
25+
final rawRefreshToken = json['refresh_token'] as String?;
26+
27+
final tokenParts = rawAccessToken.split('|');
28+
final normalizedAccessToken = tokenParts.first;
29+
final normalizedRefreshToken =
30+
rawRefreshToken ?? (tokenParts.length > 1 ? tokenParts[1] : null);
2431

2532
return AuthToken(
26-
accessToken: json['access_token'] as String,
27-
refreshToken: json['refresh_token'] as String?,
33+
accessToken: normalizedAccessToken,
34+
refreshToken: normalizedRefreshToken,
2835
tokenType: json['token_type'] as String? ?? 'Bearer',
2936
expiresIn: expiresIn,
3037
expiresAt: expiresAt,
Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import 'dart:convert';
22

3-
enum PendingAuthTargetPlatform { web, android, desktop }
3+
enum PendingAuthTargetPlatform { web, android, windows }
44

55
class PendingAuthSession {
66
const PendingAuthSession({
77
required this.state,
88
required this.codeVerifier,
99
required this.targetPlatform,
10+
required this.redirectUri,
1011
required this.createdAt,
11-
this.desktopPort,
12+
this.loopbackPort,
1213
});
1314

1415
final String state;
1516
final String codeVerifier;
1617
final PendingAuthTargetPlatform targetPlatform;
17-
final int? desktopPort;
18+
final String redirectUri;
19+
final int? loopbackPort;
1820
final DateTime createdAt;
1921

2022
bool get isExpired =>
@@ -25,7 +27,8 @@ class PendingAuthSession {
2527
'state': state,
2628
'code_verifier': codeVerifier,
2729
'target_platform': targetPlatform.name,
28-
'desktop_port': desktopPort,
30+
'redirect_uri': redirectUri,
31+
'loopback_port': loopbackPort,
2932
'created_at': createdAt.toIso8601String(),
3033
};
3134
}
@@ -36,10 +39,9 @@ class PendingAuthSession {
3639
return PendingAuthSession(
3740
state: json['state'] as String,
3841
codeVerifier: json['code_verifier'] as String,
39-
targetPlatform: PendingAuthTargetPlatform.values.firstWhere(
40-
(value) => value.name == json['target_platform'],
41-
),
42-
desktopPort: json['desktop_port'] as int?,
42+
targetPlatform: _parseTargetPlatform(json['target_platform'] as String?),
43+
redirectUri: (json['redirect_uri'] as String?) ?? '',
44+
loopbackPort: (json['loopback_port'] ?? json['desktop_port']) as int?,
4345
createdAt: DateTime.parse(json['created_at'] as String),
4446
);
4547
}
@@ -49,4 +51,17 @@ class PendingAuthSession {
4951
jsonDecode(jsonString) as Map<String, dynamic>,
5052
);
5153
}
54+
55+
static PendingAuthTargetPlatform _parseTargetPlatform(String? value) {
56+
if (value == PendingAuthTargetPlatform.web.name) {
57+
return PendingAuthTargetPlatform.web;
58+
}
59+
if (value == PendingAuthTargetPlatform.android.name) {
60+
return PendingAuthTargetPlatform.android;
61+
}
62+
if (value == PendingAuthTargetPlatform.windows.name || value == 'desktop') {
63+
return PendingAuthTargetPlatform.windows;
64+
}
65+
return PendingAuthTargetPlatform.web;
66+
}
5267
}

lib/models/user_info.dart

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,18 @@ class UserInfo {
6060

6161
factory UserInfo.fromJson(Map<String, dynamic> json) {
6262
return UserInfo(
63-
userId: json['user_id'] as String,
64-
email: json['email'] as String,
65-
name: json['name'] as String,
63+
userId: _stringValue(json['user_id']),
64+
email: _stringValue(json['email']),
65+
name: _stringValue(json['name'], fallback: '用户'),
6666
githubUsername: json['github_username'] as String?,
67-
permission: json['permission'].toString(),
68-
role: json['role'] as String,
67+
permission: _stringValue(json['permission'], fallback: 'user'),
68+
role: _stringValue(json['role'], fallback: '普通用户'),
6969
avatarUrl: json['avatar_url'] as String?,
7070
backgroundUrl: json['background_url'] as String?,
7171
bio: json['bio'] as String? ?? '',
72-
tags: json['tags'] is List ? (json['tags'] as List<dynamic>).map((e) => e.toString()).toList() : [],
72+
tags: json['tags'] is List
73+
? (json['tags'] as List<dynamic>).map((e) => e.toString()).toList()
74+
: [],
7375
gender: json['gender'] as String? ?? 'secret',
7476
genderVisible: json['gender_visible'] as bool? ?? false,
7577
birthDate: json['birth_date'] as String?,
@@ -80,15 +82,32 @@ class UserInfo {
8082
locationVisible: json['location_visible'] as bool? ?? false,
8183
website: json['website'] as String?,
8284
emailVisible: json['email_visible'] as bool? ?? false,
83-
developedPlatforms: (json['developed_platforms'] as List<dynamic>?)?.map((e) => e.toString()).toList() ?? [],
84-
contributedPlatforms: (json['contributed_platforms'] as List<dynamic>?)?.map((e) => e.toString()).toList() ?? [],
85+
developedPlatforms:
86+
(json['developed_platforms'] as List<dynamic>?)
87+
?.map((e) => e.toString())
88+
.toList() ??
89+
[],
90+
contributedPlatforms:
91+
(json['contributed_platforms'] as List<dynamic>?)
92+
?.map((e) => e.toString())
93+
.toList() ??
94+
[],
8595
userType: json['user_type'] as String? ?? 'normal',
86-
createdAt: json['created_at'] as String,
87-
platformId: json['platform_id'] as String,
88-
loginTime: json['login_time'] as String,
96+
createdAt: _stringValue(json['created_at']),
97+
platformId: _stringValue(json['platform_id']),
98+
loginTime: _stringValue(json['login_time']),
8999
);
90100
}
91101

102+
static String _stringValue(Object? value, {String fallback = ''}) {
103+
if (value == null) {
104+
return fallback;
105+
}
106+
107+
final text = value.toString();
108+
return text.isEmpty ? fallback : text;
109+
}
110+
92111
Map<String, dynamic> toJson() {
93112
return {
94113
'user_id': userId,

lib/services/auth/auth_config.dart

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@ class AuthConfig {
1919
defaultValue: 'https://secrandom-online.sectl.top/auth_callback',
2020
);
2121

22+
static const String authCallbackWebUrl = String.fromEnvironment(
23+
'SECTL_AUTH_CALLBACK_WEB_URL',
24+
defaultValue: 'https://secrandom-online.sectl.top/auth_callback_web.html',
25+
);
26+
27+
static const String authCallbackAndroidUrl = String.fromEnvironment(
28+
'SECTL_AUTH_CALLBACK_ANDROID_URL',
29+
defaultValue:
30+
'https://secrandom-online.sectl.top/auth_callback_android.html',
31+
);
32+
33+
static const String authCallbackWindowsUrl = String.fromEnvironment(
34+
'SECTL_AUTH_CALLBACK_WINDOWS_URL',
35+
defaultValue:
36+
'https://secrandom-online.sectl.top/auth_callback_windows.html',
37+
);
38+
2239
static const String webAppUrl = String.fromEnvironment(
2340
'SECTL_WEB_APP_URL',
2441
defaultValue: 'https://secrandom-online.sectl.top/',
@@ -35,15 +52,30 @@ class AuthConfig {
3552
static const String refreshEndpoint = '/api/oauth/refresh';
3653
static const String userInfoEndpoint = '/api/oauth/userinfo';
3754
static const String logoutEndpoint = '/api/oauth/logout';
55+
static const String publicIpLookupUrl = String.fromEnvironment(
56+
'SECTL_PUBLIC_IP_URL',
57+
defaultValue: 'https://api64.ipify.org?format=json',
58+
);
3859

3960
static const String callbackScheme = 'secrandom';
4061
static const String callbackHost = 'auth';
4162
static const String callbackPath = '/callback';
4263

4364
static const String loopbackHost = '127.0.0.1';
44-
static const int loopbackPort = 8788;
65+
static const int windowsLoopbackPort = int.fromEnvironment(
66+
'SECTL_WINDOWS_LOOPBACK_PORT',
67+
defaultValue: 8788,
68+
);
69+
static const int androidLoopbackPort = int.fromEnvironment(
70+
'SECTL_ANDROID_LOOPBACK_PORT',
71+
defaultValue: 8789,
72+
);
4573
static const String loopbackPath = '/callback';
4674

75+
static const int webAuthCookieMaxAgeDays = 30;
76+
static const String webCookieAuthSignalKey = 'oauth_cookie';
77+
static const String webCookieAuthSignalValue = '1';
78+
4779
static const String accessTokenKey = 'sectl_access_token';
4880
static const String refreshTokenKey = 'sectl_refresh_token';
4981
static const String tokenExpiresAtKey = 'sectl_token_expires_at';
@@ -60,6 +92,33 @@ class AuthConfig {
6092
return authCallbackBridgeUrl;
6193
}
6294

95+
static String get webOauthRedirectUri {
96+
if (useMockAuth &&
97+
authCallbackWebUrl ==
98+
'https://secrandom-online.sectl.top/auth_callback_web.html') {
99+
return '$mockAuthBaseUrl/auth_callback_web.html';
100+
}
101+
return authCallbackWebUrl;
102+
}
103+
104+
static String get androidOauthRedirectUri {
105+
if (useMockAuth &&
106+
authCallbackAndroidUrl ==
107+
'https://secrandom-online.sectl.top/auth_callback_android.html') {
108+
return '$mockAuthBaseUrl/auth_callback_android.html';
109+
}
110+
return authCallbackAndroidUrl;
111+
}
112+
113+
static String get windowsOauthRedirectUri {
114+
if (useMockAuth &&
115+
authCallbackWindowsUrl ==
116+
'https://secrandom-online.sectl.top/auth_callback_windows.html') {
117+
return '$mockAuthBaseUrl/auth_callback_windows.html';
118+
}
119+
return authCallbackWindowsUrl;
120+
}
121+
63122
static String get deepLinkCallbackUri =>
64123
'$callbackScheme://$callbackHost$callbackPath';
65124

lib/services/auth/auth_web_security.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ bool isTrustedWebPopupCallbackMessage({
1010
required Object? href,
1111
required String eventOrigin,
1212
required bool isFromExpectedPopupWindow,
13-
String callbackBridgeUrl = AuthConfig.authCallbackBridgeUrl,
13+
String callbackBridgeUrl = AuthConfig.authCallbackWebUrl,
1414
String webAppUrl = AuthConfig.webAppUrl,
1515
}) {
1616
if (messageType != 'sectl-auth-callback') {

lib/services/auth/client_ip.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export 'client_ip_web.dart' if (dart.library.io) 'client_ip_io.dart';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import 'dart:io';
2+
3+
Future<String?> getLocalIpAddress() async {
4+
try {
5+
final interfaces = await NetworkInterface.list(
6+
type: InternetAddressType.any,
7+
includeLoopback: false,
8+
);
9+
10+
for (final interface in interfaces) {
11+
for (final address in interface.addresses) {
12+
if (address.type == InternetAddressType.IPv4 &&
13+
!address.isLoopback &&
14+
address.address.isNotEmpty) {
15+
return address.address;
16+
}
17+
}
18+
}
19+
} catch (_) {
20+
// Fall back to caller defaults.
21+
}
22+
23+
return '127.0.0.1';
24+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Future<String?> getLocalIpAddress() async => null;

0 commit comments

Comments
 (0)