@lobehub/chat 0.153.1 → 0.154.1

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 (143) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/docs/self-hosting/advanced/authentication.mdx +25 -18
  3. package/docs/self-hosting/advanced/authentication.zh-CN.mdx +26 -17
  4. package/docs/self-hosting/advanced/model-list.mdx +2 -2
  5. package/docs/self-hosting/advanced/model-list.zh-CN.mdx +2 -2
  6. package/docs/self-hosting/environment-variables/auth.mdx +48 -39
  7. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +49 -43
  8. package/locales/ar/auth.json +6 -0
  9. package/locales/ar/clerk.json +769 -0
  10. package/locales/ar/common.json +1 -0
  11. package/locales/ar/error.json +8 -0
  12. package/locales/bg-BG/auth.json +6 -0
  13. package/locales/bg-BG/clerk.json +769 -0
  14. package/locales/bg-BG/common.json +1 -0
  15. package/locales/bg-BG/error.json +8 -0
  16. package/locales/de-DE/auth.json +6 -0
  17. package/locales/de-DE/clerk.json +769 -0
  18. package/locales/de-DE/common.json +1 -0
  19. package/locales/de-DE/error.json +8 -0
  20. package/locales/en-US/auth.json +6 -0
  21. package/locales/en-US/clerk.json +769 -0
  22. package/locales/en-US/common.json +1 -0
  23. package/locales/en-US/error.json +8 -0
  24. package/locales/es-ES/auth.json +6 -0
  25. package/locales/es-ES/clerk.json +769 -0
  26. package/locales/es-ES/common.json +1 -0
  27. package/locales/es-ES/error.json +8 -0
  28. package/locales/fr-FR/auth.json +6 -0
  29. package/locales/fr-FR/clerk.json +769 -0
  30. package/locales/fr-FR/common.json +1 -0
  31. package/locales/fr-FR/error.json +8 -0
  32. package/locales/it-IT/auth.json +6 -0
  33. package/locales/it-IT/clerk.json +769 -0
  34. package/locales/it-IT/common.json +1 -0
  35. package/locales/it-IT/error.json +8 -0
  36. package/locales/ja-JP/auth.json +6 -0
  37. package/locales/ja-JP/clerk.json +769 -0
  38. package/locales/ja-JP/common.json +1 -0
  39. package/locales/ja-JP/error.json +8 -0
  40. package/locales/ko-KR/auth.json +6 -0
  41. package/locales/ko-KR/clerk.json +769 -0
  42. package/locales/ko-KR/common.json +1 -0
  43. package/locales/ko-KR/error.json +8 -0
  44. package/locales/nl-NL/auth.json +6 -0
  45. package/locales/nl-NL/clerk.json +769 -0
  46. package/locales/nl-NL/common.json +1 -0
  47. package/locales/nl-NL/error.json +8 -0
  48. package/locales/pl-PL/auth.json +6 -0
  49. package/locales/pl-PL/clerk.json +769 -0
  50. package/locales/pl-PL/common.json +1 -0
  51. package/locales/pl-PL/error.json +8 -0
  52. package/locales/pt-BR/auth.json +6 -0
  53. package/locales/pt-BR/clerk.json +769 -0
  54. package/locales/pt-BR/common.json +1 -0
  55. package/locales/pt-BR/error.json +8 -0
  56. package/locales/ru-RU/auth.json +6 -0
  57. package/locales/ru-RU/clerk.json +769 -0
  58. package/locales/ru-RU/common.json +1 -0
  59. package/locales/ru-RU/error.json +8 -0
  60. package/locales/tr-TR/auth.json +6 -0
  61. package/locales/tr-TR/clerk.json +769 -0
  62. package/locales/tr-TR/common.json +1 -0
  63. package/locales/tr-TR/error.json +8 -0
  64. package/locales/vi-VN/auth.json +6 -0
  65. package/locales/vi-VN/clerk.json +769 -0
  66. package/locales/vi-VN/common.json +1 -0
  67. package/locales/vi-VN/error.json +8 -0
  68. package/locales/zh-CN/auth.json +6 -0
  69. package/locales/zh-CN/clerk.json +769 -0
  70. package/locales/zh-CN/common.json +1 -0
  71. package/locales/zh-CN/error.json +8 -0
  72. package/locales/zh-TW/auth.json +6 -0
  73. package/locales/zh-TW/clerk.json +769 -0
  74. package/locales/zh-TW/common.json +1 -0
  75. package/locales/zh-TW/error.json +8 -0
  76. package/package.json +10 -5
  77. package/src/app/(auth)/layout.tsx +19 -0
  78. package/src/app/(auth)/login/[[...login]]/PageTitle.tsx +13 -0
  79. package/src/app/(auth)/login/[[...login]]/page.tsx +14 -0
  80. package/src/app/(auth)/profile/[[...slugs]]/PageTitle.tsx +13 -0
  81. package/src/app/(auth)/profile/[[...slugs]]/page.tsx +14 -0
  82. package/src/app/(auth)/signup/[[...signup]]/PageTitle.tsx +13 -0
  83. package/src/app/(auth)/signup/[[...signup]]/page.tsx +14 -0
  84. package/src/app/(main)/settings/common/features/Common.tsx +1 -1
  85. package/src/app/(main)/settings/common/features/Theme/index.tsx +2 -0
  86. package/src/app/(main)/settings/common/index.tsx +7 -3
  87. package/src/app/api/chat/[provider]/route.test.ts +76 -3
  88. package/src/app/api/chat/auth/index.test.ts +77 -0
  89. package/src/app/api/chat/auth/index.ts +19 -3
  90. package/src/app/api/chat/auth/utils.ts +31 -9
  91. package/src/app/api/plugin/gateway/route.ts +3 -3
  92. package/src/config/auth.ts +146 -0
  93. package/src/config/server/index.ts +1 -3
  94. package/src/const/auth.ts +7 -0
  95. package/src/features/AgentSetting/AgentMeta/AutoGenerateInput.tsx +1 -2
  96. package/src/features/AgentSetting/AgentMeta/AutoGenerateSelect.tsx +1 -1
  97. package/src/features/AgentSetting/AgentMeta/index.tsx +7 -2
  98. package/src/features/Conversation/Error/ClerkLogin/index.tsx +47 -0
  99. package/src/features/Conversation/Error/index.tsx +5 -0
  100. package/src/features/Conversation/components/InboxWelcome/index.tsx +4 -17
  101. package/src/features/DataImporter/index.tsx +2 -0
  102. package/src/features/User/UserAvatar.tsx +11 -5
  103. package/src/features/User/UserInfo.tsx +8 -7
  104. package/src/features/User/UserLoginOrSignup.tsx +23 -0
  105. package/src/features/User/UserPanel/PanelContent.tsx +74 -0
  106. package/src/features/User/UserPanel/UpgradeBadge.tsx +19 -0
  107. package/src/features/User/UserPanel/index.tsx +8 -27
  108. package/src/features/User/UserPanel/useMenu.tsx +49 -21
  109. package/src/features/User/__tests__/PanelContent.test.tsx +151 -0
  110. package/src/features/User/__tests__/UserAvatar.test.tsx +78 -0
  111. package/src/hooks/useGreeting/greetingTime.ts +14 -0
  112. package/src/hooks/useGreeting/index.ts +16 -0
  113. package/src/hooks/useTokenCount.ts +1 -0
  114. package/src/layout/AuthProvider/Clerk/UserUpdater.tsx +41 -0
  115. package/src/layout/AuthProvider/Clerk/index.tsx +26 -0
  116. package/src/layout/AuthProvider/Clerk/useAppearance.ts +118 -0
  117. package/src/layout/AuthProvider/index.tsx +8 -4
  118. package/src/layout/GlobalProvider/AppTheme.tsx +1 -1
  119. package/src/libs/next-auth/index.ts +4 -6
  120. package/src/libs/next-auth/sso-providers/auth0.ts +4 -6
  121. package/src/libs/next-auth/sso-providers/authentik.ts +4 -6
  122. package/src/libs/next-auth/sso-providers/azure-ad.ts +4 -6
  123. package/src/libs/next-auth/sso-providers/github.ts +3 -5
  124. package/src/libs/next-auth/sso-providers/zitadel.ts +4 -6
  125. package/src/locales/default/auth.ts +6 -0
  126. package/src/locales/default/clerk.ts +782 -0
  127. package/src/locales/default/common.ts +3 -0
  128. package/src/locales/default/error.ts +8 -0
  129. package/src/locales/default/index.ts +4 -0
  130. package/src/middleware.ts +19 -6
  131. package/src/server/globalConfig/index.ts +2 -2
  132. package/src/store/user/selectors.ts +1 -1
  133. package/src/store/user/slices/auth/action.test.ts +105 -0
  134. package/src/store/user/slices/auth/action.ts +40 -5
  135. package/src/store/user/slices/auth/initialState.ts +15 -0
  136. package/src/store/user/slices/auth/selectors.test.ts +127 -0
  137. package/src/store/user/slices/auth/selectors.ts +38 -1
  138. package/src/store/user/slices/settings/selectors/selectors.test.ts +25 -0
  139. package/src/store/user/slices/settings/selectors/settings.ts +6 -0
  140. package/src/styles/antdOverride.ts +5 -0
  141. package/src/types/fetch.ts +1 -0
  142. package/src/config/server/auth.ts +0 -71
  143. package/src/features/User/UserPanel/Popover.tsx +0 -35
