@lobehub/chat 0.153.0 → 0.154.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/CHANGELOG.md +50 -0
- package/docs/self-hosting/advanced/authentication.mdx +25 -18
- package/docs/self-hosting/advanced/authentication.zh-CN.mdx +26 -17
- package/docs/self-hosting/advanced/model-list.mdx +2 -2
- package/docs/self-hosting/advanced/model-list.zh-CN.mdx +2 -2
- package/docs/self-hosting/environment-variables/auth.mdx +48 -39
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +49 -43
- package/locales/ar/auth.json +6 -0
- package/locales/ar/clerk.json +769 -0
- package/locales/ar/common.json +2 -0
- package/locales/ar/error.json +8 -0
- package/locales/bg-BG/auth.json +6 -0
- package/locales/bg-BG/clerk.json +769 -0
- package/locales/bg-BG/common.json +2 -0
- package/locales/bg-BG/error.json +8 -0
- package/locales/de-DE/auth.json +6 -0
- package/locales/de-DE/clerk.json +769 -0
- package/locales/de-DE/common.json +2 -0
- package/locales/de-DE/error.json +8 -0
- package/locales/en-US/auth.json +6 -0
- package/locales/en-US/clerk.json +769 -0
- package/locales/en-US/common.json +2 -0
- package/locales/en-US/error.json +8 -0
- package/locales/es-ES/auth.json +6 -0
- package/locales/es-ES/clerk.json +769 -0
- package/locales/es-ES/common.json +2 -0
- package/locales/es-ES/error.json +8 -0
- package/locales/fr-FR/auth.json +6 -0
- package/locales/fr-FR/clerk.json +769 -0
- package/locales/fr-FR/common.json +2 -0
- package/locales/fr-FR/error.json +8 -0
- package/locales/it-IT/auth.json +6 -0
- package/locales/it-IT/clerk.json +769 -0
- package/locales/it-IT/common.json +2 -0
- package/locales/it-IT/error.json +8 -0
- package/locales/ja-JP/auth.json +6 -0
- package/locales/ja-JP/clerk.json +769 -0
- package/locales/ja-JP/common.json +2 -0
- package/locales/ja-JP/error.json +8 -0
- package/locales/ko-KR/auth.json +6 -0
- package/locales/ko-KR/clerk.json +769 -0
- package/locales/ko-KR/common.json +2 -0
- package/locales/ko-KR/error.json +8 -0
- package/locales/nl-NL/auth.json +6 -0
- package/locales/nl-NL/clerk.json +769 -0
- package/locales/nl-NL/common.json +2 -0
- package/locales/nl-NL/error.json +8 -0
- package/locales/pl-PL/auth.json +6 -0
- package/locales/pl-PL/clerk.json +769 -0
- package/locales/pl-PL/common.json +2 -0
- package/locales/pl-PL/error.json +8 -0
- package/locales/pt-BR/auth.json +6 -0
- package/locales/pt-BR/clerk.json +769 -0
- package/locales/pt-BR/common.json +2 -0
- package/locales/pt-BR/error.json +8 -0
- package/locales/ru-RU/auth.json +6 -0
- package/locales/ru-RU/clerk.json +769 -0
- package/locales/ru-RU/common.json +2 -0
- package/locales/ru-RU/error.json +8 -0
- package/locales/tr-TR/auth.json +6 -0
- package/locales/tr-TR/clerk.json +769 -0
- package/locales/tr-TR/common.json +2 -0
- package/locales/tr-TR/error.json +8 -0
- package/locales/vi-VN/auth.json +6 -0
- package/locales/vi-VN/clerk.json +769 -0
- package/locales/vi-VN/common.json +2 -0
- package/locales/vi-VN/error.json +8 -0
- package/locales/zh-CN/auth.json +6 -0
- package/locales/zh-CN/clerk.json +769 -0
- package/locales/zh-CN/common.json +2 -0
- package/locales/zh-CN/error.json +8 -0
- package/locales/zh-TW/auth.json +6 -0
- package/locales/zh-TW/clerk.json +769 -0
- package/locales/zh-TW/common.json +2 -0
- package/locales/zh-TW/error.json +8 -0
- package/package.json +5 -1
- package/src/app/(auth)/layout.tsx +19 -0
- package/src/app/(auth)/login/[[...login]]/PageTitle.tsx +13 -0
- package/src/app/(auth)/login/[[...login]]/page.tsx +14 -0
- package/src/app/(auth)/profile/[[...slugs]]/PageTitle.tsx +13 -0
- package/src/app/(auth)/profile/[[...slugs]]/page.tsx +14 -0
- package/src/app/(auth)/signup/[[...signup]]/PageTitle.tsx +13 -0
- package/src/app/(auth)/signup/[[...signup]]/page.tsx +14 -0
- package/src/app/(main)/(mobile)/me/features/AvatarBanner.tsx +12 -40
- package/src/app/(main)/(mobile)/me/features/Header.tsx +37 -0
- package/src/app/(main)/(mobile)/me/features/style.ts +29 -0
- package/src/app/(main)/(mobile)/me/layout.tsx +7 -1
- package/src/app/(main)/(mobile)/me/loading.tsx +22 -7
- package/src/app/(main)/(mobile)/me/page.tsx +4 -5
- package/src/app/(main)/@nav/_layout/Mobile.tsx +2 -2
- package/src/app/(main)/market/@detail/features/{AgentDetailContent/index.tsx → AgentDetailContent.tsx} +6 -12
- package/src/app/(main)/market/@detail/features/Banner.tsx +46 -0
- package/src/app/(main)/market/@detail/features/{AgentDetailContent/Header.tsx → Header.tsx} +4 -18
- package/src/app/(main)/market/@detail/features/{AgentDetailContent/Loading.tsx → Loading.tsx} +5 -4
- package/src/app/(main)/market/@detail/features/{AgentDetailContent/style.ts → style.ts} +0 -3
- package/src/app/(main)/market/features/AgentCard/AgentCardBanner.tsx +13 -8
- package/src/app/(main)/market/loading.tsx +13 -1
- package/src/app/(main)/settings/common/features/Common.tsx +1 -1
- package/src/app/(main)/settings/common/features/Theme/index.tsx +2 -0
- package/src/app/(main)/settings/common/index.tsx +7 -3
- package/src/app/api/chat/[provider]/route.test.ts +76 -3
- package/src/app/api/chat/auth/index.test.ts +77 -0
- package/src/app/api/chat/auth/index.ts +19 -3
- package/src/app/api/chat/auth/utils.ts +31 -9
- package/src/app/api/plugin/gateway/route.ts +3 -3
- package/src/components/Cell/Divider.tsx +2 -0
- package/src/components/Cell/index.tsx +5 -1
- package/src/components/server/MobileNavLayout.tsx +1 -0
- package/src/config/auth.ts +146 -0
- package/src/config/server/index.ts +1 -3
- package/src/const/auth.ts +7 -0
- package/src/features/Conversation/Error/ClerkLogin/index.tsx +47 -0
- package/src/features/Conversation/Error/index.tsx +5 -0
- package/src/features/Conversation/components/InboxWelcome/AgentsSuggest.tsx +15 -6
- package/src/features/Conversation/components/InboxWelcome/QuestionSuggest.tsx +9 -4
- package/src/features/Conversation/components/InboxWelcome/index.tsx +9 -20
- package/src/features/DataImporter/index.tsx +4 -1
- package/src/features/User/PlanTag.tsx +45 -0
- package/src/features/User/UserAvatar.tsx +11 -5
- package/src/features/User/UserInfo.tsx +30 -12
- package/src/features/User/UserLoginOrSignup.tsx +23 -0
- package/src/features/User/UserPanel/PanelContent.tsx +74 -0
- package/src/features/User/UserPanel/UpgradeBadge.tsx +19 -0
- package/src/features/User/UserPanel/index.tsx +8 -27
- package/src/features/User/UserPanel/useMenu.tsx +49 -21
- package/src/features/User/__tests__/PanelContent.test.tsx +151 -0
- package/src/features/User/__tests__/UserAvatar.test.tsx +78 -0
- package/src/hooks/useGreeting/greetingTime.ts +14 -0
- package/src/hooks/useGreeting/index.ts +16 -0
- package/src/layout/AuthProvider/Clerk/UserUpdater.tsx +41 -0
- package/src/layout/AuthProvider/Clerk/index.tsx +26 -0
- package/src/layout/AuthProvider/Clerk/useAppearance.ts +118 -0
- package/src/layout/AuthProvider/index.tsx +8 -4
- package/src/libs/next-auth/index.ts +4 -6
- package/src/libs/next-auth/sso-providers/auth0.ts +4 -6
- package/src/libs/next-auth/sso-providers/authentik.ts +4 -6
- package/src/libs/next-auth/sso-providers/azure-ad.ts +4 -6
- package/src/libs/next-auth/sso-providers/github.ts +3 -5
- package/src/libs/next-auth/sso-providers/zitadel.ts +4 -6
- package/src/locales/default/auth.ts +6 -0
- package/src/locales/default/clerk.ts +782 -0
- package/src/locales/default/common.ts +4 -1
- package/src/locales/default/error.ts +8 -0
- package/src/locales/default/index.ts +4 -0
- package/src/middleware.ts +19 -6
- package/src/server/globalConfig/index.ts +2 -2
- package/src/store/user/selectors.ts +1 -1
- package/src/store/user/slices/auth/action.test.ts +105 -0
- package/src/store/user/slices/auth/action.ts +40 -5
- package/src/store/user/slices/auth/initialState.ts +15 -0
- package/src/store/user/slices/auth/selectors.test.ts +127 -0
- package/src/store/user/slices/auth/selectors.ts +38 -1
- package/src/store/user/slices/settings/selectors/selectors.test.ts +25 -0
- package/src/store/user/slices/settings/selectors/settings.ts +6 -0
- package/src/styles/mobileHeader.ts +7 -0
- package/src/types/fetch.ts +1 -0
- package/src/config/server/auth.ts +0 -71
- package/src/features/User/UserPanel/Popover.tsx +0 -35
- /package/src/app/(main)/market/@detail/features/{AgentDetailContent/Comment.tsx → Comment.tsx} +0 -0
- /package/src/app/(main)/market/@detail/features/{AgentDetailContent/TokenTag.tsx → TokenTag.tsx} +0 -0
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
import { Skeleton } from 'antd';
|
|
4
|
+
import { Flexbox } from 'react-layout-kit';
|
|
2
5
|
|
|
3
|
-
export default () =>
|
|
6
|
+
export default () => (
|
|
7
|
+
<Flexbox gap={16}>
|
|
8
|
+
<Skeleton.Input active block />
|
|
9
|
+
<Skeleton paragraph={{ rows: 8 }} style={{ marginBlock: 24 }} title={false} />
|
|
10
|
+
<Skeleton.Button active />
|
|
11
|
+
<Skeleton paragraph={{ rows: 8 }} style={{ marginBlock: 24 }} title={false} />
|
|
12
|
+
<Skeleton.Button active />
|
|
13
|
+
<Skeleton paragraph={{ rows: 8 }} style={{ marginBlock: 24 }} title={false} />
|
|
14
|
+
</Flexbox>
|
|
15
|
+
);
|
|
@@ -21,7 +21,7 @@ type SettingItemGroup = ItemGroup;
|
|
|
21
21
|
|
|
22
22
|
export interface SettingsCommonProps {
|
|
23
23
|
showAccessCodeConfig: boolean;
|
|
24
|
-
showOAuthLogin
|
|
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
|
|
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('
|
|
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).
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
49
|
-
if (
|
|
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
|
|
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 &&
|
|
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 };
|
|
@@ -8,7 +8,11 @@ const { Item } = List;
|
|
|
8
8
|
const useStyles = createStyles(({ css, token, isDarkMode }) => ({
|
|
9
9
|
container: css`
|
|
10
10
|
position: relative;
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
gap: 12px;
|
|
13
|
+
|
|
14
|
+
padding: 16px !important;
|
|
15
|
+
|
|
12
16
|
background: ${isDarkMode ? token.colorBgLayout : token.colorBgContainer};
|
|
13
17
|
border-radius: 0;
|
|
14
18
|
`,
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix , typescript-sort-keys/interface */
|
|
2
|
+
import { createEnv } from '@t3-oss/env-nextjs';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
7
|
+
namespace NodeJS {
|
|
8
|
+
interface ProcessEnv {
|
|
9
|
+
// ===== Clerk ===== //
|
|
10
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY?: string;
|
|
11
|
+
CLERK_SECRET_KEY?: string;
|
|
12
|
+
CLERK_WEBHOOK_SECRET?: string;
|
|
13
|
+
|
|
14
|
+
// ===== Next Auth ===== //
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated
|
|
17
|
+
*/
|
|
18
|
+
ENABLE_OAUTH_SSO?: string;
|
|
19
|
+
NEXT_AUTH_SECRET?: string;
|
|
20
|
+
/**
|
|
21
|
+
* @deprecated
|
|
22
|
+
*/
|
|
23
|
+
SSO_PROVIDERS?: string;
|
|
24
|
+
NEXT_AUTH_SSO_PROVIDERS?: string;
|
|
25
|
+
|
|
26
|
+
AUTH0_CLIENT_ID?: string;
|
|
27
|
+
AUTH0_CLIENT_SECRET?: string;
|
|
28
|
+
AUTH0_ISSUER?: string;
|
|
29
|
+
|
|
30
|
+
// Github
|
|
31
|
+
GITHUB_CLIENT_ID?: string;
|
|
32
|
+
GITHUB_CLIENT_SECRET?: string;
|
|
33
|
+
|
|
34
|
+
// Azure AD
|
|
35
|
+
AZURE_AD_CLIENT_ID?: string;
|
|
36
|
+
AZURE_AD_CLIENT_SECRET?: string;
|
|
37
|
+
AZURE_AD_TENANT_ID?: string;
|
|
38
|
+
|
|
39
|
+
// AUTHENTIK
|
|
40
|
+
AUTHENTIK_CLIENT_ID?: string;
|
|
41
|
+
AUTHENTIK_CLIENT_SECRET?: string;
|
|
42
|
+
AUTHENTIK_ISSUER?: string;
|
|
43
|
+
|
|
44
|
+
// ZITADEL
|
|
45
|
+
ZITADEL_CLIENT_ID?: string;
|
|
46
|
+
ZITADEL_CLIENT_SECRET?: string;
|
|
47
|
+
ZITADEL_ISSUER?: string;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const getAuthConfig = () => {
|
|
53
|
+
if (process.env.ENABLE_OAUTH_SSO) {
|
|
54
|
+
console.warn(
|
|
55
|
+
'`ENABLE_OAUTH_SSO` is deprecated and will be removed in LobeChat 1.0. just set `NEXT_AUTH_SECRET` enough',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (process.env.SSO_PROVIDERS) {
|
|
60
|
+
console.warn(
|
|
61
|
+
'`SSO_PROVIDERS` is deprecated and will be removed in LobeChat 1.0. Please replace with `NEXT_AUTH_SSO_PROVIDERS`',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return createEnv({
|
|
66
|
+
client: {
|
|
67
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().optional(),
|
|
68
|
+
/**
|
|
69
|
+
* whether to enabled clerk
|
|
70
|
+
*/
|
|
71
|
+
NEXT_PUBLIC_ENABLE_CLERK_AUTH: z.boolean().optional(),
|
|
72
|
+
|
|
73
|
+
NEXT_PUBLIC_ENABLE_NEXT_AUTH: z.boolean().optional(),
|
|
74
|
+
},
|
|
75
|
+
server: {
|
|
76
|
+
// Clerk
|
|
77
|
+
CLERK_SECRET_KEY: z.string().optional(),
|
|
78
|
+
|
|
79
|
+
// NEXT-AUTH
|
|
80
|
+
NEXT_AUTH_SECRET: z.string().optional(),
|
|
81
|
+
NEXT_AUTH_SSO_PROVIDERS: z.string().optional().default('auth0'),
|
|
82
|
+
|
|
83
|
+
// Auth0
|
|
84
|
+
AUTH0_CLIENT_ID: z.string().optional(),
|
|
85
|
+
AUTH0_CLIENT_SECRET: z.string().optional(),
|
|
86
|
+
AUTH0_ISSUER: z.string().optional(),
|
|
87
|
+
|
|
88
|
+
// Github
|
|
89
|
+
GITHUB_CLIENT_ID: z.string().optional(),
|
|
90
|
+
GITHUB_CLIENT_SECRET: z.string().optional(),
|
|
91
|
+
|
|
92
|
+
// Azure AD
|
|
93
|
+
AZURE_AD_CLIENT_ID: z.string().optional(),
|
|
94
|
+
AZURE_AD_CLIENT_SECRET: z.string().optional(),
|
|
95
|
+
AZURE_AD_TENANT_ID: z.string().optional(),
|
|
96
|
+
|
|
97
|
+
// AUTHENTIK
|
|
98
|
+
AUTHENTIK_CLIENT_ID: z.string().optional(),
|
|
99
|
+
AUTHENTIK_CLIENT_SECRET: z.string().optional(),
|
|
100
|
+
AUTHENTIK_ISSUER: z.string().optional(),
|
|
101
|
+
|
|
102
|
+
// ZITADEL
|
|
103
|
+
ZITADEL_CLIENT_ID: z.string().optional(),
|
|
104
|
+
ZITADEL_CLIENT_SECRET: z.string().optional(),
|
|
105
|
+
ZITADEL_ISSUER: z.string().optional(),
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
runtimeEnv: {
|
|
109
|
+
// Clerk
|
|
110
|
+
NEXT_PUBLIC_ENABLE_CLERK_AUTH: !!process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
|
111
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
|
112
|
+
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
|
|
113
|
+
|
|
114
|
+
// Next Auth
|
|
115
|
+
NEXT_PUBLIC_ENABLE_NEXT_AUTH: !!process.env.NEXT_AUTH_SECRET || process.env.ENABLE_OAUTH_SSO,
|
|
116
|
+
NEXT_AUTH_SSO_PROVIDERS: process.env.NEXT_AUTH_SSO_PROVIDERS || process.env.SSO_PROVIDERS,
|
|
117
|
+
NEXT_AUTH_SECRET: process.env.NEXT_AUTH_SECRET,
|
|
118
|
+
|
|
119
|
+
// Auth0
|
|
120
|
+
AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID,
|
|
121
|
+
AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET,
|
|
122
|
+
AUTH0_ISSUER: process.env.AUTH0_ISSUER,
|
|
123
|
+
|
|
124
|
+
// Github
|
|
125
|
+
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
|
|
126
|
+
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
|
127
|
+
|
|
128
|
+
// Azure AD
|
|
129
|
+
AZURE_AD_CLIENT_ID: process.env.AZURE_AD_CLIENT_ID,
|
|
130
|
+
AZURE_AD_CLIENT_SECRET: process.env.AZURE_AD_CLIENT_SECRET,
|
|
131
|
+
AZURE_AD_TENANT_ID: process.env.AZURE_AD_TENANT_ID,
|
|
132
|
+
|
|
133
|
+
// AUTHENTIK
|
|
134
|
+
AUTHENTIK_CLIENT_ID: process.env.AUTHENTIK_CLIENT_ID,
|
|
135
|
+
AUTHENTIK_CLIENT_SECRET: process.env.AUTHENTIK_CLIENT_SECRET,
|
|
136
|
+
AUTHENTIK_ISSUER: process.env.AUTHENTIK_ISSUER,
|
|
137
|
+
|
|
138
|
+
// ZITADEL
|
|
139
|
+
ZITADEL_CLIENT_ID: process.env.ZITADEL_CLIENT_ID,
|
|
140
|
+
ZITADEL_CLIENT_SECRET: process.env.ZITADEL_CLIENT_SECRET,
|
|
141
|
+
ZITADEL_ISSUER: process.env.ZITADEL_ISSUER,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const authEnv = getAuthConfig();
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { getAnalyticsConfig } from './analytics';
|
|
2
2
|
import { getAppConfig } from './app';
|
|
3
|
-
import { getAuthConfig } from './auth';
|
|
4
3
|
import { getProviderConfig } from './provider';
|
|
5
4
|
|
|
6
5
|
export const getServerConfig = () => {
|
|
@@ -10,8 +9,7 @@ export const getServerConfig = () => {
|
|
|
10
9
|
|
|
11
10
|
const provider = getProviderConfig();
|
|
12
11
|
const app = getAppConfig();
|
|
13
|
-
const auth = getAuthConfig();
|
|
14
12
|
const analytics = getAnalyticsConfig();
|
|
15
13
|
|
|
16
|
-
return { ...provider, ...app, ...analytics
|
|
14
|
+
return { ...provider, ...app, ...analytics };
|
|
17
15
|
};
|
package/src/const/auth.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { authEnv } from '@/config/auth';
|
|
2
|
+
|
|
3
|
+
export const enableClerk = authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH;
|
|
4
|
+
export const enableNextAuth = authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH;
|
|
5
|
+
export const enableAuth =
|
|
6
|
+
authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH || authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH;
|
|
7
|
+
|
|
1
8
|
export const LOBE_CHAT_AUTH_HEADER = 'X-lobe-chat-auth';
|
|
2
9
|
|
|
3
10
|
export const OAUTH_AUTHORIZED = 'X-oauth-authorized';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Button } from 'antd';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
|
|
5
|
+
import UserLoginOrSignup from '@/features/User/UserLoginOrSignup';
|
|
6
|
+
import { useGreeting } from '@/hooks/useGreeting';
|
|
7
|
+
import { useChatStore } from '@/store/chat';
|
|
8
|
+
import { useUserStore } from '@/store/user';
|
|
9
|
+
import { userProfileSelectors } from '@/store/user/selectors';
|
|
10
|
+
|
|
11
|
+
import { ErrorActionContainer, FormAction } from '../style';
|
|
12
|
+
|
|
13
|
+
const ClerkLogin = memo<{ id: string }>(({ id }) => {
|
|
14
|
+
const { t } = useTranslation('error');
|
|
15
|
+
const [openSignIn, isSignedIn] = useUserStore((s) => [s.openLogin, s.isSignedIn]);
|
|
16
|
+
const greeting = useGreeting();
|
|
17
|
+
const nickName = useUserStore(userProfileSelectors.nickName);
|
|
18
|
+
const [resend, deleteMessage] = useChatStore((s) => [s.internalResendMessage, s.deleteMessage]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<ErrorActionContainer>
|
|
22
|
+
{isSignedIn ? (
|
|
23
|
+
<FormAction
|
|
24
|
+
avatar={'🌟'}
|
|
25
|
+
description={t('clerkAuth.loginSuccess.desc', { greeting })}
|
|
26
|
+
title={t('clerkAuth.loginSuccess.title', { nickName })}
|
|
27
|
+
>
|
|
28
|
+
<Button
|
|
29
|
+
block
|
|
30
|
+
onClick={() => {
|
|
31
|
+
resend(id);
|
|
32
|
+
deleteMessage(id);
|
|
33
|
+
}}
|
|
34
|
+
size={'large'}
|
|
35
|
+
type={'primary'}
|
|
36
|
+
>
|
|
37
|
+
{t('clerkAuth.loginSuccess.action')}
|
|
38
|
+
</Button>
|
|
39
|
+
</FormAction>
|
|
40
|
+
) : (
|
|
41
|
+
<UserLoginOrSignup onClick={openSignIn} />
|
|
42
|
+
)}
|
|
43
|
+
</ErrorActionContainer>
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export default ClerkLogin;
|
|
@@ -6,6 +6,7 @@ import { AgentRuntimeErrorType, ILobeAgentRuntimeErrorType } from '@/libs/agent-
|
|
|
6
6
|
import { ChatErrorType, ErrorType } from '@/types/fetch';
|
|
7
7
|
import { ChatMessage, ChatMessageError } from '@/types/message';
|
|
8
8
|
|
|
9
|
+
import ClerkLogin from './ClerkLogin';
|
|
9
10
|
import ErrorJsonViewer from './ErrorJsonViewer';
|
|
10
11
|
import InvalidAPIKey from './InvalidAPIKey';
|
|
11
12
|
import InvalidAccessCode from './InvalidAccessCode';
|
|
@@ -63,6 +64,10 @@ const ErrorMessageExtra = memo<{ data: ChatMessage }>(({ data }) => {
|
|
|
63
64
|
return <OllamaBizError {...data} />;
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
case ChatErrorType.InvalidClerkUser: {
|
|
68
|
+
return <ClerkLogin id={data.id} />;
|
|
69
|
+
}
|
|
70
|
+
|
|
66
71
|
case ChatErrorType.InvalidAccessCode: {
|
|
67
72
|
return <InvalidAccessCode id={data.id} provider={data.error?.body?.provider} />;
|
|
68
73
|
}
|