@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.
Files changed (92) hide show
  1. package/.github/workflows/desktop-pr-build.yml +3 -3
  2. package/.github/workflows/release-desktop-beta.yml +3 -3
  3. package/CHANGELOG.md +25 -0
  4. package/apps/desktop/package.json +5 -2
  5. package/apps/desktop/src/main/controllers/AuthCtr.ts +310 -111
  6. package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +1 -1
  7. package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +50 -3
  8. package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +188 -23
  9. package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +37 -18
  10. package/apps/desktop/src/main/types/store.ts +1 -0
  11. package/apps/desktop/src/preload/electronApi.ts +2 -1
  12. package/apps/desktop/src/preload/streamer.ts +58 -0
  13. package/changelog/v1.json +9 -0
  14. package/docs/development/database-schema.dbml +9 -0
  15. package/locales/ar/electron.json +3 -0
  16. package/locales/ar/oauth.json +8 -4
  17. package/locales/bg-BG/electron.json +3 -0
  18. package/locales/bg-BG/oauth.json +8 -4
  19. package/locales/de-DE/electron.json +3 -0
  20. package/locales/de-DE/oauth.json +9 -5
  21. package/locales/en-US/electron.json +3 -0
  22. package/locales/en-US/oauth.json +8 -4
  23. package/locales/es-ES/electron.json +3 -0
  24. package/locales/es-ES/oauth.json +9 -5
  25. package/locales/fa-IR/electron.json +3 -0
  26. package/locales/fa-IR/oauth.json +8 -4
  27. package/locales/fr-FR/electron.json +3 -0
  28. package/locales/fr-FR/oauth.json +8 -4
  29. package/locales/it-IT/electron.json +3 -0
  30. package/locales/it-IT/oauth.json +9 -5
  31. package/locales/ja-JP/electron.json +3 -0
  32. package/locales/ja-JP/oauth.json +8 -4
  33. package/locales/ko-KR/electron.json +3 -0
  34. package/locales/ko-KR/oauth.json +8 -4
  35. package/locales/nl-NL/electron.json +3 -0
  36. package/locales/nl-NL/oauth.json +9 -5
  37. package/locales/pl-PL/electron.json +3 -0
  38. package/locales/pl-PL/oauth.json +8 -4
  39. package/locales/pt-BR/electron.json +3 -0
  40. package/locales/pt-BR/oauth.json +8 -4
  41. package/locales/ru-RU/electron.json +3 -0
  42. package/locales/ru-RU/oauth.json +8 -4
  43. package/locales/tr-TR/electron.json +3 -0
  44. package/locales/tr-TR/oauth.json +8 -4
  45. package/locales/vi-VN/electron.json +3 -0
  46. package/locales/vi-VN/oauth.json +9 -5
  47. package/locales/zh-CN/electron.json +3 -0
  48. package/locales/zh-CN/oauth.json +8 -4
  49. package/locales/zh-TW/electron.json +3 -0
  50. package/locales/zh-TW/oauth.json +8 -4
  51. package/package.json +3 -3
  52. package/packages/electron-client-ipc/src/dispatch.ts +14 -2
  53. package/packages/electron-client-ipc/src/index.ts +1 -0
  54. package/packages/electron-client-ipc/src/streamInvoke.ts +62 -0
  55. package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +5 -0
  56. package/packages/electron-client-ipc/src/utils/headers.ts +27 -0
  57. package/packages/electron-client-ipc/src/utils/request.ts +28 -0
  58. package/src/app/(backend)/oidc/callback/desktop/route.ts +58 -0
  59. package/src/app/(backend)/oidc/handoff/route.ts +46 -0
  60. package/src/app/[variants]/oauth/callback/error/page.tsx +55 -0
  61. package/src/app/[variants]/oauth/callback/layout.tsx +12 -0
  62. package/src/app/[variants]/oauth/callback/loading.tsx +3 -0
  63. package/src/app/[variants]/oauth/{consent/[uid] → callback}/success/page.tsx +10 -1
  64. package/src/app/[variants]/oauth/consent/[uid]/Consent.tsx +7 -1
  65. package/src/database/client/migrations.json +8 -0
  66. package/src/database/migrations/0028_oauth_handoffs.sql +8 -0
  67. package/src/database/migrations/meta/0028_snapshot.json +6055 -0
  68. package/src/database/migrations/meta/_journal.json +7 -0
  69. package/src/database/models/oauthHandoff.ts +94 -0
  70. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  71. package/src/database/schemas/oidc.ts +46 -0
  72. package/src/features/ElectronTitlebar/Connection/Waiting.tsx +59 -115
  73. package/src/features/ElectronTitlebar/Connection/WaitingAnim.tsx +114 -0
  74. package/src/libs/oidc-provider/config.ts +16 -17
  75. package/src/libs/oidc-provider/jwt.ts +135 -0
  76. package/src/libs/oidc-provider/provider.ts +22 -38
  77. package/src/libs/trpc/client/async.ts +1 -2
  78. package/src/libs/trpc/client/edge.ts +1 -2
  79. package/src/libs/trpc/client/lambda.ts +1 -1
  80. package/src/libs/trpc/client/tools.ts +1 -2
  81. package/src/libs/trpc/lambda/context.ts +9 -16
  82. package/src/locales/default/electron.ts +3 -0
  83. package/src/locales/default/oauth.ts +8 -4
  84. package/src/middleware.ts +10 -4
  85. package/src/server/services/oidc/index.ts +0 -71
  86. package/src/services/chat.ts +5 -1
  87. package/src/services/electron/remoteServer.ts +0 -7
  88. package/src/{libs/trpc/client/helpers → utils/electron}/desktopRemoteRPCFetch.ts +22 -7
  89. package/src/utils/server/auth.ts +22 -0
  90. package/src/app/[variants]/oauth/consent/[uid]/failed/page.tsx +0 -36
  91. package/src/app/[variants]/oauth/handoff/Client.tsx +0 -98
  92. 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 目录设置到 D
