@lobehub/chat 1.99.6 → 1.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/desktop-pr-build.yml +3 -3
- package/.github/workflows/release-desktop-beta.yml +3 -3
- package/CHANGELOG.md +25 -0
- package/apps/desktop/package.json +5 -2
- package/apps/desktop/src/main/controllers/AuthCtr.ts +310 -111
- package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +1 -1
- package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +50 -3
- package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +188 -23
- package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +37 -18
- package/apps/desktop/src/main/types/store.ts +1 -0
- package/apps/desktop/src/preload/electronApi.ts +2 -1
- package/apps/desktop/src/preload/streamer.ts +58 -0
- package/changelog/v1.json +9 -0
- package/docs/development/database-schema.dbml +9 -0
- package/locales/ar/electron.json +3 -0
- package/locales/ar/oauth.json +8 -4
- package/locales/bg-BG/electron.json +3 -0
- package/locales/bg-BG/oauth.json +8 -4
- package/locales/de-DE/electron.json +3 -0
- package/locales/de-DE/oauth.json +9 -5
- package/locales/en-US/electron.json +3 -0
- package/locales/en-US/oauth.json +8 -4
- package/locales/es-ES/electron.json +3 -0
- package/locales/es-ES/oauth.json +9 -5
- package/locales/fa-IR/electron.json +3 -0
- package/locales/fa-IR/oauth.json +8 -4
- package/locales/fr-FR/electron.json +3 -0
- package/locales/fr-FR/oauth.json +8 -4
- package/locales/it-IT/electron.json +3 -0
- package/locales/it-IT/oauth.json +9 -5
- package/locales/ja-JP/electron.json +3 -0
- package/locales/ja-JP/oauth.json +8 -4
- package/locales/ko-KR/electron.json +3 -0
- package/locales/ko-KR/oauth.json +8 -4
- package/locales/nl-NL/electron.json +3 -0
- package/locales/nl-NL/oauth.json +9 -5
- package/locales/pl-PL/electron.json +3 -0
- package/locales/pl-PL/oauth.json +8 -4
- package/locales/pt-BR/electron.json +3 -0
- package/locales/pt-BR/oauth.json +8 -4
- package/locales/ru-RU/electron.json +3 -0
- package/locales/ru-RU/oauth.json +8 -4
- package/locales/tr-TR/electron.json +3 -0
- package/locales/tr-TR/oauth.json +8 -4
- package/locales/vi-VN/electron.json +3 -0
- package/locales/vi-VN/oauth.json +9 -5
- package/locales/zh-CN/electron.json +3 -0
- package/locales/zh-CN/oauth.json +8 -4
- package/locales/zh-TW/electron.json +3 -0
- package/locales/zh-TW/oauth.json +8 -4
- package/package.json +3 -3
- package/packages/electron-client-ipc/src/dispatch.ts +14 -2
- package/packages/electron-client-ipc/src/index.ts +1 -0
- package/packages/electron-client-ipc/src/streamInvoke.ts +62 -0
- package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +5 -0
- package/packages/electron-client-ipc/src/utils/headers.ts +27 -0
- package/packages/electron-client-ipc/src/utils/request.ts +28 -0
- package/src/app/(backend)/oidc/callback/desktop/route.ts +58 -0
- package/src/app/(backend)/oidc/handoff/route.ts +46 -0
- package/src/app/[variants]/oauth/callback/error/page.tsx +55 -0
- package/src/app/[variants]/oauth/callback/layout.tsx +12 -0
- package/src/app/[variants]/oauth/callback/loading.tsx +3 -0
- package/src/app/[variants]/oauth/{consent/[uid] → callback}/success/page.tsx +10 -1
- package/src/app/[variants]/oauth/consent/[uid]/Consent.tsx +7 -1
- package/src/database/client/migrations.json +8 -0
- package/src/database/migrations/0028_oauth_handoffs.sql +8 -0
- package/src/database/migrations/meta/0028_snapshot.json +6055 -0
- package/src/database/migrations/meta/_journal.json +7 -0
- package/src/database/models/oauthHandoff.ts +94 -0
- package/src/database/repositories/tableViewer/index.test.ts +1 -1
- package/src/database/schemas/oidc.ts +46 -0
- package/src/features/ElectronTitlebar/Connection/Waiting.tsx +59 -115
- package/src/features/ElectronTitlebar/Connection/WaitingAnim.tsx +114 -0
- package/src/libs/oidc-provider/config.ts +16 -17
- package/src/libs/oidc-provider/jwt.ts +135 -0
- package/src/libs/oidc-provider/provider.ts +22 -38
- package/src/libs/trpc/client/async.ts +1 -2
- package/src/libs/trpc/client/edge.ts +1 -2
- package/src/libs/trpc/client/lambda.ts +1 -1
- package/src/libs/trpc/client/tools.ts +1 -2
- package/src/libs/trpc/lambda/context.ts +9 -16
- package/src/locales/default/electron.ts +3 -0
- package/src/locales/default/oauth.ts +8 -4
- package/src/middleware.ts +10 -4
- package/src/server/services/oidc/index.ts +0 -71
- package/src/services/chat.ts +5 -1
- package/src/services/electron/remoteServer.ts +0 -7
- package/src/{libs/trpc/client/helpers → utils/electron}/desktopRemoteRPCFetch.ts +22 -7
- package/src/utils/server/auth.ts +22 -0
- package/src/app/[variants]/oauth/consent/[uid]/failed/page.tsx +0 -36
- package/src/app/[variants]/oauth/handoff/Client.tsx +0 -98
- package/src/app/[variants]/oauth/handoff/page.tsx +0 -13
@@ -155,9 +155,9 @@ jobs:
|
|
155
155
|
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
|
156
156
|
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_PROJECT_ID }}
|
157
157
|
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_BASE_URL }}
|
158
|
-
# 将 TEMP 和 TMP 目录设置到
|
159
|
-
TEMP:
|
160
|
-
TMP:
|
158
|
+
# 将 TEMP 和 TMP 目录设置到 C 盘
|
159
|
+
TEMP: C:\temp
|
160
|
+
TMP: C:\temp
|
161
161
|
|
162
162
|
# Linux 平台构建处理
|
163
163
|
- name: Build artifact on Linux
|
@@ -139,9 +139,9 @@ jobs:
|
|
139
139
|
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }}
|
140
140
|
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_BETA_DESKTOP_BASE_URL }}
|
141
141
|
|
142
|
-
# 将 TEMP 和 TMP 目录设置到
|
143
|
-
TEMP:
|
144
|
-
TMP:
|
142
|
+
# 将 TEMP 和 TMP 目录设置到 C 盘
|
143
|
+
TEMP: C:\temp
|
144
|
+
TMP: C:\temp
|
145
145
|
|
146
146
|
# Linux 平台构建处理
|
147
147
|
- name: Build artifact on Linux
|
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
## [Version 1.100.0](https://github.com/lobehub/lobe-chat/compare/v1.99.6...v1.100.0)
|
6
|
+
|
7
|
+
<sup>Released on **2025-07-17**</sup>
|
8
|
+
|
9
|
+
#### ✨ Features
|
10
|
+
|
11
|
+
- **misc**: Refactor desktop oauth and use JWTs token to support remote chat.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### What's improved
|
19
|
+
|
20
|
+
- **misc**: Refactor desktop oauth and use JWTs token to support remote chat, closes [#8446](https://github.com/lobehub/lobe-chat/issues/8446) ([054ca5f](https://github.com/lobehub/lobe-chat/commit/054ca5f))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
5
30
|
### [Version 1.99.6](https://github.com/lobehub/lobe-chat/compare/v1.99.5...v1.99.6)
|
6
31
|
|
7
32
|
<sup>Released on **2025-07-16**</sup>
|
@@ -25,7 +25,7 @@
|
|
25
25
|
"lint": "eslint --cache ",
|
26
26
|
"pg-server": "bun run scripts/pglite-server.ts",
|
27
27
|
"start": "electron-vite preview",
|
28
|
-
"test": "
|
28
|
+
"test": "vitest --run",
|
29
29
|
"typecheck": "tsgo --noEmit -p tsconfig.json"
|
30
30
|
},
|
31
31
|
"dependencies": {
|
@@ -58,6 +58,8 @@
|
|
58
58
|
"electron-vite": "^3.0.0",
|
59
59
|
"execa": "^9.5.2",
|
60
60
|
"fix-path": "^4.0.0",
|
61
|
+
"http-proxy-agent": "^7.0.2",
|
62
|
+
"https-proxy-agent": "^7.0.6",
|
61
63
|
"just-diff": "^6.0.2",
|
62
64
|
"lodash": "^4.17.21",
|
63
65
|
"lodash-es": "^4.17.21",
|
@@ -67,7 +69,8 @@
|
|
67
69
|
"tsx": "^4.19.3",
|
68
70
|
"typescript": "^5.7.3",
|
69
71
|
"undici": "^7.9.0",
|
70
|
-
"vite": "^6.
|
72
|
+
"vite": "^6.3.5",
|
73
|
+
"vitest": "^3.2.4"
|
71
74
|
},
|
72
75
|
"pnpm": {
|
73
76
|
"onlyBuiltDependencies": [
|
@@ -1,10 +1,9 @@
|
|
1
1
|
import { DataSyncConfig } from '@lobechat/electron-client-ipc';
|
2
|
-
import { BrowserWindow,
|
2
|
+
import { BrowserWindow, shell } from 'electron';
|
3
3
|
import crypto from 'node:crypto';
|
4
4
|
import querystring from 'node:querystring';
|
5
5
|
import { URL } from 'node:url';
|
6
6
|
|
7
|
-
import { name } from '@/../../package.json';
|
8
7
|
import { createLogger } from '@/utils/logger';
|
9
8
|
|
10
9
|
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
|
@@ -13,10 +12,9 @@ import { ControllerModule, ipcClientEvent } from './index';
|
|
13
12
|
// Create logger
|
14
13
|
const logger = createLogger('controllers:AuthCtr');
|
15
14
|
|
16
|
-
const protocolPrefix = `com.lobehub.${name}`;
|
17
15
|
/**
|
18
16
|
* Authentication Controller
|
19
|
-
*
|
17
|
+
* 使用中间页 + 轮询的方式实现 OAuth 授权流程
|
20
18
|
*/
|
21
19
|
export default class AuthCtr extends ControllerModule {
|
22
20
|
/**
|
@@ -32,9 +30,29 @@ export default class AuthCtr extends ControllerModule {
|
|
32
30
|
private codeVerifier: string | null = null;
|
33
31
|
private authRequestState: string | null = null;
|
34
32
|
|
35
|
-
|
36
|
-
|
37
|
-
|
33
|
+
/**
|
34
|
+
* 轮询相关参数
|
35
|
+
*/
|
36
|
+
// eslint-disable-next-line no-undef
|
37
|
+
private pollingInterval: NodeJS.Timeout | null = null;
|
38
|
+
private cachedRemoteUrl: string | null = null;
|
39
|
+
|
40
|
+
/**
|
41
|
+
* 自动刷新定时器
|
42
|
+
*/
|
43
|
+
// eslint-disable-next-line no-undef
|
44
|
+
private autoRefreshTimer: NodeJS.Timeout | null = null;
|
45
|
+
|
46
|
+
/**
|
47
|
+
* 构造 redirect_uri,确保授权和令牌交换时使用相同的 URI
|
48
|
+
* @param remoteUrl 远程服务器 URL
|
49
|
+
* @param includeHandoffId 是否包含 handoff ID(仅在授权时需要)
|
50
|
+
*/
|
51
|
+
private constructRedirectUri(remoteUrl: string): string {
|
52
|
+
const callbackUrl = new URL('/oidc/callback/desktop', remoteUrl);
|
53
|
+
|
54
|
+
return callbackUrl.toString();
|
55
|
+
}
|
38
56
|
|
39
57
|
/**
|
40
58
|
* Request OAuth authorization
|
@@ -43,6 +61,9 @@ export default class AuthCtr extends ControllerModule {
|
|
43
61
|
async requestAuthorization(config: DataSyncConfig) {
|
44
62
|
const remoteUrl = await this.remoteServerConfigCtr.getRemoteServerUrl(config);
|
45
63
|
|
64
|
+
// 缓存远程服务器 URL 用于后续轮询
|
65
|
+
this.cachedRemoteUrl = remoteUrl;
|
66
|
+
|
46
67
|
logger.info(
|
47
68
|
`Requesting OAuth authorization, storageMode:${config.storageMode} server URL: ${remoteUrl}`,
|
48
69
|
);
|
@@ -57,8 +78,11 @@ export default class AuthCtr extends ControllerModule {
|
|
57
78
|
this.authRequestState = crypto.randomBytes(16).toString('hex');
|
58
79
|
logger.debug(`Generated state parameter: ${this.authRequestState}`);
|
59
80
|
|
60
|
-
// Construct authorization URL
|
81
|
+
// Construct authorization URL with new redirect_uri
|
61
82
|
const authUrl = new URL('/oidc/auth', remoteUrl);
|
83
|
+
const redirectUri = this.constructRedirectUri(remoteUrl);
|
84
|
+
|
85
|
+
logger.info('redirectUri', redirectUri);
|
62
86
|
|
63
87
|
// Add query parameters
|
64
88
|
authUrl.search = querystring.stringify({
|
@@ -66,7 +90,9 @@ export default class AuthCtr extends ControllerModule {
|
|
66
90
|
code_challenge: codeChallenge,
|
67
91
|
code_challenge_method: 'S256',
|
68
92
|
prompt: 'consent',
|
69
|
-
redirect_uri:
|
93
|
+
redirect_uri: redirectUri,
|
94
|
+
// https://github.com/lobehub/lobe-chat/pull/8450
|
95
|
+
resource: 'urn:lobehub:chat',
|
70
96
|
response_type: 'code',
|
71
97
|
scope: 'profile email offline_access',
|
72
98
|
state: this.authRequestState,
|
@@ -78,6 +104,9 @@ export default class AuthCtr extends ControllerModule {
|
|
78
104
|
await shell.openExternal(authUrl.toString());
|
79
105
|
logger.debug('Opening authorization URL in default browser');
|
80
106
|
|
107
|
+
// Start polling for credentials
|
108
|
+
this.startPolling();
|
109
|
+
|
81
110
|
return { success: true };
|
82
111
|
} catch (error) {
|
83
112
|
logger.error('Authorization request failed:', error);
|
@@ -86,85 +115,188 @@ export default class AuthCtr extends ControllerModule {
|
|
86
115
|
}
|
87
116
|
|
88
117
|
/**
|
89
|
-
*
|
90
|
-
* This method is called when the browser redirects to our custom protocol
|
118
|
+
* 启动轮询机制获取凭证
|
91
119
|
*/
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
// Get authorization code
|
99
|
-
const code = params.get('code');
|
100
|
-
const state = params.get('state');
|
101
|
-
logger.debug(`Got parameters from callback URL: code=${code}, state=${state}`);
|
102
|
-
|
103
|
-
// Validate state parameter to prevent CSRF attacks
|
104
|
-
if (state !== this.authRequestState) {
|
105
|
-
logger.error(
|
106
|
-
`Invalid state parameter: expected ${this.authRequestState}, received ${state}`,
|
107
|
-
);
|
108
|
-
throw new Error('Invalid state parameter');
|
109
|
-
}
|
110
|
-
logger.debug('State parameter validation passed');
|
120
|
+
private startPolling() {
|
121
|
+
if (!this.authRequestState) {
|
122
|
+
logger.error('No handoff ID available for polling');
|
123
|
+
return;
|
124
|
+
}
|
111
125
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
126
|
+
logger.info('Starting credential polling');
|
127
|
+
const pollInterval = 3000; // 3 seconds
|
128
|
+
const maxPollTime = 5 * 60 * 1000; // 5 minutes
|
129
|
+
const startTime = Date.now();
|
130
|
+
|
131
|
+
this.pollingInterval = setInterval(async () => {
|
132
|
+
try {
|
133
|
+
// Check if polling has timed out
|
134
|
+
if (Date.now() - startTime > maxPollTime) {
|
135
|
+
logger.warn('Credential polling timed out');
|
136
|
+
this.stopPolling();
|
137
|
+
this.broadcastAuthorizationFailed('Authorization timed out');
|
138
|
+
return;
|
139
|
+
}
|
116
140
|
|
117
|
-
|
118
|
-
|
119
|
-
|
141
|
+
// Poll for credentials
|
142
|
+
const result = await this.pollForCredentials();
|
143
|
+
|
144
|
+
if (result) {
|
145
|
+
logger.info('Successfully received credentials from polling');
|
146
|
+
this.stopPolling();
|
120
147
|
|
121
|
-
|
122
|
-
|
123
|
-
|
148
|
+
// Validate state parameter
|
149
|
+
if (result.state !== this.authRequestState) {
|
150
|
+
logger.error(
|
151
|
+
`Invalid state parameter: expected ${this.authRequestState}, received ${result.state}`,
|
152
|
+
);
|
153
|
+
this.broadcastAuthorizationFailed('Invalid state parameter');
|
154
|
+
return;
|
155
|
+
}
|
156
|
+
|
157
|
+
// Exchange code for tokens
|
158
|
+
const exchangeResult = await this.exchangeCodeForToken(result.code, this.codeVerifier!);
|
159
|
+
|
160
|
+
if (exchangeResult.success) {
|
161
|
+
logger.info('Authorization successful');
|
162
|
+
this.broadcastAuthorizationSuccessful();
|
163
|
+
} else {
|
164
|
+
logger.warn(`Authorization failed: ${exchangeResult.error || 'Unknown error'}`);
|
165
|
+
this.broadcastAuthorizationFailed(exchangeResult.error || 'Unknown error');
|
166
|
+
}
|
167
|
+
}
|
168
|
+
} catch (error) {
|
169
|
+
logger.error('Error during credential polling:', error);
|
170
|
+
this.stopPolling();
|
171
|
+
this.broadcastAuthorizationFailed('Polling error: ' + error.message);
|
124
172
|
}
|
173
|
+
}, pollInterval);
|
174
|
+
}
|
125
175
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
176
|
+
/**
|
177
|
+
* 停止轮询
|
178
|
+
*/
|
179
|
+
private stopPolling() {
|
180
|
+
if (this.pollingInterval) {
|
181
|
+
clearInterval(this.pollingInterval);
|
182
|
+
this.pollingInterval = null;
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|
186
|
+
/**
|
187
|
+
* 启动自动刷新定时器
|
188
|
+
*/
|
189
|
+
private startAutoRefresh() {
|
190
|
+
// 先停止现有的定时器
|
191
|
+
this.stopAutoRefresh();
|
192
|
+
|
193
|
+
const checkInterval = 2 * 60 * 1000; // 每 2 分钟检查一次
|
194
|
+
logger.debug('Starting auto-refresh timer');
|
195
|
+
|
196
|
+
this.autoRefreshTimer = setInterval(async () => {
|
197
|
+
try {
|
198
|
+
// 检查 token 是否即将过期 (提前 5 分钟刷新)
|
199
|
+
if (this.remoteServerConfigCtr.isTokenExpiringSoon()) {
|
200
|
+
const expiresAt = this.remoteServerConfigCtr.getTokenExpiresAt();
|
201
|
+
logger.info(
|
202
|
+
`Token is expiring soon, triggering auto-refresh. Expires at: ${expiresAt ? new Date(expiresAt).toISOString() : 'unknown'}`,
|
203
|
+
);
|
204
|
+
|
205
|
+
const result = await this.remoteServerConfigCtr.refreshAccessToken();
|
206
|
+
if (result.success) {
|
207
|
+
logger.info('Auto-refresh successful');
|
208
|
+
this.broadcastTokenRefreshed();
|
209
|
+
} else {
|
210
|
+
logger.error(`Auto-refresh failed: ${result.error}`);
|
211
|
+
// 如果自动刷新失败,停止定时器并清除 token
|
212
|
+
this.stopAutoRefresh();
|
213
|
+
await this.remoteServerConfigCtr.clearTokens();
|
214
|
+
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: false });
|
215
|
+
this.broadcastAuthorizationRequired();
|
216
|
+
}
|
217
|
+
}
|
218
|
+
} catch (error) {
|
219
|
+
logger.error('Error during auto-refresh check:', error);
|
131
220
|
}
|
132
|
-
|
221
|
+
}, checkInterval);
|
222
|
+
}
|
133
223
|
|
134
|
-
|
135
|
-
|
136
|
-
|
224
|
+
/**
|
225
|
+
* 停止自动刷新定时器
|
226
|
+
*/
|
227
|
+
private stopAutoRefresh() {
|
228
|
+
if (this.autoRefreshTimer) {
|
229
|
+
clearInterval(this.autoRefreshTimer);
|
230
|
+
this.autoRefreshTimer = null;
|
231
|
+
logger.debug('Stopped auto-refresh timer');
|
232
|
+
}
|
233
|
+
}
|
137
234
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
235
|
+
/**
|
236
|
+
* 轮询获取凭证
|
237
|
+
* 直接发送 HTTP 请求到远程服务器
|
238
|
+
*/
|
239
|
+
private async pollForCredentials(): Promise<{ code: string; state: string } | null> {
|
240
|
+
if (!this.authRequestState || !this.cachedRemoteUrl) {
|
241
|
+
return null;
|
242
|
+
}
|
243
|
+
|
244
|
+
try {
|
245
|
+
// 使用缓存的远程服务器 URL
|
246
|
+
const remoteUrl = this.cachedRemoteUrl;
|
247
|
+
|
248
|
+
// 构造请求 URL
|
249
|
+
const url = new URL('/oidc/handoff', remoteUrl);
|
250
|
+
url.searchParams.set('id', this.authRequestState);
|
251
|
+
url.searchParams.set('client', 'desktop');
|
252
|
+
|
253
|
+
logger.debug(`Polling for credentials: ${url.toString()}`);
|
254
|
+
|
255
|
+
// 直接发送 HTTP 请求
|
256
|
+
const response = await fetch(url.toString(), {
|
257
|
+
headers: {
|
258
|
+
'Content-Type': 'application/json',
|
259
|
+
},
|
260
|
+
method: 'GET',
|
261
|
+
});
|
262
|
+
|
263
|
+
// 检查响应状态
|
264
|
+
if (response.status === 404) {
|
265
|
+
// 凭证还未准备好,这是正常情况
|
266
|
+
return null;
|
146
267
|
}
|
147
268
|
|
148
|
-
|
149
|
-
|
150
|
-
|
269
|
+
if (!response.ok) {
|
270
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
271
|
+
}
|
151
272
|
|
152
|
-
//
|
153
|
-
|
273
|
+
// 解析响应数据
|
274
|
+
const data = (await response.json()) as {
|
275
|
+
data: {
|
276
|
+
id: string;
|
277
|
+
payload: { code: string; state: string };
|
278
|
+
};
|
279
|
+
success: boolean;
|
280
|
+
};
|
281
|
+
|
282
|
+
if (data.success && data.data?.payload) {
|
283
|
+
logger.debug('Successfully retrieved credentials from handoff');
|
284
|
+
return {
|
285
|
+
code: data.data.payload.code,
|
286
|
+
state: data.data.payload.state,
|
287
|
+
};
|
288
|
+
}
|
154
289
|
|
155
|
-
return
|
156
|
-
}
|
157
|
-
|
158
|
-
|
159
|
-
this.authRequestState = null;
|
160
|
-
this.codeVerifier = null;
|
290
|
+
return null;
|
291
|
+
} catch (error) {
|
292
|
+
logger.debug('Polling attempt failed (this is normal):', error.message);
|
293
|
+
return null;
|
161
294
|
}
|
162
295
|
}
|
163
296
|
|
164
297
|
/**
|
165
298
|
* Refresh access token
|
166
299
|
*/
|
167
|
-
@ipcClientEvent('refreshAccessToken')
|
168
300
|
async refreshAccessToken() {
|
169
301
|
logger.info('Starting to refresh access token');
|
170
302
|
try {
|
@@ -175,6 +307,8 @@ export default class AuthCtr extends ControllerModule {
|
|
175
307
|
logger.info('Token refresh successful via AuthCtr call.');
|
176
308
|
// Notify render process that token has been refreshed
|
177
309
|
this.broadcastTokenRefreshed();
|
310
|
+
// Restart auto-refresh timer with new expiration time
|
311
|
+
this.startAutoRefresh();
|
178
312
|
return { success: true };
|
179
313
|
} else {
|
180
314
|
// Throw an error to be caught by the catch block below
|
@@ -188,6 +322,7 @@ export default class AuthCtr extends ControllerModule {
|
|
188
322
|
|
189
323
|
// Refresh failed, clear tokens and disable remote server
|
190
324
|
logger.warn('Refresh failed, clearing tokens and disabling remote server');
|
325
|
+
this.stopAutoRefresh();
|
191
326
|
await this.remoteServerConfigCtr.clearTokens();
|
192
327
|
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: false });
|
193
328
|
|
@@ -198,48 +333,15 @@ export default class AuthCtr extends ControllerModule {
|
|
198
333
|
}
|
199
334
|
}
|
200
335
|
|
201
|
-
/**
|
202
|
-
* Register custom protocol handler
|
203
|
-
*/
|
204
|
-
private registerProtocolHandler() {
|
205
|
-
logger.info(`Registering custom protocol handler ${protocolPrefix}://`);
|
206
|
-
app.setAsDefaultProtocolClient(protocolPrefix);
|
207
|
-
|
208
|
-
// Register custom protocol handler
|
209
|
-
if (process.platform === 'darwin') {
|
210
|
-
// Handle open-url event on macOS
|
211
|
-
logger.debug('Registering open-url event handler for macOS');
|
212
|
-
app.on('open-url', (event, url) => {
|
213
|
-
event.preventDefault();
|
214
|
-
logger.info(`Received open-url event: ${url}`);
|
215
|
-
this.handleAuthCallback(url);
|
216
|
-
});
|
217
|
-
} else {
|
218
|
-
// Handle protocol callback via second-instance event on Windows and Linux
|
219
|
-
logger.debug('Registering second-instance event handler for Windows/Linux');
|
220
|
-
app.on('second-instance', async (event, commandLine) => {
|
221
|
-
// Find the URL from command line arguments
|
222
|
-
const url = commandLine.find((arg) => arg.startsWith(`${protocolPrefix}://`));
|
223
|
-
if (url) {
|
224
|
-
logger.info(`Found URL from second-instance command line arguments: ${url}`);
|
225
|
-
const { success } = await this.handleAuthCallback(url);
|
226
|
-
if (success) {
|
227
|
-
this.app.browserManager.getMainWindow().show();
|
228
|
-
}
|
229
|
-
} else {
|
230
|
-
logger.warn('Protocol URL not found in second-instance command line arguments');
|
231
|
-
}
|
232
|
-
});
|
233
|
-
}
|
234
|
-
|
235
|
-
logger.info(`Registered ${protocolPrefix}:// custom protocol handler`);
|
236
|
-
}
|
237
|
-
|
238
336
|
/**
|
239
337
|
* Exchange authorization code for token
|
240
338
|
*/
|
241
339
|
private async exchangeCodeForToken(code: string, codeVerifier: string) {
|
242
|
-
|
340
|
+
if (!this.cachedRemoteUrl) {
|
341
|
+
throw new Error('No cached remote URL available for token exchange');
|
342
|
+
}
|
343
|
+
|
344
|
+
const remoteUrl = this.cachedRemoteUrl;
|
243
345
|
logger.info('Starting to exchange authorization code for token');
|
244
346
|
try {
|
245
347
|
const tokenUrl = new URL('/oidc/token', remoteUrl);
|
@@ -251,7 +353,7 @@ export default class AuthCtr extends ControllerModule {
|
|
251
353
|
code,
|
252
354
|
code_verifier: codeVerifier,
|
253
355
|
grant_type: 'authorization_code',
|
254
|
-
redirect_uri:
|
356
|
+
redirect_uri: this.constructRedirectUri(remoteUrl),
|
255
357
|
});
|
256
358
|
|
257
359
|
logger.debug('Sending token exchange request');
|
@@ -272,10 +374,20 @@ export default class AuthCtr extends ControllerModule {
|
|
272
374
|
throw new Error(errorMessage);
|
273
375
|
}
|
274
376
|
|
377
|
+
let data;
|
378
|
+
|
275
379
|
// Parse response
|
276
|
-
|
380
|
+
try {
|
381
|
+
data = await response.clone().json();
|
382
|
+
} catch {
|
383
|
+
const status = response.status;
|
384
|
+
|
385
|
+
throw new Error(
|
386
|
+
`Parse JSON failed, please check your server, response status: ${status}, detail:\n\n ${await response.text()} `,
|
387
|
+
);
|
388
|
+
}
|
389
|
+
|
277
390
|
logger.debug('Successfully received token exchange response');
|
278
|
-
// console.log(data); // Keep original log for debugging, or remove/change to logger.debug as needed
|
279
391
|
|
280
392
|
// Ensure response contains necessary fields
|
281
393
|
if (!data.access_token || !data.refresh_token) {
|
@@ -285,13 +397,20 @@ export default class AuthCtr extends ControllerModule {
|
|
285
397
|
|
286
398
|
// Save tokens
|
287
399
|
logger.debug('Starting to save exchanged tokens');
|
288
|
-
await this.remoteServerConfigCtr.saveTokens(
|
400
|
+
await this.remoteServerConfigCtr.saveTokens(
|
401
|
+
data.access_token,
|
402
|
+
data.refresh_token,
|
403
|
+
data.expires_in,
|
404
|
+
);
|
289
405
|
logger.info('Successfully saved exchanged tokens');
|
290
406
|
|
291
407
|
// Set server to active state
|
292
408
|
logger.debug(`Setting remote server to active state: ${remoteUrl}`);
|
293
409
|
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: true });
|
294
410
|
|
411
|
+
// Start auto-refresh timer
|
412
|
+
this.startAutoRefresh();
|
413
|
+
|
295
414
|
return { success: true };
|
296
415
|
} catch (error) {
|
297
416
|
logger.error('Exchanging authorization code failed:', error);
|
@@ -390,4 +509,84 @@ export default class AuthCtr extends ControllerModule {
|
|
390
509
|
logger.debug('Generated code challenge (partial): ' + challenge.slice(0, 10) + '...'); // Avoid logging full sensitive info
|
391
510
|
return challenge;
|
392
511
|
}
|
512
|
+
|
513
|
+
/**
|
514
|
+
* 应用启动后初始化
|
515
|
+
*/
|
516
|
+
afterAppReady() {
|
517
|
+
logger.debug('AuthCtr initialized, checking for existing tokens');
|
518
|
+
this.initializeAutoRefresh();
|
519
|
+
}
|
520
|
+
|
521
|
+
/**
|
522
|
+
* 清理所有定时器
|
523
|
+
*/
|
524
|
+
cleanup() {
|
525
|
+
logger.debug('Cleaning up AuthCtr timers');
|
526
|
+
this.stopPolling();
|
527
|
+
this.stopAutoRefresh();
|
528
|
+
}
|
529
|
+
|
530
|
+
/**
|
531
|
+
* 初始化自动刷新功能
|
532
|
+
* 在应用启动时检查是否有有效的 token,如果有就启动自动刷新定时器
|
533
|
+
*/
|
534
|
+
private async initializeAutoRefresh() {
|
535
|
+
try {
|
536
|
+
const config = await this.remoteServerConfigCtr.getRemoteServerConfig();
|
537
|
+
|
538
|
+
// 检查是否配置了远程服务器且处于活动状态
|
539
|
+
if (!config.active || !config.remoteServerUrl) {
|
540
|
+
logger.debug(
|
541
|
+
'Remote server not active or configured, skipping auto-refresh initialization',
|
542
|
+
);
|
543
|
+
return;
|
544
|
+
}
|
545
|
+
|
546
|
+
// 检查是否有有效的访问令牌
|
547
|
+
const accessToken = await this.remoteServerConfigCtr.getAccessToken();
|
548
|
+
if (!accessToken) {
|
549
|
+
logger.debug('No access token found, skipping auto-refresh initialization');
|
550
|
+
return;
|
551
|
+
}
|
552
|
+
|
553
|
+
// 检查是否有过期时间信息
|
554
|
+
const expiresAt = this.remoteServerConfigCtr.getTokenExpiresAt();
|
555
|
+
if (!expiresAt) {
|
556
|
+
logger.debug('No token expiration time found, skipping auto-refresh initialization');
|
557
|
+
return;
|
558
|
+
}
|
559
|
+
|
560
|
+
// 检查 token 是否已经过期
|
561
|
+
const currentTime = Date.now();
|
562
|
+
if (currentTime >= expiresAt) {
|
563
|
+
logger.info('Token has expired, attempting to refresh it');
|
564
|
+
|
565
|
+
// 尝试刷新 token
|
566
|
+
const refreshResult = await this.remoteServerConfigCtr.refreshAccessToken();
|
567
|
+
if (refreshResult.success) {
|
568
|
+
logger.info('Token refresh successful during initialization');
|
569
|
+
this.broadcastTokenRefreshed();
|
570
|
+
// 重新启动自动刷新定时器
|
571
|
+
this.startAutoRefresh();
|
572
|
+
return;
|
573
|
+
} else {
|
574
|
+
logger.error(`Token refresh failed during initialization: ${refreshResult.error}`);
|
575
|
+
// 只有在刷新失败时才清除 token 并要求重新授权
|
576
|
+
await this.remoteServerConfigCtr.clearTokens();
|
577
|
+
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: false });
|
578
|
+
this.broadcastAuthorizationRequired();
|
579
|
+
return;
|
580
|
+
}
|
581
|
+
}
|
582
|
+
|
583
|
+
// 启动自动刷新定时器
|
584
|
+
logger.info(
|
585
|
+
`Token is valid, starting auto-refresh timer. Token expires at: ${new Date(expiresAt).toISOString()}`,
|
586
|
+
);
|
587
|
+
this.startAutoRefresh();
|
588
|
+
} catch (error) {
|
589
|
+
logger.error('Error during auto-refresh initialization:', error);
|
590
|
+
}
|
591
|
+
}
|
393
592
|
}
|
@@ -135,7 +135,7 @@ export default class NetworkProxyCtr extends ControllerModule {
|
|
135
135
|
/**
|
136
136
|
* 应用初始代理设置
|
137
137
|
*/
|
138
|
-
async
|
138
|
+
async beforeAppReady(): Promise<void> {
|
139
139
|
try {
|
140
140
|
// 获取存储的代理设置
|
141
141
|
const networkProxy = this.app.storeManager.get(
|