@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
@@ -0,0 +1,135 @@
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
2
|
+
import debug from 'debug';
|
3
|
+
import { importJWK, jwtVerify } from 'jose';
|
4
|
+
|
5
|
+
import { oidcEnv } from '@/envs/oidc';
|
6
|
+
|
7
|
+
const log = debug('oidc-jwt');
|
8
|
+
|
9
|
+
/**
|
10
|
+
* 从环境变量中获取 JWKS
|
11
|
+
* 该 JWKS 是一个包含 RS256 私钥的 JSON 对象
|
12
|
+
*/
|
13
|
+
export const getJWKS = (): object => {
|
14
|
+
try {
|
15
|
+
const jwksString = oidcEnv.OIDC_JWKS_KEY;
|
16
|
+
|
17
|
+
if (!jwksString) {
|
18
|
+
throw new Error(
|
19
|
+
'OIDC_JWKS_KEY 环境变量是必需的。请使用 scripts/generate-oidc-jwk.mjs 生成 JWKS。',
|
20
|
+
);
|
21
|
+
}
|
22
|
+
|
23
|
+
// 尝试解析 JWKS JSON 字符串
|
24
|
+
const jwks = JSON.parse(jwksString);
|
25
|
+
|
26
|
+
// 检查 JWKS 格式是否正确
|
27
|
+
if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
|
28
|
+
throw new Error('JWKS 格式无效: 缺少或为空的 keys 数组');
|
29
|
+
}
|
30
|
+
|
31
|
+
// 检查是否有 RS256 算法的密钥
|
32
|
+
const hasRS256Key = jwks.keys.some((key: any) => key.alg === 'RS256' && key.kty === 'RSA');
|
33
|
+
if (!hasRS256Key) {
|
34
|
+
throw new Error('JWKS 中没有找到 RS256 算法的 RSA 密钥');
|
35
|
+
}
|
36
|
+
|
37
|
+
return jwks;
|
38
|
+
} catch (error) {
|
39
|
+
console.error('解析 JWKS 失败:', error);
|
40
|
+
throw new Error(`OIDC_JWKS_KEY 解析错误: ${(error as Error).message}`);
|
41
|
+
}
|
42
|
+
};
|
43
|
+
|
44
|
+
/**
|
45
|
+
* 从环境变量中获取 JWKS 并提取第一个 RSA 密钥
|
46
|
+
*/
|
47
|
+
const getJWKSPublicKey = async () => {
|
48
|
+
try {
|
49
|
+
const jwksString = oidcEnv.OIDC_JWKS_KEY;
|
50
|
+
|
51
|
+
if (!jwksString) {
|
52
|
+
throw new Error('OIDC_JWKS_KEY 环境变量未设置');
|
53
|
+
}
|
54
|
+
|
55
|
+
const jwks = JSON.parse(jwksString);
|
56
|
+
|
57
|
+
if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
|
58
|
+
throw new Error('JWKS 格式无效: 缺少或为空的 keys 数组');
|
59
|
+
}
|
60
|
+
|
61
|
+
// 查找 RS256 算法的 RSA 密钥
|
62
|
+
const rsaKey = jwks.keys.find((key: any) => key.alg === 'RS256' && key.kty === 'RSA');
|
63
|
+
|
64
|
+
if (!rsaKey) {
|
65
|
+
throw new Error('JWKS 中没有找到 RS256 算法的 RSA 密钥');
|
66
|
+
}
|
67
|
+
|
68
|
+
// 导入 JWK 为公钥
|
69
|
+
const publicKey = await importJWK(rsaKey, 'RS256');
|
70
|
+
|
71
|
+
return publicKey;
|
72
|
+
} catch (error) {
|
73
|
+
log('获取 JWKS 公钥失败: %O', error);
|
74
|
+
throw new Error(`JWKS 公钥获取失败: ${(error as Error).message}`);
|
75
|
+
}
|
76
|
+
};
|
77
|
+
|
78
|
+
/**
|
79
|
+
* 验证 OIDC JWT Access Token
|
80
|
+
* @param token - JWT access token
|
81
|
+
* @returns 解析后的 token payload 和用户信息
|
82
|
+
*/
|
83
|
+
export const validateOIDCJWT = async (token: string) => {
|
84
|
+
try {
|
85
|
+
log('开始验证 OIDC JWT token');
|
86
|
+
|
87
|
+
// 获取公钥
|
88
|
+
const publicKey = await getJWKSPublicKey();
|
89
|
+
|
90
|
+
// 验证 JWT
|
91
|
+
const { payload } = await jwtVerify(token, publicKey, {
|
92
|
+
algorithms: ['RS256'],
|
93
|
+
// 可以添加其他验证选项,如 issuer、audience 等
|
94
|
+
});
|
95
|
+
|
96
|
+
log('JWT 验证成功,payload: %O', payload);
|
97
|
+
|
98
|
+
// 提取用户信息
|
99
|
+
const userId = payload.sub;
|
100
|
+
const clientId = payload.aud;
|
101
|
+
|
102
|
+
if (!userId) {
|
103
|
+
throw new TRPCError({
|
104
|
+
code: 'UNAUTHORIZED',
|
105
|
+
message: 'JWT token 中缺少用户 ID (sub)',
|
106
|
+
});
|
107
|
+
}
|
108
|
+
|
109
|
+
return {
|
110
|
+
clientId,
|
111
|
+
payload,
|
112
|
+
tokenData: {
|
113
|
+
aud: clientId,
|
114
|
+
client_id: clientId,
|
115
|
+
exp: payload.exp,
|
116
|
+
iat: payload.iat,
|
117
|
+
jti: payload.jti,
|
118
|
+
scope: payload.scope,
|
119
|
+
sub: userId,
|
120
|
+
},
|
121
|
+
userId,
|
122
|
+
};
|
123
|
+
} catch (error) {
|
124
|
+
if (error instanceof TRPCError) {
|
125
|
+
throw error;
|
126
|
+
}
|
127
|
+
|
128
|
+
log('JWT 验证失败: %O', error);
|
129
|
+
|
130
|
+
throw new TRPCError({
|
131
|
+
code: 'UNAUTHORIZED',
|
132
|
+
message: `JWT token 验证失败: ${(error as Error).message}`,
|
133
|
+
});
|
134
|
+
}
|
135
|
+
};
|
@@ -1,12 +1,12 @@
|
|
1
1
|
import debug from 'debug';
|
2
|
-
import Provider, { Configuration, KoaContextWithOIDC } from 'oidc-provider';
|
2
|
+
import Provider, { Configuration, KoaContextWithOIDC, errors } from 'oidc-provider';
|
3
3
|
import urlJoin from 'url-join';
|
4
4
|
|
5
5
|
import { serverDBEnv } from '@/config/db';
|
6
6
|
import { UserModel } from '@/database/models/user';
|
7
7
|
import { LobeChatDatabase } from '@/database/type';
|
8
8
|
import { appEnv } from '@/envs/app';
|
9
|
-
import {
|
9
|
+
import { getJWKS } from '@/libs/oidc-provider/jwt';
|
10
10
|
|
11
11
|
import { DrizzleAdapter } from './adapter';
|
12
12
|
import { defaultClaims, defaultClients, defaultScopes } from './config';
|
@@ -14,40 +14,7 @@ import { createInteractionPolicy } from './interaction-policy';
|
|
14
14
|
|
15
15
|
const logProvider = debug('lobe-oidc:provider'); // <--- 添加 provider 日志实例
|
16
16
|
|
17
|
-
|
18
|
-
* 从环境变量中获取 JWKS
|
19
|
-
* 该 JWKS 是一个包含 RS256 私钥的 JSON 对象
|
20
|
-
*/
|
21
|
-
const getJWKS = (): object => {
|
22
|
-
try {
|
23
|
-
const jwksString = oidcEnv.OIDC_JWKS_KEY;
|
24
|
-
|
25
|
-
if (!jwksString) {
|
26
|
-
throw new Error(
|
27
|
-
'OIDC_JWKS_KEY 环境变量是必需的。请使用 scripts/generate-oidc-jwk.mjs 生成 JWKS。',
|
28
|
-
);
|
29
|
-
}
|
30
|
-
|
31
|
-
// 尝试解析 JWKS JSON 字符串
|
32
|
-
const jwks = JSON.parse(jwksString);
|
33
|
-
|
34
|
-
// 检查 JWKS 格式是否正确
|
35
|
-
if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
|
36
|
-
throw new Error('JWKS 格式无效: 缺少或为空的 keys 数组');
|
37
|
-
}
|
38
|
-
|
39
|
-
// 检查是否有 RS256 算法的密钥
|
40
|
-
const hasRS256Key = jwks.keys.some((key: any) => key.alg === 'RS256' && key.kty === 'RSA');
|
41
|
-
if (!hasRS256Key) {
|
42
|
-
throw new Error('JWKS 中没有找到 RS256 算法的 RSA 密钥');
|
43
|
-
}
|
44
|
-
|
45
|
-
return jwks;
|
46
|
-
} catch (error) {
|
47
|
-
console.error('解析 JWKS 失败:', error);
|
48
|
-
throw new Error(`OIDC_JWKS_KEY 解析错误: ${(error as Error).message}`);
|
49
|
-
}
|
50
|
-
};
|
17
|
+
export const API_AUDIENCE = 'urn:lobehub:chat'; // <-- 把这里换成你自己的 API 标识符
|
51
18
|
|
52
19
|
/**
|
53
20
|
* 获取 Cookie 密钥,使用 KEY_VAULTS_SECRET
|
@@ -123,7 +90,24 @@ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider
|
|
123
90
|
devInteractions: { enabled: false },
|
124
91
|
deviceFlow: { enabled: false },
|
125
92
|
introspection: { enabled: true },
|
126
|
-
resourceIndicators: {
|
93
|
+
resourceIndicators: {
|
94
|
+
defaultResource: () => API_AUDIENCE,
|
95
|
+
enabled: true,
|
96
|
+
getResourceServerInfo: (ctx, resourceIndicator) => {
|
97
|
+
logProvider('getResourceServerInfo called with indicator: %s', resourceIndicator); // <-- 添加这行日志
|
98
|
+
if (resourceIndicator === API_AUDIENCE) {
|
99
|
+
logProvider('Indicator matches API_AUDIENCE, returning JWT config.'); // <-- 添加这行日志
|
100
|
+
return {
|
101
|
+
accessTokenFormat: 'jwt',
|
102
|
+
audience: API_AUDIENCE,
|
103
|
+
scope: ctx.oidc.client?.scope || 'read',
|
104
|
+
};
|
105
|
+
}
|
106
|
+
|
107
|
+
logProvider('Indicator does not match API_AUDIENCE, throwing InvalidTarget.'); // <-- 添加这行日志
|
108
|
+
throw new errors.InvalidTarget();
|
109
|
+
},
|
110
|
+
},
|
127
111
|
revocation: { enabled: true },
|
128
112
|
rpInitiatedLogout: { enabled: true },
|
129
113
|
userinfo: { enabled: true },
|
@@ -256,7 +240,7 @@ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider
|
|
256
240
|
|
257
241
|
// 8. 令牌有效期
|
258
242
|
ttl: {
|
259
|
-
AccessToken:
|
243
|
+
AccessToken: 25 * 3600, // 25 hour
|
260
244
|
AuthorizationCode: 600, // 10 minutes
|
261
245
|
DeviceCode: 600, // 10 minutes (if enabled)
|
262
246
|
|
@@ -3,8 +3,7 @@ import superjson from 'superjson';
|
|
3
3
|
|
4
4
|
import { isDesktop } from '@/const/version';
|
5
5
|
import { AsyncRouter } from '@/server/routers/async';
|
6
|
-
|
7
|
-
import { fetchWithDesktopRemoteRPC } from './helpers/desktopRemoteRPCFetch';
|
6
|
+
import { fetchWithDesktopRemoteRPC } from '@/utils/electron/desktopRemoteRPCFetch';
|
8
7
|
|
9
8
|
export const asyncClient = createTRPCClient<AsyncRouter>({
|
10
9
|
links: [
|
@@ -4,8 +4,7 @@ import superjson from 'superjson';
|
|
4
4
|
import { isDesktop } from '@/const/version';
|
5
5
|
import type { EdgeRouter } from '@/server/routers/edge';
|
6
6
|
import { withBasePath } from '@/utils/basePath';
|
7
|
-
|
8
|
-
import { fetchWithDesktopRemoteRPC } from './helpers/desktopRemoteRPCFetch';
|
7
|
+
import { fetchWithDesktopRemoteRPC } from '@/utils/electron/desktopRemoteRPCFetch';
|
9
8
|
|
10
9
|
export const edgeClient = createTRPCClient<EdgeRouter>({
|
11
10
|
links: [
|
@@ -15,7 +15,7 @@ const links = [
|
|
15
15
|
httpBatchLink({
|
16
16
|
fetch: async (input, init) => {
|
17
17
|
if (isDesktop) {
|
18
|
-
const { desktopRemoteRPCFetch } = await import('
|
18
|
+
const { desktopRemoteRPCFetch } = await import('@/utils/electron/desktopRemoteRPCFetch');
|
19
19
|
|
20
20
|
const res = await desktopRemoteRPCFetch(input as string, init);
|
21
21
|
|
@@ -3,8 +3,7 @@ import superjson from 'superjson';
|
|
3
3
|
|
4
4
|
import { isDesktop } from '@/const/version';
|
5
5
|
import type { ToolsRouter } from '@/server/routers/tools';
|
6
|
-
|
7
|
-
import { fetchWithDesktopRemoteRPC } from './helpers/desktopRemoteRPCFetch';
|
6
|
+
import { fetchWithDesktopRemoteRPC } from '@/utils/electron/desktopRemoteRPCFetch';
|
8
7
|
|
9
8
|
export const toolsClient = createTRPCClient<ToolsRouter>({
|
10
9
|
links: [
|
@@ -6,7 +6,7 @@ import { NextRequest } from 'next/server';
|
|
6
6
|
import { JWTPayload, LOBE_CHAT_AUTH_HEADER, enableClerk, enableNextAuth } from '@/const/auth';
|
7
7
|
import { oidcEnv } from '@/envs/oidc';
|
8
8
|
import { ClerkAuth, IClerkAuth } from '@/libs/clerk-auth';
|
9
|
-
import {
|
9
|
+
import { validateOIDCJWT } from '@/libs/oidc-provider/jwt';
|
10
10
|
|
11
11
|
// Create context logger namespace
|
12
12
|
const log = debug('lobe-trpc:lambda:context');
|
@@ -98,26 +98,19 @@ export const createLambdaContext = async (request: NextRequest): Promise<LambdaC
|
|
98
98
|
let auth;
|
99
99
|
let oidcAuth = null;
|
100
100
|
|
101
|
-
// Prioritize checking
|
101
|
+
// Prioritize checking for OIDC authentication (both standard Authorization and custom Oidc-Auth headers)
|
102
102
|
if (oidcEnv.ENABLE_OIDC) {
|
103
103
|
log('OIDC enabled, attempting OIDC authentication');
|
104
104
|
const standardAuthorization = request.headers.get('Authorization');
|
105
|
+
const oidcAuthToken = request.headers.get('Oidc-Auth');
|
105
106
|
log('Standard Authorization header: %s', standardAuthorization ? 'exists' : 'not found');
|
107
|
+
log('Oidc-Auth header: %s', oidcAuthToken ? 'exists' : 'not found');
|
106
108
|
|
107
109
|
try {
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
if (bearerToken) {
|
113
|
-
const { OIDCService } = await import('@/server/services/oidc');
|
114
|
-
|
115
|
-
// Initialize OIDC service
|
116
|
-
log('Initializing OIDC service');
|
117
|
-
const oidcService = await OIDCService.initialize();
|
118
|
-
// Validate token using OIDCService
|
119
|
-
log('Validating OIDC token');
|
120
|
-
const tokenInfo = await oidcService.validateToken(bearerToken);
|
110
|
+
if (oidcAuthToken) {
|
111
|
+
// Use direct JWT validation instead of database lookup
|
112
|
+
const tokenInfo = await validateOIDCJWT(oidcAuthToken);
|
113
|
+
|
121
114
|
oidcAuth = {
|
122
115
|
payload: tokenInfo.tokenData,
|
123
116
|
...tokenInfo.tokenData, // Spread payload into oidcAuth
|
@@ -136,7 +129,7 @@ export const createLambdaContext = async (request: NextRequest): Promise<LambdaC
|
|
136
129
|
}
|
137
130
|
} catch (error) {
|
138
131
|
// If OIDC authentication fails, log error and continue with other authentication methods
|
139
|
-
if (
|
132
|
+
if (oidcAuthToken) {
|
140
133
|
log('OIDC authentication failed, error: %O', error);
|
141
134
|
console.error('OIDC authentication failed, trying other methods:', error);
|
142
135
|
}
|
@@ -101,7 +101,10 @@ const electron = {
|
|
101
101
|
waitingOAuth: {
|
102
102
|
cancel: '取消',
|
103
103
|
description: '浏览器已打开授权页面,请在浏览器中完成授权',
|
104
|
+
error: '授权失败: {{error}}',
|
105
|
+
errorTitle: '授权连接失败',
|
104
106
|
helpText: '如果浏览器没有自动打开,请点击取消后重新尝试',
|
107
|
+
retry: '重试',
|
105
108
|
title: '等待授权连接',
|
106
109
|
},
|
107
110
|
};
|
@@ -29,10 +29,14 @@ const oauth = {
|
|
29
29
|
},
|
30
30
|
title: '授权 {{clientName}}',
|
31
31
|
},
|
32
|
-
|
32
|
+
error: {
|
33
33
|
backToHome: '返回首页',
|
34
|
-
|
35
|
-
|
34
|
+
desc: 'OAuth 授权失败,失败原因:{{reason}}',
|
35
|
+
reason: {
|
36
|
+
internal_error: '服务端错误',
|
37
|
+
invalid_request: '无效的请求参数',
|
38
|
+
},
|
39
|
+
title: '授权失败',
|
36
40
|
},
|
37
41
|
handoff: {
|
38
42
|
desc: {
|
@@ -51,7 +55,7 @@ const oauth = {
|
|
51
55
|
userWelcome: '欢迎回来,',
|
52
56
|
},
|
53
57
|
success: {
|
54
|
-
subTitle: '
|
58
|
+
subTitle: '您已成功授权应用访问您的账户,可以关闭该页面了',
|
55
59
|
title: '授权成功',
|
56
60
|
},
|
57
61
|
};
|
package/src/middleware.ts
CHANGED
@@ -18,9 +18,9 @@ import { OAUTH_AUTHORIZED } from './const/auth';
|
|
18
18
|
import { oidcEnv } from './envs/oidc';
|
19
19
|
|
20
20
|
// Create debug logger instances
|
21
|
-
const logDefault = debug('
|
22
|
-
const logNextAuth = debug('
|
23
|
-
const logClerk = debug('
|
21
|
+
const logDefault = debug('middleware:default');
|
22
|
+
const logNextAuth = debug('middleware:next-auth');
|
23
|
+
const logClerk = debug('middleware:clerk');
|
24
24
|
|
25
25
|
// OIDC session pre-sync constant
|
26
26
|
const OIDC_SESSION_HEADER = 'x-oidc-session-sync';
|
@@ -146,13 +146,19 @@ const defaultMiddleware = (request: NextRequest) => {
|
|
146
146
|
};
|
147
147
|
|
148
148
|
const isPublicRoute = createRouteMatcher([
|
149
|
+
// backend api
|
149
150
|
'/api/auth(.*)',
|
151
|
+
'/api/webhooks(.*)',
|
152
|
+
'/webapi(.*)',
|
150
153
|
'/trpc(.*)',
|
151
154
|
// next auth
|
152
155
|
'/next-auth/(.*)',
|
153
156
|
// clerk
|
154
157
|
'/login',
|
155
158
|
'/signup',
|
159
|
+
// oauth
|
160
|
+
'/oidc/handoff',
|
161
|
+
'/oidc/token',
|
156
162
|
]);
|
157
163
|
|
158
164
|
const isProtectedRoute = createRouteMatcher([
|
@@ -164,7 +170,7 @@ const isProtectedRoute = createRouteMatcher([
|
|
164
170
|
]);
|
165
171
|
|
166
172
|
// Initialize an Edge compatible NextAuth middleware
|
167
|
-
const nextAuthMiddleware = NextAuthEdge.auth((req) => {
|
173
|
+
const nextAuthMiddleware = NextAuthEdge.auth(async (req) => {
|
168
174
|
logNextAuth('NextAuth middleware processing request: %s %s', req.method, req.url);
|
169
175
|
|
170
176
|
const response = defaultMiddleware(req);
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import { TRPCError } from '@trpc/server';
|
2
1
|
import debug from 'debug';
|
3
2
|
|
4
3
|
import { createContextForInteractionDetails } from '@/libs/oidc-provider/http-adapter';
|
@@ -20,76 +19,6 @@ export class OIDCService {
|
|
20
19
|
return new OIDCService(provider);
|
21
20
|
}
|
22
21
|
|
23
|
-
/**
|
24
|
-
* 验证 OIDC Bearer Token 并返回用户信息
|
25
|
-
* 使用 oidc-provider 实例的 AccessToken.find 方法验证 token
|
26
|
-
*
|
27
|
-
* @param token - Bearer Token
|
28
|
-
* @returns 包含用户ID和Token数据的对象
|
29
|
-
* @throws 如果token无效或OIDC未启用则抛出 TRPCError
|
30
|
-
*/
|
31
|
-
async validateToken(token: string) {
|
32
|
-
try {
|
33
|
-
log('Validating access token using AccessToken.find');
|
34
|
-
|
35
|
-
// 使用 oidc-provider 的 AccessToken 查找和验证方法
|
36
|
-
const accessToken = await this.provider.AccessToken.find(token);
|
37
|
-
|
38
|
-
if (!accessToken) {
|
39
|
-
log('Access token not found, expired, or consumed');
|
40
|
-
throw new TRPCError({
|
41
|
-
code: 'UNAUTHORIZED',
|
42
|
-
message: 'Access token 无效、已过期或已被使用',
|
43
|
-
});
|
44
|
-
}
|
45
|
-
|
46
|
-
// 从 accessToken 实例中获取必要的数据
|
47
|
-
// 注意:accessToken 没有 payload() 方法,而是直接访问其属性
|
48
|
-
const userId = accessToken.accountId; // 用户 ID 通常存储在 accountId 属性中
|
49
|
-
const clientId = accessToken.clientId;
|
50
|
-
|
51
|
-
// 如果需要更多的声明信息,可以从 accessToken 的其他属性中获取
|
52
|
-
// 例如,scopes、claims、exp 等
|
53
|
-
const tokenData = {
|
54
|
-
client_id: clientId,
|
55
|
-
exp: accessToken.exp,
|
56
|
-
iat: accessToken.iat,
|
57
|
-
jti: accessToken.jti,
|
58
|
-
scope: accessToken.scope,
|
59
|
-
// OIDC 标准中,sub 字段表示用户 ID
|
60
|
-
sub: userId,
|
61
|
-
};
|
62
|
-
|
63
|
-
if (!userId) {
|
64
|
-
log('Access token does not contain user ID (accountId)');
|
65
|
-
throw new TRPCError({
|
66
|
-
code: 'UNAUTHORIZED',
|
67
|
-
message: 'Access token 中未包含用户 ID',
|
68
|
-
});
|
69
|
-
}
|
70
|
-
|
71
|
-
log('Access token validated successfully for user: %s', userId);
|
72
|
-
return {
|
73
|
-
// 包含 token 原始数据,可用于获取更多信息
|
74
|
-
accessToken,
|
75
|
-
// 构建的 token 数据对象
|
76
|
-
tokenData,
|
77
|
-
// 用户 ID
|
78
|
-
userId,
|
79
|
-
};
|
80
|
-
} catch (error) {
|
81
|
-
if (error instanceof TRPCError) throw error;
|
82
|
-
|
83
|
-
// AccessToken.find 可能抛出特定错误
|
84
|
-
log('Error validating access token with AccessToken.find: %O', error);
|
85
|
-
console.error('OIDC 令牌验证错误:', error);
|
86
|
-
throw new TRPCError({
|
87
|
-
code: 'UNAUTHORIZED',
|
88
|
-
message: `OIDC 认证失败: ${(error as Error).message}`,
|
89
|
-
});
|
90
|
-
}
|
91
|
-
}
|
92
|
-
|
93
22
|
async getInteractionDetails(uid: string) {
|
94
23
|
const { req, res } = await createContextForInteractionDetails(uid);
|
95
24
|
return this.provider.interactionDetails(req, res);
|
package/src/services/chat.ts
CHANGED
@@ -40,6 +40,7 @@ import { ChatImageItem, ChatMessage, MessageToolCall } from '@/types/message';
|
|
40
40
|
import type { ChatStreamPayload, OpenAIChatMessage } from '@/types/openai/chat';
|
41
41
|
import { UserMessageContentPart } from '@/types/openai/chat';
|
42
42
|
import { parsePlaceholderVariablesMessages } from '@/utils/client/parserPlaceholder';
|
43
|
+
import { fetchWithInvokeStream } from '@/utils/electron/desktopRemoteRPCFetch';
|
43
44
|
import { createErrorResponse } from '@/utils/errorResponse';
|
44
45
|
import {
|
45
46
|
FetchSSEOptions,
|
@@ -361,7 +362,10 @@ class ChatService {
|
|
361
362
|
|
362
363
|
let fetcher: typeof fetch | undefined = undefined;
|
363
364
|
|
364
|
-
|
365
|
+
// Add desktop remote RPC fetch support
|
366
|
+
if (isDesktop) {
|
367
|
+
fetcher = fetchWithInvokeStream;
|
368
|
+
} else if (enableFetchOnClient) {
|
365
369
|
/**
|
366
370
|
* Notes:
|
367
371
|
* 1. Browser agent runtime will skip auth check if a key and endpoint provided by
|
@@ -28,13 +28,6 @@ class RemoteServerService {
|
|
28
28
|
requestAuthorization = async (config: DataSyncConfig) => {
|
29
29
|
return dispatch('requestAuthorization', config);
|
30
30
|
};
|
31
|
-
|
32
|
-
/**
|
33
|
-
* 刷新访问令牌
|
34
|
-
*/
|
35
|
-
refreshAccessToken = async () => {
|
36
|
-
return dispatch('refreshAccessToken');
|
37
|
-
};
|
38
31
|
}
|
39
32
|
|
40
33
|
export const remoteServerService = new RemoteServerService();
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { ProxyTRPCRequestParams, dispatch } from '@lobechat/electron-client-ipc';
|
1
|
+
import { ProxyTRPCRequestParams, dispatch, streamInvoke } from '@lobechat/electron-client-ipc';
|
2
2
|
import debug from 'debug';
|
3
3
|
|
4
4
|
import { isDesktop } from '@/const/version';
|
@@ -6,7 +6,7 @@ import { getElectronStoreState } from '@/store/electron';
|
|
6
6
|
import { electronSyncSelectors } from '@/store/electron/selectors';
|
7
7
|
import { getRequestBody, headersToRecord } from '@/utils/fetch';
|
8
8
|
|
9
|
-
const log = debug('
|
9
|
+
const log = debug('utils:desktopRemoteRPCFetch');
|
10
10
|
|
11
11
|
// eslint-disable-next-line no-undef
|
12
12
|
export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) => {
|
@@ -15,8 +15,8 @@ export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) =
|
|
15
15
|
|
16
16
|
if (isSyncActive) {
|
17
17
|
log('Using IPC proxy for tRPC request');
|
18
|
+
const url = input as string;
|
18
19
|
try {
|
19
|
-
const url = input as string;
|
20
20
|
const parsedUrl = new URL(url, window.location.origin);
|
21
21
|
const urlPath = parsedUrl.pathname + parsedUrl.search;
|
22
22
|
const method = init?.method?.toUpperCase() || 'GET';
|
@@ -32,7 +32,7 @@ export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) =
|
|
32
32
|
|
33
33
|
const ipcResult = await dispatch('proxyTRPCRequest', params);
|
34
34
|
|
35
|
-
log(
|
35
|
+
log(`Received ${url} IPC proxy response:`, { status: ipcResult.status });
|
36
36
|
const response = new Response(ipcResult.body, {
|
37
37
|
headers: ipcResult.headers,
|
38
38
|
status: ipcResult.status,
|
@@ -41,7 +41,7 @@ export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) =
|
|
41
41
|
|
42
42
|
if (!response.ok) {
|
43
43
|
console.warn(
|
44
|
-
|
44
|
+
`[lambda] ${url} IPC proxy response indicates an error:`,
|
45
45
|
response.status,
|
46
46
|
response.statusText,
|
47
47
|
);
|
@@ -49,7 +49,7 @@ export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) =
|
|
49
49
|
|
50
50
|
return response;
|
51
51
|
} catch (error) {
|
52
|
-
console.error(
|
52
|
+
console.error(`[lambda] Error during ${url} IPC proxy call:`, error);
|
53
53
|
return new Response(
|
54
54
|
`IPC Proxy Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
55
55
|
{
|
@@ -62,11 +62,26 @@ export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) =
|
|
62
62
|
};
|
63
63
|
|
64
64
|
// eslint-disable-next-line no-undef
|
65
|
-
export const fetchWithDesktopRemoteRPC = async (input:
|
65
|
+
export const fetchWithDesktopRemoteRPC = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
66
66
|
if (isDesktop) {
|
67
67
|
const res = await desktopRemoteRPCFetch(input as string, init);
|
68
68
|
if (res) return res;
|
69
69
|
}
|
70
70
|
|
71
|
+
return fetch(input, init);
|
72
|
+
}) as typeof fetch;
|
73
|
+
|
74
|
+
// eslint-disable-next-line no-undef
|
75
|
+
export const fetchWithInvokeStream = async (input: RequestInfo | URL, init?: RequestInit) => {
|
76
|
+
if (isDesktop) {
|
77
|
+
const isSyncActive = electronSyncSelectors.isSyncActive(getElectronStoreState());
|
78
|
+
log('isSyncActive:', isSyncActive);
|
79
|
+
if (isSyncActive) {
|
80
|
+
log('Using IPC stream proxy for request to:', input);
|
81
|
+
|
82
|
+
return streamInvoke(input, init);
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
71
86
|
return fetch(input, init);
|
72
87
|
};
|
package/src/utils/server/auth.ts
CHANGED
@@ -49,3 +49,25 @@ export const extractBearerToken = (authHeader?: string | null): string | null =>
|
|
49
49
|
// Return the token only if it's not an empty string after trimming
|
50
50
|
return token || null;
|
51
51
|
};
|
52
|
+
|
53
|
+
/**
|
54
|
+
* 从 Oidc-Auth header 中提取 JWT token
|
55
|
+
* @param authHeader - Oidc-Auth header 值 (例如 "Oidc-Auth xxx")
|
56
|
+
* @returns JWT token 或 null(如果授权头无效或不存在)
|
57
|
+
*/
|
58
|
+
export const extractOidcAuthToken = (authHeader?: string | null): string | null => {
|
59
|
+
if (!authHeader) return null;
|
60
|
+
|
61
|
+
const trimmedHeader = authHeader.trim(); // Trim leading/trailing spaces
|
62
|
+
|
63
|
+
// Check if it starts with 'Oidc-Auth ' (case-insensitive check)
|
64
|
+
if (!trimmedHeader.toLowerCase().startsWith('oidc-auth ')) {
|
65
|
+
return null;
|
66
|
+
}
|
67
|
+
|
68
|
+
// Extract the token part after "Oidc-Auth " and trim potential spaces around the token itself
|
69
|
+
const token = trimmedHeader.slice(10).trim(); // 'Oidc-Auth ' length is 10
|
70
|
+
|
71
|
+
// Return the token only if it's not an empty string after trimming
|
72
|
+
return token || null;
|
73
|
+
};
|
@@ -1,36 +0,0 @@
|
|
1
|
-
'use client';
|
2
|
-
|
3
|
-
import { Button, Icon } from '@lobehub/ui';
|
4
|
-
import { Card, Result } from 'antd';
|
5
|
-
import { XCircle } from 'lucide-react';
|
6
|
-
import Link from 'next/link';
|
7
|
-
import React, { memo } from 'react';
|
8
|
-
import { useTranslation } from 'react-i18next';
|
9
|
-
import { Center } from 'react-layout-kit';
|
10
|
-
|
11
|
-
const FailedPage = memo(() => {
|
12
|
-
const { t } = useTranslation('oauth');
|
13
|
-
|
14
|
-
return (
|
15
|
-
<Center height="100vh">
|
16
|
-
<Card style={{ maxWidth: 500, width: '100%' }}>
|
17
|
-
<Result
|
18
|
-
extra={
|
19
|
-
<Link href="/">
|
20
|
-
<Button type="primary">{t('failed.backToHome')}</Button>
|
21
|
-
</Link>
|
22
|
-
}
|
23
|
-
icon={<Icon icon={XCircle} />}
|
24
|
-
status="error"
|
25
|
-
style={{ padding: 0 }}
|
26
|
-
subTitle={t('failed.subTitle')}
|
27
|
-
title={t('failed.title')}
|
28
|
-
/>
|
29
|
-
</Card>
|
30
|
-
</Center>
|
31
|
-
);
|
32
|
-
});
|
33
|
-
|
34
|
-
FailedPage.displayName = 'FailedPage';
|
35
|
-
|
36
|
-
export default FailedPage;
|