@@ -160,6 +160,7 @@
160
160
  "newVersion": "有新版本可用:{{version}}"
161
161
  },
162
162
  "userPanel": {
163
+ "anonymousNickName": "匿名使用者",
163
164
  "billing": "帳單管理",
164
165
  "defaultNickname": "社群版使用者",
165
166
  "discord": "社區支援",
@@ -1,4 +1,11 @@
1
1
  {
2
+ "clerkAuth": {
3
+ "loginSuccess": {
4
+ "action": "繼續會話",
5
+ "desc": "{{greeting}},很高興能夠繼續為你服務。讓我們接著剛剛的話題聊下去吧",
6
+ "title": "歡迎回來, {{nickName}}"
7
+ }
8
+ },
2
9
  "error": {
3
10
  "backHome": "返回首頁",
4
11
  "desc": "待會再試試,或者回到已知的世界",
@@ -54,6 +61,7 @@
54
61
  "InvalidAnthropicAPIKey": "Anthropic API 金鑰不正確或為空,請檢查 Anthropic API 金鑰後重試",
55
62
  "InvalidAzureAPIKey": "Azure API Key 不正確或為空,請檢查 Azure API Key 後重試",
56
63
  "InvalidBedrockCredentials": "Bedrock 驗證未通過,請檢查 AccessKeyId/SecretAccessKey 後重試",
64
+ "InvalidClerkUser": "很抱歉,你當前尚未登錄,請先登錄或註冊帳號後繼續操作",
57
65
  "InvalidGoogleAPIKey": "Google API Key 不正確或為空,請檢查 Google API Key 後重試",
58
66
  "InvalidGroqAPIKey": "Groq API 金鑰不正確或為空,請檢查 Groq API 金鑰後重試",
59
67
  "InvalidMinimaxAPIKey": "Minimax API 金鑰不正確或為空,請檢查 Minimax API 金鑰後重試",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.153.1",
3
+ "version": "0.154.1",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -32,6 +32,7 @@
32
32
  "build:analyze": "ANALYZE=true next build",
33
33
  "build:docker": "DOCKER=true next build && npm run build-sitemap",
34
34
  "dev": "next dev -p 3010",
35
+ "dev:clerk-proxy": "ngrok http http://localhost:3011",
35
36
  "docs:i18n": "lobe-i18n md && npm run workflow:docs && npm run lint:mdx",
36
37
  "docs:seo": "lobe-seo && npm run workflow:mdx && npm run lint:mdx",
37
38
  "i18n": "npm run workflow:i18n && lobe-i18n",
@@ -84,9 +85,13 @@
84
85
  "@anthropic-ai/sdk": "^0.18.0",
85
86
  "@auth/core": "0.28.0",
86
87
  "@aws-sdk/client-bedrock-runtime": "^3.565.0",
87
- "@azure/openai": "1.0.0-beta.12",
88
+ "@azure/openai": "^1.0.0-beta.12",
88
89
  "@cfworker/json-schema": "^1.12.8",
89
- "@google/generative-ai": "^0.8.0",
90
+ "@clerk/backend": "^1.1.1",
91
+ "@clerk/localizations": "2.0.0",
92
+ "@clerk/nextjs": "^5.0.6",
93
+ "@clerk/themes": "^2.0.0",
94
+ "@google/generative-ai": "^0.10.0",
90
95
  "@icons-pack/react-simple-icons": "^9.4.1",
91
96
  "@lobehub/chat-plugin-sdk": "latest",
92
97
  "@lobehub/chat-plugins-gateway": "latest",
@@ -179,8 +184,8 @@
179
184
  "@next/bundle-analyzer": "^14.2.3",
180
185
  "@next/eslint-plugin-next": "^14.2.3",
181
186
  "@peculiar/webcrypto": "^1.4.6",
182
- "@testing-library/jest-dom": "6.4.2",
183
- "@testing-library/react": "^15.0.5",
187
+ "@testing-library/jest-dom": "^6.4.5",
188
+ "@testing-library/react": "^15.0.6",
184
189
  "@types/chroma-js": "^2.4.4",
185
190
  "@types/debug": "^4.1.12",
186
191
  "@types/diff": "^5.2.0",
@@ -0,0 +1,19 @@
1
+ import { notFound } from 'next/navigation';
2
+ import { PropsWithChildren } from 'react';
3
+ import { Center, Flexbox } from 'react-layout-kit';
4
+
5
+ import { enableClerk } from '@/const/auth';
6
+
7
+ const Page = ({ children }: PropsWithChildren) => {
8
+ if (!enableClerk) return notFound();
9
+
10
+ return (
11
+ <Flexbox height={'100%'} width={'100%'}>
12
+ <Center height={'100%'} width={'100%'}>
13
+ {children}
14
+ </Center>
15
+ </Flexbox>
16
+ );
17
+ };
18
+
19
+ export default Page;
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import PageTitle from '@/components/PageTitle';
7
+
8
+ const Title = memo(() => {
9
+ const { t } = useTranslation('auth');
10
+
11
+ return <PageTitle title={t('login')} />;
12
+ });
13
+ export default Title;
@@ -0,0 +1,14 @@
1
+ import { SignIn } from '@clerk/nextjs';
2
+
3
+ import PageTitle from './PageTitle';
4
+
5
+ const Page = () => {
6
+ return (
7
+ <>
8
+ <PageTitle />
9
+ <SignIn path="/login" />
10
+ </>
11
+ );
12
+ };
13
+
14
+ export default Page;
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import PageTitle from '@/components/PageTitle';
7
+
8
+ const Title = memo(() => {
9
+ const { t } = useTranslation('auth');
10
+
11
+ return <PageTitle title={t('signup')} />;
12
+ });
13
+ export default Title;
@@ -0,0 +1,14 @@
1
+ import { UserProfile } from '@clerk/nextjs';
2
+
3
+ import PageTitle from './PageTitle';
4
+
5
+ const Page = () => {
6
+ return (
7
+ <>
8
+ <PageTitle />
9
+ <UserProfile />
10
+ </>
11
+ );
12
+ };
13
+
14
+ export default Page;
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import PageTitle from '@/components/PageTitle';
7
+
8
+ const Title = memo(() => {
9
+ const { t } = useTranslation('auth');
10
+
11
+ return <PageTitle title={t('signup')} />;
12
+ });
13
+ export default Title;
@@ -0,0 +1,14 @@
1
+ import { SignUp } from '@clerk/nextjs';
2
+
3
+ import PageTitle from './PageTitle';
4
+
5
+ const Page = () => {
6
+ return (
7
+ <>
8
+ <PageTitle />
9
+ <SignUp path="/signup" />
10
+ </>
11
+ );
12
+ };
13
+
14
+ export default Page;
@@ -21,7 +21,7 @@ type SettingItemGroup = ItemGroup;
21
21
 
22
22
  export interface SettingsCommonProps {
23
23
  showAccessCodeConfig: boolean;
24
- showOAuthLogin: boolean;
24
+ showOAuthLogin?: boolean;
25
25
  }
26
26
 
27
27
  const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig, showOAuthLogin }) => {
@@ -8,6 +8,7 @@ import { memo } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
9
 
10
10
  import { useSyncSettings } from '@/app/(main)/settings/hooks/useSyncSettings';
11
+ import { enableAuth } from '@/const/auth';
11
12
  import { FORM_STYLE } from '@/const/layoutTokens';
12
13
  import { imageUrl } from '@/const/url';
13
14
  import AvatarWithUpload from '@/features/AvatarWithUpload';
@@ -32,6 +33,7 @@ const Theme = memo(() => {
32
33
  children: [
33
34
  {
34
35
  children: <AvatarWithUpload />,
36
+ hidden: enableAuth,
35
37
  label: t('settingTheme.avatar.title'),
36
38
  minWidth: undefined,
37
39
  },
@@ -1,15 +1,19 @@
1
+ import { authEnv } from '@/config/auth';
1
2
  import { getServerConfig } from '@/config/server';
2
3
 
3
4
  import Common from './features/Common';
4
5
  import Theme from './features/Theme';
5
6
 
6
- const { SHOW_ACCESS_CODE_CONFIG, ENABLE_OAUTH_SSO } = getServerConfig();
7
-
8
7
  const Page = () => {
8
+ const { SHOW_ACCESS_CODE_CONFIG } = getServerConfig();
9
+
9
10
  return (
10
11
  <>
11
12
  <Theme />
12
- <Common showAccessCodeConfig={SHOW_ACCESS_CODE_CONFIG} showOAuthLogin={ENABLE_OAUTH_SSO} />
13
+ <Common
14
+ showAccessCodeConfig={SHOW_ACCESS_CODE_CONFIG}
15
+ showOAuthLogin={authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH}
16
+ />
13
17
  </>
14
18
  );
15
19
  };
@@ -1,18 +1,37 @@
1
1
  // @vitest-environment edge-runtime
2
+ import { getAuth } from '@clerk/nextjs/server';
2
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
4
 
4
5
  import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/const/auth';
5
6
  import { AgentRuntime, LobeRuntimeAI } from '@/libs/agent-runtime';
6
7
  import { ChatErrorType } from '@/types/fetch';
7
8
 
8
- import { getJWTPayload } from '../auth/utils';
9
+ import { checkAuthMethod, getJWTPayload } from '../auth/utils';
9
10
  import { POST } from './route';
10
11
 
12
+ vi.mock('@clerk/nextjs/server', () => ({
13
+ getAuth: vi.fn(),
14
+ }));
15
+
11
16
  vi.mock('../auth/utils', () => ({
12
17
  getJWTPayload: vi.fn(),
13
18
  checkAuthMethod: vi.fn(),
14
19
  }));
15
20
 
21
+ // 定义一个变量来存储 enableAuth 的值
22
+ let enableClerk = false;
23
+
24
+ // 模拟 @/const/auth 模块
25
+ vi.mock('@/const/auth', async (importOriginal) => {
26
+ const modules = await importOriginal();
27
+ return {
28
+ ...(modules as any),
29
+ get enableClerk() {
30
+ return enableClerk;
31
+ },
32
+ };
33
+ });
34
+
16
35
  // 模拟请求和响应
17
36
  let request: Request;
18
37
  beforeEach(() => {
@@ -29,15 +48,16 @@ beforeEach(() => {
29
48
  afterEach(() => {
30
49
  // 清除模拟调用历史
31
50
  vi.clearAllMocks();
51
+ enableClerk = false;
32
52
  });
33
53
 
34
54
  describe('POST handler', () => {
35
- describe(' init chat model', () => {
55
+ describe('init chat model', () => {
36
56
  it('should initialize AgentRuntime correctly with valid authorization', async () => {
37
57
  const mockParams = { provider: 'test-provider' };
38
58
 
39
59
  // 设置 getJWTPayload 和 initAgentRuntimeWithUserPayload 的模拟返回值
40
- vi.mocked(getJWTPayload).mockResolvedValue({
60
+ vi.mocked(getJWTPayload).mockResolvedValueOnce({
41
61
  accessCode: 'test-access-code',
42
62
  apiKey: 'test-api-key',
43
63
  azureApiVersion: 'v1',
@@ -76,6 +96,46 @@ describe('POST handler', () => {
76
96
  errorType: 401,
77
97
  });
78
98
  });
99
+
100
+ it('should have pass clerk Auth when enable clerk', async () => {
101
+ enableClerk = true;
102
+
103
+ vi.mocked(getJWTPayload).mockResolvedValueOnce({
104
+ accessCode: 'test-access-code',
105
+ apiKey: 'test-api-key',
106
+ azureApiVersion: 'v1',
107
+ });
108
+
109
+ const mockParams = { provider: 'test-provider' };
110
+ // 设置 initAgentRuntimeWithUserPayload 的模拟返回值
111
+ vi.mocked(getAuth).mockReturnValue({} as any);
112
+ vi.mocked(checkAuthMethod).mockReset();
113
+
114
+ const mockRuntime: LobeRuntimeAI = { baseURL: 'abc', chat: vi.fn() };
115
+
116
+ vi.spyOn(AgentRuntime, 'initializeWithProviderOptions').mockResolvedValue(
117
+ new AgentRuntime(mockRuntime),
118
+ );
119
+
120
+ const request = new Request(new URL('https://test.com'), {
121
+ method: 'POST',
122
+ body: JSON.stringify({ model: 'test-model' }),
123
+ headers: {
124
+ [LOBE_CHAT_AUTH_HEADER]: 'some-valid-token',
125
+ [OAUTH_AUTHORIZED]: '1',
126
+ },
127
+ });
128
+
129
+ await POST(request, { params: mockParams });
130
+
131
+ expect(checkAuthMethod).toBeCalledWith({
132
+ accessCode: 'test-access-code',
133
+ apiKey: 'test-api-key',
134
+ clerkAuth: {},
135
+ nextAuthAuthorized: true,
136
+ });
137
+ });
138
+
79
139
  it('should return InternalServerError error when throw a unknown error', async () => {
80
140
  const mockParams = { provider: 'test-provider' };
81
141
  vi.mocked(getJWTPayload).mockRejectedValueOnce(new Error('unknown error'));
@@ -95,6 +155,12 @@ describe('POST handler', () => {
95
155
 
96
156
  describe('chat', () => {
97
157
  it('should correctly handle chat completion with valid payload', async () => {
158
+ vi.mocked(getJWTPayload).mockResolvedValueOnce({
159
+ accessCode: 'test-access-code',
160
+ apiKey: 'test-api-key',
161
+ azureApiVersion: 'v1',
162
+ });
163
+
98
164
  const mockParams = { provider: 'test-provider' };
99
165
  const mockChatPayload = { message: 'Hello, world!' };
100
166
  request = new Request(new URL('https://test.com'), {
@@ -114,6 +180,13 @@ describe('POST handler', () => {
114
180
  });
115
181
 
116
182
  it('should return an error response when chat completion fails', async () => {
183
+ // 设置 getJWTPayload 和 initAgentRuntimeWithUserPayload 的模拟返回值
184
+ vi.mocked(getJWTPayload).mockResolvedValueOnce({
185
+ accessCode: 'test-access-code',
186
+ apiKey: 'test-api-key',
187
+ azureApiVersion: 'v1',
188
+ });
189
+
117
190
  const mockParams = { provider: 'test-provider' };
118
191
  const mockChatPayload = { message: 'Hello, world!' };
119
192
  request = new Request(new URL('https://test.com'), {
@@ -0,0 +1,77 @@
1
+ import { getAuth } from '@clerk/nextjs/server';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { createErrorResponse } from '@/app/api/errorResponse';
5
+ import { AgentRuntimeError } from '@/libs/agent-runtime';
6
+ import { ChatErrorType } from '@/types/fetch';
7
+
8
+ import { RequestHandler, checkAuth } from './index';
9
+ import { checkAuthMethod, getJWTPayload } from './utils';
10
+
11
+ vi.mock('@clerk/nextjs/server', () => ({
12
+ getAuth: vi.fn(),
13
+ }));
14
+
15
+ vi.mock('@/app/api/errorResponse', () => ({
16
+ createErrorResponse: vi.fn(),
17
+ }));
18
+
19
+ vi.mock('./utils', () => ({
20
+ checkAuthMethod: vi.fn(),
21
+ getJWTPayload: vi.fn(),
22
+ }));
23
+
24
+ describe('checkAuth', () => {
25
+ const mockHandler: RequestHandler = vi.fn();
26
+ const mockRequest = new Request('https://example.com');
27
+ const mockOptions = { params: { provider: 'mock' } };
28
+
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ });
32
+
33
+ afterEach(() => {
34
+ vi.resetAllMocks();
35
+ });
36
+
37
+ it('should return unauthorized error if no authorization header', async () => {
38
+ await checkAuth(mockHandler)(mockRequest, mockOptions);
39
+
40
+ expect(createErrorResponse).toHaveBeenCalledWith(ChatErrorType.Unauthorized, {
41
+ error: AgentRuntimeError.createError(ChatErrorType.Unauthorized),
42
+ provider: 'mock',
43
+ });
44
+ expect(mockHandler).not.toHaveBeenCalled();
45
+ });
46
+
47
+ it('should return error response on getJWTPayload error', async () => {
48
+ const mockError = AgentRuntimeError.createError(ChatErrorType.Unauthorized);
49
+ mockRequest.headers.set('Authorization', 'invalid');
50
+ vi.mocked(getJWTPayload).mockRejectedValueOnce(mockError);
51
+
52
+ await checkAuth(mockHandler)(mockRequest, mockOptions);
53
+
54
+ expect(createErrorResponse).toHaveBeenCalledWith(ChatErrorType.Unauthorized, {
55
+ error: mockError,
56
+ provider: 'mock',
57
+ });
58
+ expect(mockHandler).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it('should return error response on checkAuthMethod error', async () => {
62
+ const mockError = AgentRuntimeError.createError(ChatErrorType.Unauthorized);
63
+ mockRequest.headers.set('Authorization', 'valid');
64
+ vi.mocked(getJWTPayload).mockResolvedValueOnce({});
65
+ vi.mocked(checkAuthMethod).mockImplementationOnce(() => {
66
+ throw mockError;
67
+ });
68
+
69
+ await checkAuth(mockHandler)(mockRequest, mockOptions);
70
+
71
+ expect(createErrorResponse).toHaveBeenCalledWith(ChatErrorType.Unauthorized, {
72
+ error: mockError,
73
+ provider: 'mock',
74
+ });
75
+ expect(mockHandler).not.toHaveBeenCalled();
76
+ });
77
+ });
@@ -1,5 +1,9 @@
1
+ import { AuthObject } from '@clerk/backend/internal';
2
+ import { getAuth } from '@clerk/nextjs/server';
3
+ import { NextRequest } from 'next/server';
4
+
1
5
  import { createErrorResponse } from '@/app/api/errorResponse';
2
- import { JWTPayload, LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/const/auth';
6
+ import { JWTPayload, LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED, enableClerk } from '@/const/auth';
3
7
  import { AgentRuntimeError, ChatCompletionErrorPayload } from '@/libs/agent-runtime';
4
8
  import { ChatErrorType } from '@/types/fetch';
5
9
 
@@ -23,9 +27,21 @@ export const checkAuth =
23
27
 
24
28
  if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized);
25
29
 
26
- // check the Auth With payload
30
+ // check the Auth With payload and clerk auth
31
+ let clerkAuth = {} as AuthObject;
32
+
33
+ if (enableClerk) {
34
+ clerkAuth = getAuth(req as NextRequest);
35
+ }
36
+
27
37
  jwtPayload = await getJWTPayload(authorization);
28
- checkAuthMethod(jwtPayload.accessCode, jwtPayload.apiKey, oauthAuthorized);
38
+
39
+ checkAuthMethod({
40
+ accessCode: jwtPayload.accessCode,
41
+ apiKey: jwtPayload.apiKey,
42
+ clerkAuth,
43
+ nextAuthAuthorized: oauthAuthorized,
44
+ });
29
45
  } catch (e) {
30
46
  const {
31
47
  errorType = ChatErrorType.InternalServerError,
@@ -1,7 +1,14 @@
1
+ import { type AuthObject } from '@clerk/backend/internal';
1
2
  import { importJWK, jwtVerify } from 'jose';
2
3
 
3
4
  import { getServerConfig } from '@/config/server';
4
- import { JWTPayload, JWT_SECRET_KEY, NON_HTTP_PREFIX } from '@/const/auth';
5
+ import {
6
+ JWTPayload,
7
+ JWT_SECRET_KEY,
8
+ NON_HTTP_PREFIX,
9
+ enableClerk,
10
+ enableNextAuth,
11
+ } from '@/const/auth';
5
12
  import { AgentRuntimeError } from '@/libs/agent-runtime';
6
13
  import { ChatErrorType } from '@/types/fetch';
7
14
 
@@ -30,6 +37,12 @@ export const getJWTPayload = async (token: string): Promise<JWTPayload> => {
30
37
  return payload as JWTPayload;
31
38
  };
32
39
 
40
+ interface CheckAuthParams {
41
+ accessCode?: string;
42
+ apiKey?: string;
43
+ clerkAuth: AuthObject;
44
+ nextAuthAuthorized?: boolean;
45
+ }
33
46
  /**
34
47
  * Check if the provided access code is valid, a user API key should be used or the OAuth 2 header is provided.
35
48
  *
@@ -38,19 +51,28 @@ export const getJWTPayload = async (token: string): Promise<JWTPayload> => {
38
51
  * @param {boolean} oauthAuthorized - Whether the OAuth 2 header is provided.
39
52
  * @throws {AgentRuntimeError} If the access code is invalid and no user API key is provided.
40
53
  */
41
- export const checkAuthMethod = (
42
- accessCode?: string,
43
- apiKey?: string,
44
- oauthAuthorized?: boolean,
45
- ) => {
46
- const { ACCESS_CODES, ENABLE_OAUTH_SSO } = getServerConfig();
54
+ export const checkAuthMethod = ({
55
+ apiKey,
56
+ nextAuthAuthorized,
57
+ accessCode,
58
+ clerkAuth,
59
+ }: CheckAuthParams) => {
60
+ // clerk auth handler
61
+ if (enableClerk) {
62
+ // if there is no userId, means the use is not login, just throw error
63
+ if (!clerkAuth?.userId) throw AgentRuntimeError.createError(ChatErrorType.InvalidClerkUser);
64
+ // if the user is login, just return
65
+ else return;
66
+ }
47
67
 
48
- // if OAuth 2 header is provided
49
- if (ENABLE_OAUTH_SSO && oauthAuthorized) return;
68
+ // if next auth handler is provided
69
+ if (enableNextAuth && nextAuthAuthorized) return;
50
70
 
51
71
  // if apiKey exist
52
72
  if (apiKey) return;
53
73
 
74
+ const { ACCESS_CODES } = getServerConfig();
75
+
54
76
  // if accessCode doesn't exist
55
77
  if (!ACCESS_CODES.length) return;
56
78
 
@@ -4,7 +4,7 @@ import { createGatewayOnEdgeRuntime } from '@lobehub/chat-plugins-gateway';
4
4
  import { getJWTPayload } from '@/app/api/chat/auth/utils';
5
5
  import { createErrorResponse } from '@/app/api/errorResponse';
6
6
  import { getServerConfig } from '@/config/server';
7
- import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/const/auth';
7
+ import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED, enableNextAuth } from '@/const/auth';
8
8
  import { LOBE_CHAT_TRACE_ID, TraceNameMap } from '@/const/trace';
9
9
  import { AgentRuntimeError } from '@/libs/agent-runtime';
10
10
  import { TraceClient } from '@/libs/traces';
@@ -14,13 +14,13 @@ import { getTracePayload } from '@/utils/trace';
14
14
  import { parserPluginSettings } from './settings';
15
15
 
16
16
  const checkAuth = (accessCode: string | null, oauthAuthorized: boolean | null) => {
17
- const { ACCESS_CODES, PLUGIN_SETTINGS, ENABLE_OAUTH_SSO } = getServerConfig();
17
+ const { ACCESS_CODES, PLUGIN_SETTINGS } = getServerConfig();
18
18
 
19
19
  // if there is no plugin settings, just skip the auth
20
20
  if (!PLUGIN_SETTINGS) return { auth: true };
21
21
 
22
22
  // If authorized by oauth
23
- if (oauthAuthorized && ENABLE_OAUTH_SSO) return { auth: true };
23
+ if (oauthAuthorized && enableNextAuth) return { auth: true };
24
24
 
25
25
  // if accessCode doesn't exist
26
26
  if (!ACCESS_CODES.length) return { auth: true };