159
- TEMP: D:\temp
160
- TMP: D:\temp
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 目录设置到 D
143
- TEMP: D:\temp
144
- TMP: D:\temp
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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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": "vite --run",
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.2.5"
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, app, shell } from 'electron';
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
- * Used to implement the OAuth authorization flow
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
- beforeAppReady = () => {
36
- this.registerProtocolHandler();
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: `${protocolPrefix}://auth/callback`,
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
- * Handle authorization callback
90
- * This method is called when the browser redirects to our custom protocol
118
+ * 启动轮询机制获取凭证
91
119
  */
92
- async handleAuthCallback(callbackUrl: string) {
93
- logger.info(`Handling authorization callback: ${callbackUrl}`);
94
- try {
95
- const url = new URL(callbackUrl);
96
- const params = new URLSearchParams(url.search);
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
- if (!code) {
113
- logger.error('No authorization code received');
114
- throw new Error('No authorization code received');
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
- // Get configuration information
118
- const config = await this.remoteServerConfigCtr.getRemoteServerConfig();
119
- logger.debug(`Getting remote server configuration: url=${config.remoteServerUrl}`);
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
- if (config.storageMode === 'selfHost' && !config.remoteServerUrl) {
122
- logger.error('Server URL not configured');
123
- throw new Error('No server URL configured');
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
- // Get the previously saved code_verifier
127
- const codeVerifier = this.codeVerifier;
128
- if (!codeVerifier) {
129
- logger.error('Code verifier not found');
130
- throw new Error('No code verifier found');
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
- logger.debug('Found code verifier');
221
+ }, checkInterval);
222
+ }
133
223
 
134
- // Exchange authorization code for token
135
- logger.debug('Starting to exchange authorization code for token');
136
- const result = await this.exchangeCodeForToken(code, codeVerifier);
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
- if (result.success) {
139
- logger.info('Authorization successful');
140
- // Notify render process of successful authorization
141
- this.broadcastAuthorizationSuccessful();
142
- } else {
143
- logger.warn(`Authorization failed: ${result.error || 'Unknown error'}`);
144
- // Notify render process of failed authorization
145
- this.broadcastAuthorizationFailed(result.error || 'Unknown error');
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
- return result;
149
- } catch (error) {
150
- logger.error('Handling authorization callback failed:', error);
269
+ if (!response.ok) {
270
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
271
+ }
151
272
 
152
- // Notify render process of failed authorization
153
- this.broadcastAuthorizationFailed(error.message);
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 { error: error.message, success: false };
156
- } finally {
157
- // Clear authorization request state
158
- logger.debug('Clearing authorization request state');
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
- const remoteUrl = await this.remoteServerConfigCtr.getRemoteServerUrl();
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: `${protocolPrefix}://auth/callback`,
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
- const data = await response.json();
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(data.access_token, data.refresh_token);
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 afterAppReady(): Promise<void> {
138
+ async beforeAppReady(): Promise<void> {
139
139
  try {
140
140
  // 获取存储的代理设置
141
141
  const networkProxy = this.app.storeManager.get(