@lobehub/chat 1.80.1 → 1.80.3
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/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/next.config.ts +5 -1
- package/package.json +1 -1
- package/src/app/[variants]/oauth/consent/[uid]/Client.tsx +36 -23
- package/src/app/[variants]/oauth/consent/[uid]/page.tsx +2 -0
- package/src/config/aiModels/azure.ts +79 -1
- package/src/config/aiModels/azureai.ts +181 -0
- package/src/config/aiModels/google.ts +36 -2
- package/src/config/aiModels/groq.ts +31 -3
- package/src/config/aiModels/hunyuan.ts +54 -18
- package/src/config/aiModels/moonshot.ts +17 -17
- package/src/config/aiModels/novita.ts +25 -30
- package/src/config/aiModels/siliconcloud.ts +80 -2
- package/src/config/aiModels/stepfun.ts +40 -31
- package/src/config/aiModels/tencentcloud.ts +7 -6
- package/src/config/aiModels/volcengine.ts +1 -0
- package/src/config/aiModels/zhipu.ts +91 -27
- package/src/const/settings/knowledge.ts +2 -2
- package/src/database/models/user.ts +13 -1
- package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +18 -11
- package/src/libs/oidc-provider/adapter.ts +5 -6
- package/src/libs/oidc-provider/config.ts +0 -3
- package/src/libs/oidc-provider/provider.ts +1 -0
- package/src/libs/trpc/edge/index.ts +0 -4
- package/src/libs/trpc/lambda/context.ts +90 -6
- package/src/libs/trpc/lambda/index.ts +2 -1
- package/src/libs/trpc/lambda/middleware/oidcAuth.ts +14 -0
- package/src/libs/trpc/middleware/userAuth.ts +2 -4
- package/src/server/routers/lambda/user.ts +9 -2
- package/src/server/services/oidc/index.ts +71 -0
- package/src/services/user/client.ts +5 -2
- package/src/store/user/slices/common/action.ts +9 -2
- package/src/types/user/index.ts +6 -1
- package/src/utils/parseModels.test.ts +19 -3
- package/src/utils/server/__tests__/auth.test.ts +45 -1
- package/src/utils/server/auth.ts +26 -2
@@ -1,3 +1,4 @@
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
1
2
|
import debug from 'debug';
|
2
3
|
|
3
4
|
import { createContextForInteractionDetails } from '@/libs/oidc-provider/http-adapter';
|
@@ -19,6 +20,76 @@ export class OIDCService {
|
|
19
20
|
return new OIDCService(provider);
|
20
21
|
}
|
21
22
|
|
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
|
+
|
22
93
|
async getInteractionDetails(uid: string) {
|
23
94
|
const { req, res } = await createContextForInteractionDetails(uid);
|
24
95
|
return this.provider.interactionDetails(req, res);
|
@@ -39,18 +39,21 @@ export class ClientService extends BaseClientService implements IUserService {
|
|
39
39
|
encryptKeyVaultsStr ? JSON.parse(encryptKeyVaultsStr) : {},
|
40
40
|
);
|
41
41
|
|
42
|
-
const user = await UserModel.findById(clientDB as any, this.userId);
|
43
42
|
const messageCount = await this.messageModel.count();
|
44
43
|
const sessionCount = await this.sessionModel.count();
|
45
44
|
|
46
45
|
return {
|
47
46
|
...state,
|
48
|
-
avatar:
|
47
|
+
avatar: state.avatar ?? '',
|
49
48
|
canEnablePWAGuide: messageCount >= 4,
|
50
49
|
canEnableTrace: messageCount >= 4,
|
50
|
+
firstName: state.firstName,
|
51
|
+
fullName: state.fullName,
|
51
52
|
hasConversation: messageCount > 0 || sessionCount > 0,
|
52
53
|
isOnboard: true,
|
54
|
+
lastName: state.lastName,
|
53
55
|
preference: await this.preferenceStorage.getFromLocalStorage(),
|
56
|
+
username: state.username,
|
54
57
|
};
|
55
58
|
};
|
56
59
|
|
@@ -7,7 +7,7 @@ import { useOnlyFetchOnceSWR } from '@/libs/swr';
|
|
7
7
|
import { userService } from '@/services/user';
|
8
8
|
import type { UserStore } from '@/store/user';
|
9
9
|
import type { GlobalServerConfig } from '@/types/serverConfig';
|
10
|
-
import { UserInitializationState } from '@/types/user';
|
10
|
+
import { LobeUser, UserInitializationState } from '@/types/user';
|
11
11
|
import type { UserSettings } from '@/types/user/settings';
|
12
12
|
import { merge } from '@/utils/merge';
|
13
13
|
import { setNamespace } from '@/utils/storeDebug';
|
@@ -91,7 +91,14 @@ export const createCommonSlice: StateCreator<
|
|
91
91
|
// if there is avatar or userId (from client DB), update it into user
|
92
92
|
const user =
|
93
93
|
data.avatar || data.userId
|
94
|
-
? merge(get().user, {
|
94
|
+
? merge(get().user, {
|
95
|
+
avatar: data.avatar,
|
96
|
+
firstName: data.firstName,
|
97
|
+
fullName: data.fullName,
|
98
|
+
id: data.userId,
|
99
|
+
latestName: data.lastName,
|
100
|
+
username: data.username,
|
101
|
+
} as LobeUser)
|
95
102
|
: get().user;
|
96
103
|
|
97
104
|
set(
|
package/src/types/user/index.ts
CHANGED
@@ -48,14 +48,19 @@ export interface UserInitializationState {
|
|
48
48
|
avatar?: string;
|
49
49
|
canEnablePWAGuide?: boolean;
|
50
50
|
canEnableTrace?: boolean;
|
51
|
+
email?: string;
|
52
|
+
firstName?: string;
|
53
|
+
fullName?: string;
|
51
54
|
hasConversation?: boolean;
|
52
55
|
isOnboard?: boolean;
|
56
|
+
lastName?: string;
|
53
57
|
preference: UserPreference;
|
54
58
|
settings: DeepPartial<UserSettings>;
|
55
59
|
userId?: string;
|
60
|
+
username?: string;
|
56
61
|
}
|
57
62
|
|
58
63
|
export const NextAuthAccountSchame = z.object({
|
59
64
|
provider: z.string(),
|
60
65
|
providerAccountId: z.string(),
|
61
|
-
});
|
66
|
+
});
|
@@ -87,7 +87,9 @@ describe('parseModelString', () => {
|
|
87
87
|
});
|
88
88
|
|
89
89
|
it('token and image output', () => {
|
90
|
-
const result = parseModelString(
|
90
|
+
const result = parseModelString(
|
91
|
+
'gemini-2.0-flash-exp-image-generation=Gemini 2.0 Flash (Image Generation) Experimental<32768:imageOutput>',
|
92
|
+
);
|
91
93
|
|
92
94
|
expect(result.add[0]).toEqual({
|
93
95
|
displayName: 'Gemini 2.0 Flash (Image Generation) Experimental',
|
@@ -565,7 +567,12 @@ describe('transformToChatModelCards', () => {
|
|
565
567
|
displayName: 'GPT-4o',
|
566
568
|
enabled: true,
|
567
569
|
id: 'gpt-4o',
|
568
|
-
|
570
|
+
maxOutput: 4096,
|
571
|
+
pricing: {
|
572
|
+
cachedInput: 1.25,
|
573
|
+
input: 2.5,
|
574
|
+
output: 10,
|
575
|
+
},
|
569
576
|
providerId: 'azure',
|
570
577
|
releasedAt: '2024-05-13',
|
571
578
|
source: 'builtin',
|
@@ -582,6 +589,11 @@ describe('transformToChatModelCards', () => {
|
|
582
589
|
enabled: true,
|
583
590
|
id: 'gpt-4o-mini',
|
584
591
|
maxOutput: 4096,
|
592
|
+
pricing: {
|
593
|
+
cachedInput: 0.075,
|
594
|
+
input: 0.15,
|
595
|
+
output: 0.6,
|
596
|
+
},
|
585
597
|
type: 'chat',
|
586
598
|
},
|
587
599
|
{
|
@@ -596,7 +608,11 @@ describe('transformToChatModelCards', () => {
|
|
596
608
|
source: 'builtin',
|
597
609
|
id: 'o1-mini',
|
598
610
|
maxOutput: 65536,
|
599
|
-
pricing: {
|
611
|
+
pricing: {
|
612
|
+
cachedInput: 0.55,
|
613
|
+
input: 1.1,
|
614
|
+
output: 4.4,
|
615
|
+
},
|
600
616
|
releasedAt: '2024-09-12',
|
601
617
|
type: 'chat',
|
602
618
|
},
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
2
2
|
|
3
|
-
import { getUserAuth } from '../auth';
|
3
|
+
import { extractBearerToken, getUserAuth } from '../auth';
|
4
4
|
|
5
5
|
// Mock auth constants
|
6
6
|
let mockEnableClerk = false;
|
@@ -93,3 +93,47 @@ describe('getUserAuth', () => {
|
|
93
93
|
});
|
94
94
|
});
|
95
95
|
});
|
96
|
+
|
97
|
+
describe('extractBearerToken', () => {
|
98
|
+
it('should return the token when authHeader is valid', () => {
|
99
|
+
const token = 'test-token';
|
100
|
+
const authHeader = `Bearer ${token}`;
|
101
|
+
expect(extractBearerToken(authHeader)).toBe(token);
|
102
|
+
});
|
103
|
+
|
104
|
+
it('should return null when authHeader is missing', () => {
|
105
|
+
expect(extractBearerToken()).toBeNull();
|
106
|
+
});
|
107
|
+
|
108
|
+
it('should return null when authHeader is null', () => {
|
109
|
+
expect(extractBearerToken(null)).toBeNull();
|
110
|
+
});
|
111
|
+
|
112
|
+
it('should return null when authHeader does not start with "Bearer "', () => {
|
113
|
+
const authHeader = 'Invalid format';
|
114
|
+
expect(extractBearerToken(authHeader)).toBeNull();
|
115
|
+
});
|
116
|
+
|
117
|
+
it('should return null when authHeader is only "Bearer"', () => {
|
118
|
+
const authHeader = 'Bearer';
|
119
|
+
expect(extractBearerToken(authHeader)).toBeNull();
|
120
|
+
});
|
121
|
+
|
122
|
+
it('should return null when authHeader is an empty string', () => {
|
123
|
+
const authHeader = '';
|
124
|
+
expect(extractBearerToken(authHeader)).toBeNull();
|
125
|
+
});
|
126
|
+
|
127
|
+
it('should handle extra spaces correctly', () => {
|
128
|
+
const token = 'test-token-with-spaces';
|
129
|
+
const authHeaderWithExtraSpaces = ` Bearer ${token} `;
|
130
|
+
const authHeaderLeadingSpace = ` Bearer ${token}`;
|
131
|
+
const authHeaderTrailingSpace = `Bearer ${token} `;
|
132
|
+
const authHeaderMultipleSpacesBetween = `Bearer ${token}`;
|
133
|
+
|
134
|
+
expect(extractBearerToken(authHeaderWithExtraSpaces)).toBe(token);
|
135
|
+
expect(extractBearerToken(authHeaderLeadingSpace)).toBe(token);
|
136
|
+
expect(extractBearerToken(authHeaderTrailingSpace)).toBe(token);
|
137
|
+
expect(extractBearerToken(authHeaderMultipleSpacesBetween)).toBe(token);
|
138
|
+
});
|
139
|
+
});
|
package/src/utils/server/auth.ts
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
import { enableClerk, enableNextAuth } from '@/const/auth';
|
2
|
-
import { ClerkAuth } from '@/libs/clerk-auth';
|
3
|
-
import NextAuthEdge from '@/libs/next-auth/edge';
|
4
2
|
|
5
3
|
export const getUserAuth = async () => {
|
6
4
|
if (enableClerk) {
|
5
|
+
const { ClerkAuth } = await import('@/libs/clerk-auth');
|
6
|
+
|
7
7
|
const clerkAuth = new ClerkAuth();
|
8
8
|
|
9
9
|
return await clerkAuth.getAuth();
|
10
10
|
}
|
11
11
|
|
12
12
|
if (enableNextAuth) {
|
13
|
+
const { default: NextAuthEdge } = await import('@/libs/next-auth/edge');
|
14
|
+
|
13
15
|
const session = await NextAuthEdge.auth();
|
14
16
|
|
15
17
|
const userId = session?.user.id;
|
@@ -19,3 +21,25 @@ export const getUserAuth = async () => {
|
|
19
21
|
|
20
22
|
throw new Error('Auth method is not enabled');
|
21
23
|
};
|
24
|
+
|
25
|
+
/**
|
26
|
+
* 从授权头中提取 Bearer Token
|
27
|
+
* @param authHeader - 授权头 (例如 "Bearer xxx")
|
28
|
+
* @returns Bearer Token 或 null(如果授权头无效或不存在)
|
29
|
+
*/
|
30
|
+
export const extractBearerToken = (authHeader?: string | null): string | null => {
|
31
|
+
if (!authHeader) return null;
|
32
|
+
|
33
|
+
const trimmedHeader = authHeader.trim(); // Trim leading/trailing spaces
|
34
|
+
|
35
|
+
// Check if it starts with 'Bearer ' (case-insensitive check might be desired depending on spec)
|
36
|
+
if (!trimmedHeader.toLowerCase().startsWith('bearer ')) {
|
37
|
+
return null;
|
38
|
+
}
|
39
|
+
|
40
|
+
// Extract the token part after "Bearer " and trim potential spaces around the token itself
|
41
|
+
const token = trimmedHeader.slice(7).trim();
|
42
|
+
|
43
|
+
// Return the token only if it's not an empty string after trimming
|
44
|
+
return token || null;
|
45
|
+
};
|