@lobehub/chat 0.159.0 → 0.159.2
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 +58 -0
- package/README.zh-CN.md +0 -1
- package/docs/self-hosting/advanced/authentication.mdx +9 -0
- package/docs/self-hosting/advanced/authentication.zh-CN.mdx +9 -0
- package/package.json +2 -2
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/DragUpload.tsx +36 -19
- package/src/app/(main)/settings/common/features/Common.tsx +11 -8
- package/src/app/(main)/settings/common/index.tsx +1 -9
- package/src/features/Conversation/Error/OAuthForm.tsx +6 -4
- package/src/features/User/__tests__/UserAvatar.test.tsx +8 -3
- package/src/features/User/__tests__/useMenu.test.tsx +3 -3
- package/src/libs/next-auth/index.ts +0 -7
- package/src/server/globalConfig/index.ts +2 -0
- package/src/store/serverConfig/selectors.ts +1 -0
- package/src/store/user/slices/auth/action.test.ts +61 -0
- package/src/store/user/slices/auth/action.ts +17 -15
- package/src/store/user/slices/auth/selectors.test.ts +18 -2
- package/src/store/user/slices/auth/selectors.ts +4 -4
- package/src/types/next-auth.d.ts +23 -0
- package/src/types/serverConfig.ts +1 -0
- package/src/hooks/useOAuthSession.ts +0 -24
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.159.2](https://github.com/lobehub/lobe-chat/compare/v0.159.1...v0.159.2)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2024-05-14**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Dragging text mistakenly as image.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Dragging text mistakenly as image, closes [#2111](https://github.com/lobehub/lobe-chat/issues/2111) ([3c047ef](https://github.com/lobehub/lobe-chat/commit/3c047ef))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
### [Version 0.159.1](https://github.com/lobehub/lobe-chat/compare/v0.159.0...v0.159.1)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2024-05-14**</sup>
|
|
33
|
+
|
|
34
|
+
#### ♻ Code Refactoring
|
|
35
|
+
|
|
36
|
+
- **misc**: Move next-auth hooks to user store actions.
|
|
37
|
+
|
|
38
|
+
#### 🐛 Bug Fixes
|
|
39
|
+
|
|
40
|
+
- **misc**: Pin `antd@5.17.0` to fix build error.
|
|
41
|
+
|
|
42
|
+
<br/>
|
|
43
|
+
|
|
44
|
+
<details>
|
|
45
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
46
|
+
|
|
47
|
+
#### Code refactoring
|
|
48
|
+
|
|
49
|
+
- **misc**: Move next-auth hooks to user store actions, closes [#2364](https://github.com/lobehub/lobe-chat/issues/2364) ([6dbcd70](https://github.com/lobehub/lobe-chat/commit/6dbcd70))
|
|
50
|
+
|
|
51
|
+
#### What's fixed
|
|
52
|
+
|
|
53
|
+
- **misc**: Pin `antd@5.17.0` to fix build error, closes [#2483](https://github.com/lobehub/lobe-chat/issues/2483) ([aa03833](https://github.com/lobehub/lobe-chat/commit/aa03833))
|
|
54
|
+
|
|
55
|
+
</details>
|
|
56
|
+
|
|
57
|
+
<div align="right">
|
|
58
|
+
|
|
59
|
+
[](#readme-top)
|
|
60
|
+
|
|
61
|
+
</div>
|
|
62
|
+
|
|
5
63
|
## [Version 0.159.0](https://github.com/lobehub/lobe-chat/compare/v0.158.2...v0.159.0)
|
|
6
64
|
|
|
7
65
|
<sup>Released on **2024-05-14**</sup>
|
package/README.zh-CN.md
CHANGED
|
@@ -131,7 +131,6 @@
|
|
|
131
131
|
- **Minimax**: 接入了 Minimax 的 AI 模型,包括 MoE 模型 **abab6**,提供了更多的选择空间。[了解更多](https://www.minimaxi.com/)
|
|
132
132
|
- **DeepSeek**: 接入了 DeepSeek 的 AI 模型,包括最新的 **DeepSeek-V2**,提供兼顾性能与价格的模型。[了解更多](https://www.deepseek.com/)
|
|
133
133
|
|
|
134
|
-
|
|
135
134
|
同时,我们也在计划支持更多的模型服务商,如 Replicate 和 Perplexity 等,以进一步丰富我们的服务商库。如果你希望让 LobeChat 支持你喜爱的服务商,欢迎加入我们的[社区讨论](https://github.com/lobehub/lobe-chat/discussions/1284)。
|
|
136
135
|
|
|
137
136
|
<div align="right">
|
|
@@ -25,6 +25,15 @@ By setting the environment variables NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK
|
|
|
25
25
|
|
|
26
26
|
## Next Auth
|
|
27
27
|
|
|
28
|
+
Before using NextAuth, please set the following variables in LobeChat's environment variables:
|
|
29
|
+
|
|
30
|
+
| Environment Variable | Type | Description |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `NEXT_AUTH_SECRET` | Required | The key used to encrypt Auth.js session tokens. You can use the following command: `openssl rand -base64 32`, or visit `https://generate-secret.vercel.app/32` to generate the key. |
|
|
33
|
+
| `ACCESS_CODE` | Required | Add a password to access this service. You can set a sufficiently long random password to "disable" access code authorization. |
|
|
34
|
+
| `NEXTAUTH_URL` | Optional | This URL specifies the callback address for Auth.js when performing OAuth verification. Set this only if the default generated redirect address is incorrect. `https://example.com/api/auth` |
|
|
35
|
+
| `NEXT_AUTH_SSO_PROVIDERS` | Optional | This environment variable is used to enable multiple identity verification sources simultaneously, separated by commas, for example, `auth0,azure-ad,authentik`. |
|
|
36
|
+
|
|
28
37
|
Currently supported identity verification services include:
|
|
29
38
|
|
|
30
39
|
<Cards>
|
|
@@ -22,6 +22,15 @@ LobeChat 与 Clerk 做了深度集成,能够为用户提供一个更加安全
|
|
|
22
22
|
|
|
23
23
|
## Next Auth
|
|
24
24
|
|
|
25
|
+
在使用 NextAuth 之前,请先在 LobeChat 的环境变量中设置以下变量:
|
|
26
|
+
|
|
27
|
+
| 环境变量 | 类型 | 描述 |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令: `openssl rand -base64 32`,或者访问 `https://generate-secret.vercel.app/32` 生成秘钥。 |
|
|
30
|
+
| `ACCESS_CODE` | 必选 | 添加访问此服务的密码,你可以设置一个足够长的随机密码以 “禁用” 访问码授权 |
|
|
31
|
+
| `NEXTAUTH_URL` | 可选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://example.com/api/auth` |
|
|
32
|
+
| `NEXT_AUTH_SSO_PROVIDERS` | 可选 | 该环境变量用于同时启用多个身份验证源,以逗号 `,` 分割,例如 `auth0,azure-ad,authentik`。 |
|
|
33
|
+
|
|
25
34
|
目前支持的身份验证服务有:
|
|
26
35
|
|
|
27
36
|
<Cards>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.159.
|
|
3
|
+
"version": "0.159.2",
|
|
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",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"@vercel/speed-insights": "^1.0.10",
|
|
109
109
|
"ahooks": "^3.7.11",
|
|
110
110
|
"ai": "3.0.19",
|
|
111
|
-
"antd": "
|
|
111
|
+
"antd": "5.17.0",
|
|
112
112
|
"antd-style": "^3.6.2",
|
|
113
113
|
"brotli-wasm": "^3.0.0",
|
|
114
114
|
"chroma-js": "^2.4.2",
|
|
@@ -61,7 +61,12 @@ const useStyles = createStyles(({ css, token, stylish }) => {
|
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
const handleDragOver = (e: DragEvent) => {
|
|
64
|
-
e.
|
|
64
|
+
if (!e.dataTransfer?.items || e.dataTransfer.items.length === 0) return;
|
|
65
|
+
|
|
66
|
+
const isFile = e.dataTransfer.types.includes('Files');
|
|
67
|
+
if (isFile) {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
}
|
|
65
70
|
};
|
|
66
71
|
|
|
67
72
|
const DragUpload = memo(() => {
|
|
@@ -92,43 +97,55 @@ const DragUpload = memo(() => {
|
|
|
92
97
|
};
|
|
93
98
|
|
|
94
99
|
const handleDragEnter = (e: DragEvent) => {
|
|
95
|
-
e.
|
|
100
|
+
if (!e.dataTransfer?.items || e.dataTransfer.items.length === 0) return;
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
if (
|
|
102
|
+
const isFile = e.dataTransfer.types.includes('Files');
|
|
103
|
+
if (isFile) {
|
|
104
|
+
dragCounter.current += 1;
|
|
105
|
+
e.preventDefault();
|
|
99
106
|
setIsDragging(true);
|
|
100
107
|
}
|
|
101
108
|
};
|
|
102
109
|
|
|
103
110
|
const handleDragLeave = (e: DragEvent) => {
|
|
104
|
-
e.
|
|
111
|
+
if (!e.dataTransfer?.items || e.dataTransfer.items.length === 0) return;
|
|
105
112
|
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
const isFile = e.dataTransfer.types.includes('Files');
|
|
114
|
+
if (isFile) {
|
|
115
|
+
e.preventDefault();
|
|
108
116
|
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
// reset counter
|
|
118
|
+
dragCounter.current -= 1;
|
|
119
|
+
|
|
120
|
+
if (dragCounter.current === 0) {
|
|
121
|
+
setIsDragging(false);
|
|
122
|
+
}
|
|
111
123
|
}
|
|
112
124
|
};
|
|
113
125
|
|
|
114
126
|
const handleDrop = async (e: DragEvent) => {
|
|
115
|
-
e.
|
|
116
|
-
// reset counter
|
|
117
|
-
dragCounter.current = 0;
|
|
127
|
+
if (!e.dataTransfer?.items || e.dataTransfer.items.length === 0) return;
|
|
118
128
|
|
|
119
|
-
|
|
129
|
+
const isFile = e.dataTransfer.types.includes('Files');
|
|
130
|
+
if (isFile) {
|
|
131
|
+
e.preventDefault();
|
|
120
132
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const files = e.dataTransfer?.files;
|
|
133
|
+
// reset counter
|
|
134
|
+
dragCounter.current = 0;
|
|
124
135
|
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
setIsDragging(false);
|
|
137
|
+
|
|
138
|
+
// get filesList
|
|
139
|
+
// TODO: support folder files upload
|
|
140
|
+
const files = e.dataTransfer?.files;
|
|
141
|
+
|
|
142
|
+
// upload files
|
|
143
|
+
uploadImages(files);
|
|
144
|
+
}
|
|
127
145
|
};
|
|
128
146
|
|
|
129
147
|
const handlePaste = (event: ClipboardEvent) => {
|
|
130
148
|
// get files from clipboard
|
|
131
|
-
|
|
132
149
|
const files = event.clipboardData?.files;
|
|
133
150
|
|
|
134
151
|
uploadImages(files);
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { Form, type ItemGroup } from '@lobehub/ui';
|
|
4
4
|
import { App, Button, Input } from 'antd';
|
|
5
5
|
import isEqual from 'fast-deep-equal';
|
|
6
|
-
import { signIn, signOut } from 'next-auth/react';
|
|
7
6
|
import { memo, useCallback } from 'react';
|
|
8
7
|
import { useTranslation } from 'react-i18next';
|
|
9
8
|
|
|
@@ -12,6 +11,8 @@ import { FORM_STYLE } from '@/const/layoutTokens';
|
|
|
12
11
|
import { DEFAULT_SETTINGS } from '@/const/settings';
|
|
13
12
|
import { useChatStore } from '@/store/chat';
|
|
14
13
|
import { useFileStore } from '@/store/file';
|
|
14
|
+
import { useServerConfigStore } from '@/store/serverConfig';
|
|
15
|
+
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
|
|
15
16
|
import { useSessionStore } from '@/store/session';
|
|
16
17
|
import { useToolStore } from '@/store/tool';
|
|
17
18
|
import { useUserStore } from '@/store/user';
|
|
@@ -19,16 +20,13 @@ import { settingsSelectors, userProfileSelectors } from '@/store/user/selectors'
|
|
|
19
20
|
|
|
20
21
|
type SettingItemGroup = ItemGroup;
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
showAccessCodeConfig: boolean;
|
|
24
|
-
showOAuthLogin?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig, showOAuthLogin }) => {
|
|
23
|
+
const Common = memo(() => {
|
|
28
24
|
const { t } = useTranslation('setting');
|
|
29
25
|
const [form] = Form.useForm();
|
|
30
26
|
|
|
31
27
|
const isSignedIn = useUserStore((s) => s.isSignedIn);
|
|
28
|
+
const showAccessCodeConfig = useServerConfigStore(serverConfigSelectors.enabledAccessCode);
|
|
29
|
+
const showOAuthLogin = useServerConfigStore(serverConfigSelectors.enabledOAuthSSO);
|
|
32
30
|
const user = useUserStore(userProfileSelectors.userProfile, isEqual);
|
|
33
31
|
|
|
34
32
|
const [clearSessions, clearSessionGroups] = useSessionStore((s) => [
|
|
@@ -42,7 +40,12 @@ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig, showOAuthLogin
|
|
|
42
40
|
const [removeAllFiles] = useFileStore((s) => [s.removeAllFiles]);
|
|
43
41
|
const removeAllPlugins = useToolStore((s) => s.removeAllPlugins);
|
|
44
42
|
const settings = useUserStore(settingsSelectors.currentSettings, isEqual);
|
|
45
|
-
const [setSettings, resetSettings] = useUserStore((s) => [
|
|
43
|
+
const [setSettings, resetSettings, signIn, signOut] = useUserStore((s) => [
|
|
44
|
+
s.setSettings,
|
|
45
|
+
s.resetSettings,
|
|
46
|
+
s.openLogin,
|
|
47
|
+
s.logout,
|
|
48
|
+
]);
|
|
46
49
|
|
|
47
50
|
const { message, modal } = App.useApp();
|
|
48
51
|
|
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
import { authEnv } from '@/config/auth';
|
|
2
|
-
import { getServerConfig } from '@/config/server';
|
|
3
|
-
|
|
4
1
|
import Common from './features/Common';
|
|
5
2
|
import Theme from './features/Theme';
|
|
6
3
|
|
|
7
4
|
const Page = () => {
|
|
8
|
-
const { SHOW_ACCESS_CODE_CONFIG } = getServerConfig();
|
|
9
|
-
|
|
10
5
|
return (
|
|
11
6
|
<>
|
|
12
7
|
<Theme />
|
|
13
|
-
<Common
|
|
14
|
-
showAccessCodeConfig={SHOW_ACCESS_CODE_CONFIG}
|
|
15
|
-
showOAuthLogin={authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH}
|
|
16
|
-
/>
|
|
8
|
+
<Common />
|
|
17
9
|
</>
|
|
18
10
|
);
|
|
19
11
|
};
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { Icon } from '@lobehub/ui';
|
|
2
2
|
import { App, Button } from 'antd';
|
|
3
3
|
import { ScanFace } from 'lucide-react';
|
|
4
|
-
import { signIn, signOut } from 'next-auth/react';
|
|
5
4
|
import { memo, useCallback } from 'react';
|
|
6
5
|
import { useTranslation } from 'react-i18next';
|
|
7
6
|
import { Center, Flexbox } from 'react-layout-kit';
|
|
8
7
|
|
|
9
|
-
import { useOAuthSession } from '@/hooks/useOAuthSession';
|
|
10
8
|
import { useChatStore } from '@/store/chat';
|
|
9
|
+
import { useUserStore } from '@/store/user';
|
|
10
|
+
import { authSelectors, userProfileSelectors } from '@/store/user/selectors';
|
|
11
11
|
|
|
12
12
|
import { FormAction } from './style';
|
|
13
13
|
|
|
14
14
|
const OAuthForm = memo<{ id: string }>(({ id }) => {
|
|
15
15
|
const { t } = useTranslation('error');
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const [signIn, signOut] = useUserStore((s) => [s.openLogin, s.logout]);
|
|
18
|
+
const user = useUserStore(userProfileSelectors.userProfile);
|
|
19
|
+
const isOAuthLoggedIn = useUserStore(authSelectors.isLoginWithAuth);
|
|
18
20
|
|
|
19
21
|
const [resend, deleteMessage] = useChatStore((s) => [s.regenerateMessage, s.deleteMessage]);
|
|
20
22
|
|
|
@@ -38,7 +40,7 @@ const OAuthForm = memo<{ id: string }>(({ id }) => {
|
|
|
38
40
|
avatar={isOAuthLoggedIn ? '✅' : '🕵️♂️'}
|
|
39
41
|
description={
|
|
40
42
|
isOAuthLoggedIn
|
|
41
|
-
? `${t('unlock.oauth.welcome')} ${user?.
|
|
43
|
+
? `${t('unlock.oauth.welcome')} ${user?.fullName || ''}`
|
|
42
44
|
: t('unlock.oauth.description')
|
|
43
45
|
}
|
|
44
46
|
title={isOAuthLoggedIn ? t('unlock.oauth.success') : t('unlock.oauth.title')}
|
|
@@ -30,6 +30,7 @@ describe('UserAvatar', () => {
|
|
|
30
30
|
|
|
31
31
|
act(() => {
|
|
32
32
|
useUserStore.setState({
|
|
33
|
+
enableAuth: () => true,
|
|
33
34
|
isSignedIn: true,
|
|
34
35
|
user: { avatar: mockAvatar, id: 'abc', username: mockUsername },
|
|
35
36
|
});
|
|
@@ -45,7 +46,11 @@ describe('UserAvatar', () => {
|
|
|
45
46
|
const mockUsername = 'testuser';
|
|
46
47
|
|
|
47
48
|
act(() => {
|
|
48
|
-
useUserStore.setState({
|
|
49
|
+
useUserStore.setState({
|
|
50
|
+
enableAuth: () => true,
|
|
51
|
+
isSignedIn: true,
|
|
52
|
+
user: { id: 'bbb', username: mockUsername },
|
|
53
|
+
});
|
|
49
54
|
});
|
|
50
55
|
|
|
51
56
|
render(<UserAvatar />);
|
|
@@ -54,7 +59,7 @@ describe('UserAvatar', () => {
|
|
|
54
59
|
|
|
55
60
|
it('should show LobeChat and default avatar when the user is not logged in and enable auth', () => {
|
|
56
61
|
act(() => {
|
|
57
|
-
useUserStore.setState({ isSignedIn: false, user: undefined });
|
|
62
|
+
useUserStore.setState({ enableAuth: () => true, isSignedIn: false, user: undefined });
|
|
58
63
|
});
|
|
59
64
|
|
|
60
65
|
render(<UserAvatar />);
|
|
@@ -67,7 +72,7 @@ describe('UserAvatar', () => {
|
|
|
67
72
|
it('should show LobeChat and default avatar when the user is not logged in and disabled auth', () => {
|
|
68
73
|
enableAuth = false;
|
|
69
74
|
act(() => {
|
|
70
|
-
useUserStore.setState({ isSignedIn: false, user: undefined });
|
|
75
|
+
useUserStore.setState({ enableAuth: () => false, isSignedIn: false, user: undefined });
|
|
71
76
|
});
|
|
72
77
|
|
|
73
78
|
render(<UserAvatar />);
|
|
@@ -64,7 +64,7 @@ afterEach(() => {
|
|
|
64
64
|
describe('useMenu', () => {
|
|
65
65
|
it('should provide correct menu items when user is logged in with auth', () => {
|
|
66
66
|
act(() => {
|
|
67
|
-
useUserStore.setState({ isSignedIn: true });
|
|
67
|
+
useUserStore.setState({ isSignedIn: true, enableAuth: () => true });
|
|
68
68
|
});
|
|
69
69
|
enableAuth = true;
|
|
70
70
|
enableClerk = false;
|
|
@@ -104,7 +104,7 @@ describe('useMenu', () => {
|
|
|
104
104
|
|
|
105
105
|
it('should provide correct menu items when user is logged in without auth', () => {
|
|
106
106
|
act(() => {
|
|
107
|
-
useUserStore.setState({ isSignedIn: false });
|
|
107
|
+
useUserStore.setState({ isSignedIn: false, enableAuth: () => false });
|
|
108
108
|
});
|
|
109
109
|
enableAuth = false;
|
|
110
110
|
|
|
@@ -123,7 +123,7 @@ describe('useMenu', () => {
|
|
|
123
123
|
|
|
124
124
|
it('should provide correct menu items when user is not logged in', () => {
|
|
125
125
|
act(() => {
|
|
126
|
-
useUserStore.setState({ isSignedIn: false });
|
|
126
|
+
useUserStore.setState({ isSignedIn: false, enableAuth: () => true });
|
|
127
127
|
});
|
|
128
128
|
enableAuth = true;
|
|
129
129
|
|
|
@@ -13,6 +13,7 @@ import { parseAgentConfig } from './parseDefaultAgent';
|
|
|
13
13
|
|
|
14
14
|
export const getServerGlobalConfig = () => {
|
|
15
15
|
const {
|
|
16
|
+
ACCESS_CODES,
|
|
16
17
|
ENABLE_LANGFUSE,
|
|
17
18
|
|
|
18
19
|
DEFAULT_AGENT_CONFIG,
|
|
@@ -49,6 +50,7 @@ export const getServerGlobalConfig = () => {
|
|
|
49
50
|
config: parseAgentConfig(DEFAULT_AGENT_CONFIG),
|
|
50
51
|
},
|
|
51
52
|
|
|
53
|
+
enabledAccessCode: ACCESS_CODES?.length > 0,
|
|
52
54
|
enabledOAuthSSO: enableNextAuth,
|
|
53
55
|
languageModel: {
|
|
54
56
|
anthropic: {
|
|
@@ -6,6 +6,7 @@ export const featureFlagsSelectors = (s: ServerConfigStore) =>
|
|
|
6
6
|
mapFeatureFlagsEnvToState(s.featureFlags);
|
|
7
7
|
|
|
8
8
|
export const serverConfigSelectors = {
|
|
9
|
+
enabledAccessCode: (s: ServerConfigStore) => !!s.serverConfig?.enabledAccessCode,
|
|
9
10
|
enabledOAuthSSO: (s: ServerConfigStore) => s.serverConfig.enabledOAuthSSO,
|
|
10
11
|
enabledTelemetryChat: (s: ServerConfigStore) => s.serverConfig.telemetry.langfuse || false,
|
|
11
12
|
isMobile: (s: ServerConfigStore) => s.isMobile || false,
|
|
@@ -43,6 +43,16 @@ afterEach(() => {
|
|
|
43
43
|
enableClerk = false;
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Mock nextauth 库相关方法
|
|
48
|
+
*/
|
|
49
|
+
vi.mock('next-auth/react', async () => {
|
|
50
|
+
return {
|
|
51
|
+
signIn: vi.fn(),
|
|
52
|
+
signOut: vi.fn(),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
46
56
|
describe('createAuthSlice', () => {
|
|
47
57
|
describe('refreshUserConfig', () => {
|
|
48
58
|
it('should refresh user config', async () => {
|
|
@@ -162,6 +172,32 @@ describe('createAuthSlice', () => {
|
|
|
162
172
|
|
|
163
173
|
expect(clerkSignOutMock).not.toHaveBeenCalled();
|
|
164
174
|
});
|
|
175
|
+
|
|
176
|
+
it('should call next-auth signOut when NextAuth is enabled', async () => {
|
|
177
|
+
useUserStore.setState({ enabledNextAuth: () => true });
|
|
178
|
+
|
|
179
|
+
const { result } = renderHook(() => useUserStore());
|
|
180
|
+
|
|
181
|
+
await act(async () => {
|
|
182
|
+
await result.current.logout();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const { signOut } = await import('next-auth/react');
|
|
186
|
+
|
|
187
|
+
expect(signOut).toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should not call next-auth signOut when NextAuth is disabled', async () => {
|
|
191
|
+
const { result } = renderHook(() => useUserStore());
|
|
192
|
+
|
|
193
|
+
await act(async () => {
|
|
194
|
+
await result.current.logout();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const { signOut } = await import('next-auth/react');
|
|
198
|
+
|
|
199
|
+
expect(signOut).not.toHaveBeenCalled();
|
|
200
|
+
});
|
|
165
201
|
});
|
|
166
202
|
|
|
167
203
|
describe('openLogin', () => {
|
|
@@ -190,6 +226,31 @@ describe('createAuthSlice', () => {
|
|
|
190
226
|
|
|
191
227
|
expect(clerkSignInMock).not.toHaveBeenCalled();
|
|
192
228
|
});
|
|
229
|
+
|
|
230
|
+
it('should call next-auth signIn when NextAuth is enabled', async () => {
|
|
231
|
+
useUserStore.setState({ enabledNextAuth: () => true });
|
|
232
|
+
|
|
233
|
+
const { result } = renderHook(() => useUserStore());
|
|
234
|
+
|
|
235
|
+
await act(async () => {
|
|
236
|
+
await result.current.openLogin();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const { signIn } = await import('next-auth/react');
|
|
240
|
+
|
|
241
|
+
expect(signIn).toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
it('should not call next-auth signIn when NextAuth is disabled', async () => {
|
|
244
|
+
const { result } = renderHook(() => useUserStore());
|
|
245
|
+
|
|
246
|
+
await act(async () => {
|
|
247
|
+
await result.current.openLogin();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const { signIn } = await import('next-auth/react');
|
|
251
|
+
|
|
252
|
+
expect(signIn).not.toHaveBeenCalled();
|
|
253
|
+
});
|
|
193
254
|
});
|
|
194
255
|
|
|
195
256
|
describe('openUserProfile', () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import useSWR, { SWRResponse, mutate } from 'swr';
|
|
2
2
|
import { StateCreator } from 'zustand/vanilla';
|
|
3
3
|
|
|
4
|
-
import { enableClerk
|
|
4
|
+
import { enableClerk } from '@/const/auth';
|
|
5
5
|
import { UserConfig, userService } from '@/services/user';
|
|
6
6
|
import { switchLang } from '@/utils/client/switchLang';
|
|
7
7
|
import { setNamespace } from '@/utils/storeDebug';
|
|
@@ -13,8 +13,9 @@ const n = setNamespace('auth');
|
|
|
13
13
|
const USER_CONFIG_FETCH_KEY = 'fetchUserConfig';
|
|
14
14
|
|
|
15
15
|
export interface UserAuthAction {
|
|
16
|
+
enableAuth: () => boolean;
|
|
17
|
+
enabledNextAuth: () => boolean;
|
|
16
18
|
getUserConfig: () => void;
|
|
17
|
-
login: () => Promise<void>;
|
|
18
19
|
/**
|
|
19
20
|
* universal logout method
|
|
20
21
|
*/
|
|
@@ -24,8 +25,8 @@ export interface UserAuthAction {
|
|
|
24
25
|
*/
|
|
25
26
|
openLogin: () => Promise<void>;
|
|
26
27
|
openUserProfile: () => Promise<void>;
|
|
27
|
-
refreshUserConfig: () => Promise<void>;
|
|
28
28
|
|
|
29
|
+
refreshUserConfig: () => Promise<void>;
|
|
29
30
|
useFetchUserConfig: (initServer: boolean) => SWRResponse<UserConfig | undefined>;
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -35,13 +36,15 @@ export const createAuthSlice: StateCreator<
|
|
|
35
36
|
[],
|
|
36
37
|
UserAuthAction
|
|
37
38
|
> = (set, get) => ({
|
|
39
|
+
enableAuth: () => {
|
|
40
|
+
return enableClerk || get()?.enabledNextAuth();
|
|
41
|
+
},
|
|
42
|
+
enabledNextAuth: () => {
|
|
43
|
+
return !!get()?.serverConfig.enabledOAuthSSO;
|
|
44
|
+
},
|
|
38
45
|
getUserConfig: () => {
|
|
39
46
|
console.log(n('userconfig'));
|
|
40
47
|
},
|
|
41
|
-
login: async () => {
|
|
42
|
-
// TODO: 针对开启 next-auth 的场景,需要在这里调用登录方法
|
|
43
|
-
console.log(n('login'));
|
|
44
|
-
},
|
|
45
48
|
logout: async () => {
|
|
46
49
|
if (enableClerk) {
|
|
47
50
|
get().clerkSignOut?.({ redirectUrl: location.toString() });
|
|
@@ -49,9 +52,10 @@ export const createAuthSlice: StateCreator<
|
|
|
49
52
|
return;
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
const enableNextAuth = get().enabledNextAuth();
|
|
52
56
|
if (enableNextAuth) {
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
const { signOut } = await import('next-auth/react');
|
|
58
|
+
signOut();
|
|
55
59
|
}
|
|
56
60
|
},
|
|
57
61
|
openLogin: async () => {
|
|
@@ -63,20 +67,19 @@ export const createAuthSlice: StateCreator<
|
|
|
63
67
|
return;
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
const enableNextAuth = get().enabledNextAuth();
|
|
66
71
|
if (enableNextAuth) {
|
|
67
|
-
|
|
72
|
+
const { signIn } = await import('next-auth/react');
|
|
73
|
+
signIn();
|
|
68
74
|
}
|
|
69
75
|
},
|
|
76
|
+
|
|
70
77
|
openUserProfile: async () => {
|
|
71
78
|
if (enableClerk) {
|
|
72
79
|
get().clerkOpenUserProfile?.();
|
|
73
80
|
|
|
74
81
|
return;
|
|
75
82
|
}
|
|
76
|
-
|
|
77
|
-
if (enableNextAuth) {
|
|
78
|
-
// TODO: 针对开启 next-auth 的场景,需要在这里调用打开 profile 页
|
|
79
|
-
}
|
|
80
83
|
},
|
|
81
84
|
refreshUserConfig: async () => {
|
|
82
85
|
await mutate([USER_CONFIG_FETCH_KEY, true]);
|
|
@@ -84,7 +87,6 @@ export const createAuthSlice: StateCreator<
|
|
|
84
87
|
// when get the user config ,refresh the model provider list to the latest
|
|
85
88
|
get().refreshModelProviderList();
|
|
86
89
|
},
|
|
87
|
-
|
|
88
90
|
useFetchUserConfig: (initServer) =>
|
|
89
91
|
useSWR<UserConfig | undefined>(
|
|
90
92
|
[USER_CONFIG_FETCH_KEY, initServer],
|
|
@@ -31,6 +31,7 @@ describe('userProfileSelectors', () => {
|
|
|
31
31
|
const store: UserStore = {
|
|
32
32
|
isSignedIn: false,
|
|
33
33
|
user: null,
|
|
34
|
+
enableAuth: () => false,
|
|
34
35
|
} as unknown as UserStore;
|
|
35
36
|
|
|
36
37
|
expect(userProfileSelectors.nickName(store)).toBe('userPanel.defaultNickname');
|
|
@@ -43,6 +44,7 @@ describe('userProfileSelectors', () => {
|
|
|
43
44
|
const store: UserStore = {
|
|
44
45
|
isSignedIn: true,
|
|
45
46
|
user: { fullName: 'John Doe' },
|
|
47
|
+
enableAuth: () => true,
|
|
46
48
|
} as UserStore;
|
|
47
49
|
|
|
48
50
|
expect(userProfileSelectors.nickName(store)).toBe('John Doe');
|
|
@@ -52,6 +54,7 @@ describe('userProfileSelectors', () => {
|
|
|
52
54
|
const store: UserStore = {
|
|
53
55
|
isSignedIn: true,
|
|
54
56
|
user: { username: 'johndoe' },
|
|
57
|
+
enableAuth: () => true,
|
|
55
58
|
} as UserStore;
|
|
56
59
|
|
|
57
60
|
expect(userProfileSelectors.nickName(store)).toBe('johndoe');
|
|
@@ -60,7 +63,11 @@ describe('userProfileSelectors', () => {
|
|
|
60
63
|
it('should return anonymous nickname when not signed in', () => {
|
|
61
64
|
enableAuth = true;
|
|
62
65
|
|
|
63
|
-
const store: UserStore = {
|
|
66
|
+
const store: UserStore = {
|
|
67
|
+
enableAuth: () => true,
|
|
68
|
+
isSignedIn: false,
|
|
69
|
+
user: null,
|
|
70
|
+
} as unknown as UserStore;
|
|
64
71
|
|
|
65
72
|
expect(userProfileSelectors.nickName(store)).toBe('userPanel.anonymousNickName');
|
|
66
73
|
expect(t).toHaveBeenCalledWith('userPanel.anonymousNickName', { ns: 'common' });
|
|
@@ -74,6 +81,7 @@ describe('userProfileSelectors', () => {
|
|
|
74
81
|
const store: UserStore = {
|
|
75
82
|
isSignedIn: false,
|
|
76
83
|
user: null,
|
|
84
|
+
enableAuth: () => false,
|
|
77
85
|
} as unknown as UserStore;
|
|
78
86
|
|
|
79
87
|
expect(userProfileSelectors.username(store)).toBe('LobeChat');
|
|
@@ -83,13 +91,18 @@ describe('userProfileSelectors', () => {
|
|
|
83
91
|
const store: UserStore = {
|
|
84
92
|
isSignedIn: true,
|
|
85
93
|
user: { username: 'johndoe' },
|
|
94
|
+
enableAuth: () => true,
|
|
86
95
|
} as UserStore;
|
|
87
96
|
|
|
88
97
|
expect(userProfileSelectors.username(store)).toBe('johndoe');
|
|
89
98
|
});
|
|
90
99
|
|
|
91
100
|
it('should return "anonymous" when not signed in', () => {
|
|
92
|
-
const store: UserStore = {
|
|
101
|
+
const store: UserStore = {
|
|
102
|
+
enableAuth: () => true,
|
|
103
|
+
isSignedIn: false,
|
|
104
|
+
user: null,
|
|
105
|
+
} as unknown as UserStore;
|
|
93
106
|
|
|
94
107
|
expect(userProfileSelectors.username(store)).toBe('anonymous');
|
|
95
108
|
});
|
|
@@ -103,6 +116,7 @@ describe('authSelectors', () => {
|
|
|
103
116
|
|
|
104
117
|
const store: UserStore = {
|
|
105
118
|
isSignedIn: false,
|
|
119
|
+
enableAuth: () => false,
|
|
106
120
|
} as UserStore;
|
|
107
121
|
|
|
108
122
|
expect(authSelectors.isLogin(store)).toBe(true);
|
|
@@ -111,6 +125,7 @@ describe('authSelectors', () => {
|
|
|
111
125
|
it('should return true when signed in', () => {
|
|
112
126
|
const store: UserStore = {
|
|
113
127
|
isSignedIn: true,
|
|
128
|
+
enableAuth: () => true,
|
|
114
129
|
} as UserStore;
|
|
115
130
|
|
|
116
131
|
expect(authSelectors.isLogin(store)).toBe(true);
|
|
@@ -119,6 +134,7 @@ describe('authSelectors', () => {
|
|
|
119
134
|
it('should return false when not signed in and auth is enabled', () => {
|
|
120
135
|
const store: UserStore = {
|
|
121
136
|
isSignedIn: false,
|
|
137
|
+
enableAuth: () => true,
|
|
122
138
|
} as UserStore;
|
|
123
139
|
|
|
124
140
|
expect(authSelectors.isLogin(store)).toBe(false);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { t } from 'i18next';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { enableClerk } from '@/const/auth';
|
|
4
4
|
import { UserStore } from '@/store/user';
|
|
5
5
|
import { LobeUser } from '@/types/user';
|
|
6
6
|
|
|
7
7
|
const DEFAULT_USERNAME = 'LobeChat';
|
|
8
8
|
|
|
9
9
|
const nickName = (s: UserStore) => {
|
|
10
|
-
if (!enableAuth) return t('userPanel.defaultNickname', { ns: 'common' });
|
|
10
|
+
if (!s.enableAuth()) return t('userPanel.defaultNickname', { ns: 'common' });
|
|
11
11
|
|
|
12
12
|
if (s.isSignedIn) return s.user?.fullName || s.user?.username;
|
|
13
13
|
|
|
@@ -15,7 +15,7 @@ const nickName = (s: UserStore) => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
const username = (s: UserStore) => {
|
|
18
|
-
if (!enableAuth) return DEFAULT_USERNAME;
|
|
18
|
+
if (!s.enableAuth()) return DEFAULT_USERNAME;
|
|
19
19
|
|
|
20
20
|
if (s.isSignedIn) return s.user?.username;
|
|
21
21
|
|
|
@@ -35,7 +35,7 @@ export const userProfileSelectors = {
|
|
|
35
35
|
*/
|
|
36
36
|
const isLogin = (s: UserStore) => {
|
|
37
37
|
// 如果没有开启鉴权,说明不需要登录,默认是登录态
|
|
38
|
-
if (!enableAuth) return true;
|
|
38
|
+
if (!s.enableAuth()) return true;
|
|
39
39
|
|
|
40
40
|
return s.isSignedIn;
|
|
41
41
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type DefaultSession } from 'next-auth';
|
|
2
|
+
|
|
3
|
+
declare module 'next-auth' {
|
|
4
|
+
/**
|
|
5
|
+
* Returned by `useSession`, `auth`, contains information about the active session.
|
|
6
|
+
*/
|
|
7
|
+
interface Session {
|
|
8
|
+
user: {
|
|
9
|
+
firstName?: string;
|
|
10
|
+
} & DefaultSession['user'];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* More types can be extends here
|
|
14
|
+
* ref: https://authjs.dev/getting-started/typescript
|
|
15
|
+
*/
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declare module '@auth/core/jwt' {
|
|
19
|
+
/** Returned by the `jwt` callback and `auth`, when using JWT sessions */
|
|
20
|
+
interface JWT {
|
|
21
|
+
userId: string;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -15,6 +15,7 @@ export interface ServerModelProviderConfig {
|
|
|
15
15
|
|
|
16
16
|
export interface GlobalServerConfig {
|
|
17
17
|
defaultAgent?: DeepPartial<GlobalDefaultAgent>;
|
|
18
|
+
enabledAccessCode?: boolean;
|
|
18
19
|
enabledOAuthSSO?: boolean;
|
|
19
20
|
languageModel?: Partial<Record<GlobalLLMProviderKey, ServerModelProviderConfig>>;
|
|
20
21
|
telemetry: {
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { User } from '@auth/core/types';
|
|
2
|
-
import { SessionContextValue, useSession } from 'next-auth/react';
|
|
3
|
-
import { useMemo } from 'react';
|
|
4
|
-
|
|
5
|
-
interface OAuthSession {
|
|
6
|
-
isOAuthLoggedIn: boolean;
|
|
7
|
-
user?: User | null;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const useOAuthSession = () => {
|
|
11
|
-
let authSession: SessionContextValue | null;
|
|
12
|
-
try {
|
|
13
|
-
// refs: https://github.com/lobehub/lobe-chat/pull/1286
|
|
14
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
15
|
-
authSession = useSession();
|
|
16
|
-
} catch {
|
|
17
|
-
authSession = null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const { data: session, status } = authSession || {};
|
|
21
|
-
const isOAuthLoggedIn = (status === 'authenticated' && session && !!session.user) || false;
|
|
22
|
-
|
|
23
|
-
return useMemo<OAuthSession>(() => ({ isOAuthLoggedIn, user: session?.user }), [session, status]);
|
|
24
|
-
};
|