@lobehub/chat 0.124.3 → 0.125.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 +25 -0
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/docs/Deployment/Authentication.md +72 -0
- package/docs/Deployment/Authentication.zh-CN.md +72 -0
- package/docs/Deployment/Environment-Variable.md +50 -0
- package/docs/Deployment/Environment-Variable.zh-CN.md +50 -0
- package/docs/Development/Authentication.md +85 -0
- package/docs/Development/Authentication.zh-CN.md +83 -0
- package/locales/ar/common.json +1 -0
- package/locales/ar/error.json +6 -0
- package/locales/ar/setting.json +16 -0
- package/locales/de-DE/common.json +1 -0
- package/locales/de-DE/error.json +6 -0
- package/locales/de-DE/setting.json +16 -0
- package/locales/en-US/common.json +1 -0
- package/locales/en-US/error.json +6 -0
- package/locales/en-US/setting.json +16 -0
- package/locales/es-ES/common.json +1 -0
- package/locales/es-ES/error.json +6 -0
- package/locales/es-ES/setting.json +16 -0
- package/locales/fr-FR/common.json +1 -0
- package/locales/fr-FR/error.json +6 -0
- package/locales/fr-FR/setting.json +16 -0
- package/locales/it-IT/common.json +1 -0
- package/locales/it-IT/error.json +6 -0
- package/locales/it-IT/setting.json +16 -0
- package/locales/ja-JP/common.json +1 -0
- package/locales/ja-JP/error.json +6 -0
- package/locales/ja-JP/setting.json +16 -0
- package/locales/ko-KR/common.json +1 -0
- package/locales/ko-KR/error.json +6 -0
- package/locales/ko-KR/setting.json +16 -0
- package/locales/nl-NL/common.json +1 -0
- package/locales/nl-NL/error.json +6 -0
- package/locales/nl-NL/setting.json +16 -0
- package/locales/pl-PL/common.json +1 -0
- package/locales/pl-PL/error.json +6 -0
- package/locales/pl-PL/setting.json +16 -0
- package/locales/pt-BR/common.json +1 -0
- package/locales/pt-BR/error.json +6 -0
- package/locales/pt-BR/setting.json +16 -0
- package/locales/ru-RU/common.json +1 -0
- package/locales/ru-RU/error.json +6 -0
- package/locales/ru-RU/setting.json +16 -0
- package/locales/tr-TR/common.json +1 -0
- package/locales/tr-TR/error.json +6 -0
- package/locales/tr-TR/setting.json +16 -0
- package/locales/vi-VN/common.json +1 -0
- package/locales/vi-VN/error.json +6 -0
- package/locales/vi-VN/setting.json +16 -0
- package/locales/zh-CN/common.json +1 -0
- package/locales/zh-CN/error.json +6 -0
- package/locales/zh-CN/setting.json +16 -0
- package/locales/zh-TW/common.json +1 -0
- package/locales/zh-TW/error.json +6 -0
- package/locales/zh-TW/setting.json +16 -0
- package/package.json +2 -1
- package/src/app/api/auth.ts +7 -1
- package/src/app/api/chat/[provider]/route.ts +5 -3
- package/src/app/api/chat/auth.ts +11 -3
- package/src/app/api/chat/google/route.ts +5 -3
- package/src/app/api/config/route.ts +9 -2
- package/src/app/api/oauth/[...nextauth]/route.ts +3 -0
- package/src/app/api/oauth/next-auth.ts +21 -0
- package/src/app/api/openai/createBizOpenAI/index.ts +3 -2
- package/src/app/api/plugin/gateway/route.ts +7 -4
- package/src/app/settings/common/Common.tsx +39 -1
- package/src/app/settings/common/page.tsx +2 -2
- package/src/app/settings/page.tsx +2 -2
- package/src/chains/summaryTitle.ts +1 -1
- package/src/config/server/app.ts +5 -0
- package/src/const/auth.ts +2 -0
- package/src/const/fetch.ts +5 -1
- package/src/features/ChatInput/ActionBar/Clear.tsx +6 -8
- package/src/features/Conversation/Error/InvalidAccessCode.tsx +28 -10
- package/src/features/Conversation/Error/OAuthForm.tsx +95 -0
- package/src/layout/GlobalLayout/index.tsx +10 -7
- package/src/locales/default/common.ts +1 -0
- package/src/locales/default/error.ts +6 -0
- package/src/locales/default/setting.ts +16 -0
- package/src/middleware.ts +25 -0
- package/src/store/global/slices/common/initialState.ts +0 -1
- package/src/store/global/slices/common/selectors.ts +1 -0
- package/src/types/settings/index.ts +1 -0
- package/tsconfig.json +8 -8
|
@@ -263,6 +263,22 @@
|
|
|
263
263
|
"placeholder": "請輸入訪問密碼",
|
|
264
264
|
"title": "訪問密碼"
|
|
265
265
|
},
|
|
266
|
+
"oauth": {
|
|
267
|
+
"info": {
|
|
268
|
+
"desc": "已登錄",
|
|
269
|
+
"title": "帳戶資訊"
|
|
270
|
+
},
|
|
271
|
+
"signin": {
|
|
272
|
+
"action": "登錄",
|
|
273
|
+
"desc": "使用 SSO 登錄以解鎖應用",
|
|
274
|
+
"title": "登錄帳號"
|
|
275
|
+
},
|
|
276
|
+
"signout": {
|
|
277
|
+
"action": "退出登錄",
|
|
278
|
+
"confirm": "確認退出?",
|
|
279
|
+
"success": "退出登錄成功"
|
|
280
|
+
}
|
|
281
|
+
},
|
|
266
282
|
"title": "系統設定"
|
|
267
283
|
},
|
|
268
284
|
"settingTTS": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.125.0",
|
|
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",
|
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
"modern-screenshot": "^4",
|
|
106
106
|
"nanoid": "^5",
|
|
107
107
|
"next": "^14.1",
|
|
108
|
+
"next-auth": "beta",
|
|
108
109
|
"numeral": "^2.0.6",
|
|
109
110
|
"nuqs": "^1.15.4",
|
|
110
111
|
"openai": "^4.22",
|
package/src/app/api/auth.ts
CHANGED
|
@@ -4,9 +4,15 @@ import { ChatErrorType } from '@/types/fetch';
|
|
|
4
4
|
interface AuthConfig {
|
|
5
5
|
accessCode?: string | null;
|
|
6
6
|
apiKey?: string | null;
|
|
7
|
+
oauthAuthorized?: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export const checkAuth = ({ apiKey, accessCode }: AuthConfig) => {
|
|
10
|
+
export const checkAuth = ({ apiKey, accessCode, oauthAuthorized }: AuthConfig) => {
|
|
11
|
+
// If authorized by oauth
|
|
12
|
+
if (oauthAuthorized) {
|
|
13
|
+
return { auth: true };
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
const { ACCESS_CODES } = getServerConfig();
|
|
11
17
|
|
|
12
18
|
// if apiKey exist
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getPreferredRegion } from '@/app/api/config';
|
|
2
2
|
import { createErrorResponse } from '@/app/api/errorResponse';
|
|
3
|
-
import { LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
|
3
|
+
import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/const/auth';
|
|
4
4
|
import {
|
|
5
5
|
AgentInitErrorPayload,
|
|
6
6
|
AgentRuntimeError,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { ChatErrorType } from '@/types/fetch';
|
|
11
11
|
import { ChatStreamPayload } from '@/types/openai/chat';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { checkAuthMethod, getJWTPayload } from '../auth';
|
|
14
14
|
import AgentRuntime from './agentRuntime';
|
|
15
15
|
|
|
16
16
|
export const runtime = 'edge';
|
|
@@ -25,11 +25,13 @@ export const POST = async (req: Request, { params }: { params: { provider: strin
|
|
|
25
25
|
try {
|
|
26
26
|
// get Authorization from header
|
|
27
27
|
const authorization = req.headers.get(LOBE_CHAT_AUTH_HEADER);
|
|
28
|
+
const oauthAuthorized = !!req.headers.get(OAUTH_AUTHORIZED);
|
|
29
|
+
|
|
28
30
|
if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
|
29
31
|
|
|
30
32
|
// check the Auth With payload
|
|
31
33
|
const payload = await getJWTPayload(authorization);
|
|
32
|
-
|
|
34
|
+
checkAuthMethod(payload.accessCode, payload.apiKey, oauthAuthorized);
|
|
33
35
|
|
|
34
36
|
const body = await req.clone().json();
|
|
35
37
|
agentRuntime = await AgentRuntime.initializeWithUserPayload(params.provider, payload, {
|
package/src/app/api/chat/auth.ts
CHANGED
|
@@ -31,14 +31,22 @@ export const getJWTPayload = async (token: string): Promise<JWTPayload> => {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Check if the provided access code is valid
|
|
34
|
+
* Check if the provided access code is valid, a user API key should be used or the OAuth 2 header is provided.
|
|
35
35
|
*
|
|
36
36
|
* @param {string} accessCode - The access code to check.
|
|
37
37
|
* @param {string} apiKey - The user API key.
|
|
38
|
+
* @param {boolean} oauthAuthorized - Whether the OAuth 2 header is provided.
|
|
38
39
|
* @throws {AgentRuntimeError} If the access code is invalid and no user API key is provided.
|
|
39
40
|
*/
|
|
40
|
-
export const
|
|
41
|
-
|
|
41
|
+
export const checkAuthMethod = (
|
|
42
|
+
accessCode?: string,
|
|
43
|
+
apiKey?: string,
|
|
44
|
+
oauthAuthorized?: boolean,
|
|
45
|
+
) => {
|
|
46
|
+
const { ACCESS_CODES, ENABLE_OAUTH_SSO } = getServerConfig();
|
|
47
|
+
|
|
48
|
+
// if OAuth 2 header is provided
|
|
49
|
+
if (ENABLE_OAUTH_SSO && oauthAuthorized) return;
|
|
42
50
|
|
|
43
51
|
// if apiKey exist
|
|
44
52
|
if (apiKey) return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createErrorResponse } from '@/app/api/errorResponse';
|
|
2
2
|
import { getServerConfig } from '@/config/server';
|
|
3
|
-
import { LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
|
3
|
+
import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/const/auth';
|
|
4
4
|
import {
|
|
5
5
|
AgentInitErrorPayload,
|
|
6
6
|
AgentRuntimeError,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { ChatErrorType } from '@/types/fetch';
|
|
12
12
|
import { ChatStreamPayload } from '@/types/openai/chat';
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { checkAuthMethod, getJWTPayload } from '../auth';
|
|
15
15
|
|
|
16
16
|
// due to the Chinese region does not support accessing Google
|
|
17
17
|
// we need to use proxy to access it
|
|
@@ -49,11 +49,13 @@ export const POST = async (req: Request) => {
|
|
|
49
49
|
try {
|
|
50
50
|
// get Authorization from header
|
|
51
51
|
const authorization = req.headers.get(LOBE_CHAT_AUTH_HEADER);
|
|
52
|
+
const oauthAuthorized = !!req.headers.get(OAUTH_AUTHORIZED);
|
|
53
|
+
|
|
52
54
|
if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
|
53
55
|
|
|
54
56
|
// check the Auth With payload
|
|
55
57
|
const payload = await getJWTPayload(authorization);
|
|
56
|
-
|
|
58
|
+
checkAuthMethod(payload.accessCode, payload.apiKey, oauthAuthorized);
|
|
57
59
|
|
|
58
60
|
const { GOOGLE_API_KEY } = getServerConfig();
|
|
59
61
|
const apiKey = payload?.apiKey || GOOGLE_API_KEY;
|
|
@@ -7,11 +7,18 @@ export const runtime = 'edge';
|
|
|
7
7
|
* get Server config to client
|
|
8
8
|
*/
|
|
9
9
|
export const GET = async () => {
|
|
10
|
-
const {
|
|
11
|
-
|
|
10
|
+
const {
|
|
11
|
+
CUSTOM_MODELS,
|
|
12
|
+
ENABLED_MOONSHOT,
|
|
13
|
+
ENABLED_ZHIPU,
|
|
14
|
+
ENABLED_AWS_BEDROCK,
|
|
15
|
+
ENABLED_GOOGLE,
|
|
16
|
+
ENABLE_OAUTH_SSO,
|
|
17
|
+
} = getServerConfig();
|
|
12
18
|
|
|
13
19
|
const config: GlobalServerConfig = {
|
|
14
20
|
customModelName: CUSTOM_MODELS,
|
|
21
|
+
enabledOAuthSSO: ENABLE_OAUTH_SSO,
|
|
15
22
|
languageModel: {
|
|
16
23
|
bedrock: { enabled: ENABLED_AWS_BEDROCK },
|
|
17
24
|
google: { enabled: ENABLED_GOOGLE },
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import NextAuth from 'next-auth';
|
|
2
|
+
import Auth0 from 'next-auth/providers/auth0';
|
|
3
|
+
|
|
4
|
+
import { getServerConfig } from '@/config/server';
|
|
5
|
+
|
|
6
|
+
const { AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_ISSUER } = getServerConfig();
|
|
7
|
+
|
|
8
|
+
const nextAuth = NextAuth({
|
|
9
|
+
providers: [
|
|
10
|
+
Auth0({
|
|
11
|
+
clientId: AUTH0_CLIENT_ID,
|
|
12
|
+
clientSecret: AUTH0_CLIENT_SECRET,
|
|
13
|
+
issuer: AUTH0_ISSUER,
|
|
14
|
+
}),
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const {
|
|
19
|
+
handlers: { GET, POST },
|
|
20
|
+
auth,
|
|
21
|
+
} = nextAuth;
|
|
@@ -14,9 +14,10 @@ import { createOpenai } from './createOpenai';
|
|
|
14
14
|
* if auth not pass ,just return error response
|
|
15
15
|
*/
|
|
16
16
|
export const createBizOpenAI = (req: Request, model: string): Response | OpenAI => {
|
|
17
|
-
const { apiKey, accessCode, endpoint, useAzure, apiVersion } =
|
|
17
|
+
const { apiKey, accessCode, endpoint, useAzure, apiVersion, oauthAuthorized } =
|
|
18
|
+
getOpenAIAuthFromRequest(req);
|
|
18
19
|
|
|
19
|
-
const result = checkAuth({ accessCode, apiKey });
|
|
20
|
+
const result = checkAuth({ accessCode, apiKey, oauthAuthorized });
|
|
20
21
|
|
|
21
22
|
if (!result.auth) {
|
|
22
23
|
return createErrorResponse(result.error as ErrorType);
|
|
@@ -7,12 +7,15 @@ import { ChatErrorType, ErrorType } from '@/types/fetch';
|
|
|
7
7
|
|
|
8
8
|
import { parserPluginSettings } from './settings';
|
|
9
9
|
|
|
10
|
-
const checkAuth = (accessCode: string | null) => {
|
|
11
|
-
const { ACCESS_CODES, PLUGIN_SETTINGS } = getServerConfig();
|
|
10
|
+
const checkAuth = (accessCode: string | null, oauthAuthorized: boolean | null) => {
|
|
11
|
+
const { ACCESS_CODES, PLUGIN_SETTINGS, ENABLE_OAUTH_SSO } = getServerConfig();
|
|
12
12
|
|
|
13
13
|
// if there is no plugin settings, just skip the auth
|
|
14
14
|
if (!PLUGIN_SETTINGS) return { auth: true };
|
|
15
15
|
|
|
16
|
+
// If authorized by oauth
|
|
17
|
+
if (oauthAuthorized && ENABLE_OAUTH_SSO) return { auth: true };
|
|
18
|
+
|
|
16
19
|
// if accessCode doesn't exist
|
|
17
20
|
if (!ACCESS_CODES.length) return { auth: true };
|
|
18
21
|
|
|
@@ -30,9 +33,9 @@ const defaultPluginSettings = parserPluginSettings(PLUGIN_SETTINGS);
|
|
|
30
33
|
const handler = createGatewayOnEdgeRuntime({ defaultPluginSettings, pluginsIndexUrl });
|
|
31
34
|
|
|
32
35
|
export const POST = async (req: Request) => {
|
|
33
|
-
const { accessCode } = getOpenAIAuthFromRequest(req);
|
|
36
|
+
const { accessCode, oauthAuthorized } = getOpenAIAuthFromRequest(req);
|
|
34
37
|
|
|
35
|
-
const result = checkAuth(accessCode);
|
|
38
|
+
const result = checkAuth(accessCode, oauthAuthorized);
|
|
36
39
|
|
|
37
40
|
if (!result.auth) {
|
|
38
41
|
return createErrorResponse(result.error as ErrorType);
|
|
@@ -2,6 +2,7 @@ import { Form, type ItemGroup, SelectWithImg, SliderWithInput } from '@lobehub/u
|
|
|
2
2
|
import { Form as AntForm, App, Button, Input, Select } from 'antd';
|
|
3
3
|
import isEqual from 'fast-deep-equal';
|
|
4
4
|
import { AppWindow, Monitor, Moon, Palette, Sun } from 'lucide-react';
|
|
5
|
+
import { signIn, signOut, useSession } from 'next-auth/react';
|
|
5
6
|
import { memo, useCallback, useEffect } from 'react';
|
|
6
7
|
import { useTranslation } from 'react-i18next';
|
|
7
8
|
|
|
@@ -23,12 +24,16 @@ type SettingItemGroup = ItemGroup;
|
|
|
23
24
|
|
|
24
25
|
export interface SettingsCommonProps {
|
|
25
26
|
showAccessCodeConfig: boolean;
|
|
27
|
+
showOAuthLogin: boolean;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig }) => {
|
|
30
|
+
const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig, showOAuthLogin }) => {
|
|
29
31
|
const { t } = useTranslation('setting');
|
|
30
32
|
const [form] = AntForm.useForm();
|
|
31
33
|
|
|
34
|
+
const { data: session, status } = useSession();
|
|
35
|
+
const isOAuthLoggedIn = status === 'authenticated' && session && session.user;
|
|
36
|
+
|
|
32
37
|
const [clearSessions, clearSessionGroups] = useSessionStore((s) => [
|
|
33
38
|
s.clearSessions,
|
|
34
39
|
s.clearSessionGroups,
|
|
@@ -49,6 +54,22 @@ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig }) => {
|
|
|
49
54
|
|
|
50
55
|
const { message, modal } = App.useApp();
|
|
51
56
|
|
|
57
|
+
const handleSignOut = useCallback(() => {
|
|
58
|
+
modal.confirm({
|
|
59
|
+
centered: true,
|
|
60
|
+
okButtonProps: { danger: true },
|
|
61
|
+
onOk: () => {
|
|
62
|
+
signOut();
|
|
63
|
+
message.success(t('settingSystem.oauth.signout.success'));
|
|
64
|
+
},
|
|
65
|
+
title: t('settingSystem.oauth.signout.confirm'),
|
|
66
|
+
});
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const handleSignIn = useCallback(() => {
|
|
70
|
+
signIn('auth0');
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
52
73
|
const handleReset = useCallback(() => {
|
|
53
74
|
modal.confirm({
|
|
54
75
|
centered: true,
|
|
@@ -196,6 +217,23 @@ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig }) => {
|
|
|
196
217
|
label: t('settingSystem.accessCode.title'),
|
|
197
218
|
name: 'password',
|
|
198
219
|
},
|
|
220
|
+
{
|
|
221
|
+
children: isOAuthLoggedIn ? (
|
|
222
|
+
<Button onClick={handleSignOut}>{t('settingSystem.oauth.signout.action')}</Button>
|
|
223
|
+
) : (
|
|
224
|
+
<Button onClick={handleSignIn} type="primary">
|
|
225
|
+
{t('settingSystem.oauth.signin.action')}
|
|
226
|
+
</Button>
|
|
227
|
+
),
|
|
228
|
+
desc: isOAuthLoggedIn
|
|
229
|
+
? `${session.user?.email} ${t('settingSystem.oauth.info.desc')}`
|
|
230
|
+
: t('settingSystem.oauth.signin.desc'),
|
|
231
|
+
hidden: !showOAuthLogin,
|
|
232
|
+
label: isOAuthLoggedIn
|
|
233
|
+
? t('settingSystem.oauth.info.title')
|
|
234
|
+
: t('settingSystem.oauth.signin.title'),
|
|
235
|
+
minWidth: undefined,
|
|
236
|
+
},
|
|
199
237
|
{
|
|
200
238
|
children: (
|
|
201
239
|
<Button danger onClick={handleReset} type="primary">
|
|
@@ -3,7 +3,7 @@ import { getServerConfig } from '@/config/server';
|
|
|
3
3
|
import Index from './index';
|
|
4
4
|
|
|
5
5
|
export default () => {
|
|
6
|
-
const { SHOW_ACCESS_CODE_CONFIG } = getServerConfig();
|
|
6
|
+
const { SHOW_ACCESS_CODE_CONFIG, ENABLE_OAUTH_SSO } = getServerConfig();
|
|
7
7
|
|
|
8
|
-
return <Index showAccessCodeConfig={SHOW_ACCESS_CODE_CONFIG} />;
|
|
8
|
+
return <Index showAccessCodeConfig={SHOW_ACCESS_CODE_CONFIG} showOAuthLogin={ENABLE_OAUTH_SSO} />;
|
|
9
9
|
};
|
|
@@ -9,9 +9,9 @@ const Page = () => {
|
|
|
9
9
|
|
|
10
10
|
const Page = mobile ? MobilePage : DesktopPage;
|
|
11
11
|
|
|
12
|
-
const { SHOW_ACCESS_CODE_CONFIG } = getServerConfig();
|
|
12
|
+
const { SHOW_ACCESS_CODE_CONFIG, ENABLE_OAUTH_SSO } = getServerConfig();
|
|
13
13
|
|
|
14
|
-
return <Page showAccessCodeConfig={SHOW_ACCESS_CODE_CONFIG} />;
|
|
14
|
+
return <Page showAccessCodeConfig={SHOW_ACCESS_CODE_CONFIG} showOAuthLogin={ENABLE_OAUTH_SSO} />;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export default Page;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { chatHelpers } from '@/store/chat/helpers';
|
|
2
2
|
import { globalHelpers } from '@/store/global/helpers';
|
|
3
3
|
import { LanguageModel } from '@/types/llm';
|
|
4
|
-
import {
|
|
4
|
+
import { ChatStreamPayload, OpenAIChatMessage } from '@/types/openai/chat';
|
|
5
5
|
|
|
6
6
|
export const chainSummaryTitle = async (
|
|
7
7
|
messages: OpenAIChatMessage[],
|
package/src/config/server/app.ts
CHANGED
|
@@ -47,5 +47,10 @@ export const getAppConfig = () => {
|
|
|
47
47
|
: 'https://chat-plugins.lobehub.com',
|
|
48
48
|
|
|
49
49
|
PLUGIN_SETTINGS: process.env.PLUGIN_SETTINGS,
|
|
50
|
+
|
|
51
|
+
ENABLE_OAUTH_SSO: !!process.env.ENABLE_OAUTH_SSO,
|
|
52
|
+
AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID || '',
|
|
53
|
+
AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET || '',
|
|
54
|
+
AUTH0_ISSUER: process.env.AUTH0_ISSUER || '',
|
|
50
55
|
};
|
|
51
56
|
};
|
package/src/const/auth.ts
CHANGED
package/src/const/fetch.ts
CHANGED
|
@@ -7,6 +7,8 @@ export const AZURE_OPENAI_API_VERSION = 'X-azure-openai-api-version';
|
|
|
7
7
|
|
|
8
8
|
export const LOBE_CHAT_ACCESS_CODE = 'X-lobe-chat-access-code';
|
|
9
9
|
|
|
10
|
+
export const OAUTH_AUTHORIZED = 'X-oauth-authorized';
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
13
|
* @deprecated
|
|
12
14
|
*/
|
|
@@ -16,8 +18,10 @@ export const getOpenAIAuthFromRequest = (req: Request) => {
|
|
|
16
18
|
const accessCode = req.headers.get(LOBE_CHAT_ACCESS_CODE);
|
|
17
19
|
const useAzureStr = req.headers.get(USE_AZURE_OPENAI);
|
|
18
20
|
const apiVersion = req.headers.get(AZURE_OPENAI_API_VERSION);
|
|
21
|
+
const oauthAuthorizedStr = req.headers.get(OAUTH_AUTHORIZED);
|
|
19
22
|
|
|
23
|
+
const oauthAuthorized = !!oauthAuthorizedStr;
|
|
20
24
|
const useAzure = !!useAzureStr;
|
|
21
25
|
|
|
22
|
-
return { accessCode, apiKey, apiVersion, endpoint, useAzure };
|
|
26
|
+
return { accessCode, apiKey, apiVersion, endpoint, oauthAuthorized, useAzure };
|
|
23
27
|
};
|
|
@@ -21,9 +21,11 @@ const Clear = memo(() => {
|
|
|
21
21
|
clearImageList();
|
|
22
22
|
}, []);
|
|
23
23
|
|
|
24
|
-
const actionTitle: any = confirmOpened
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const actionTitle: any = confirmOpened ? (
|
|
25
|
+
void 0
|
|
26
|
+
) : (
|
|
27
|
+
<HotKeys desc={t('clearCurrentMessages', { ns: 'chat' })} keys={hotkeys} />
|
|
28
|
+
);
|
|
27
29
|
|
|
28
30
|
return (
|
|
29
31
|
<Popconfirm
|
|
@@ -35,11 +37,7 @@ const Clear = memo(() => {
|
|
|
35
37
|
placement={'topRight'}
|
|
36
38
|
title={t('confirmClearCurrentMessages', { ns: 'chat' })}
|
|
37
39
|
>
|
|
38
|
-
<ActionIcon
|
|
39
|
-
icon={Eraser}
|
|
40
|
-
placement={'bottom'}
|
|
41
|
-
title={actionTitle}
|
|
42
|
-
/>
|
|
40
|
+
<ActionIcon icon={Eraser} placement={'bottom'} title={actionTitle} />
|
|
43
41
|
</Popconfirm>
|
|
44
42
|
);
|
|
45
43
|
});
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { Icon } from '@lobehub/ui';
|
|
2
2
|
import { Segmented } from 'antd';
|
|
3
|
-
import {
|
|
3
|
+
import { SegmentedLabeledOption } from 'antd/es/segmented';
|
|
4
|
+
import { AsteriskSquare, KeySquare, ScanFace } from 'lucide-react';
|
|
4
5
|
import { memo, useState } from 'react';
|
|
5
6
|
import { useTranslation } from 'react-i18next';
|
|
6
7
|
import { Flexbox } from 'react-layout-kit';
|
|
7
8
|
|
|
9
|
+
import { useGlobalStore } from '@/store/global';
|
|
10
|
+
import { commonSelectors } from '@/store/global/selectors';
|
|
11
|
+
|
|
8
12
|
import APIKeyForm from './APIKeyForm';
|
|
9
13
|
import AccessCodeForm from './AccessCodeForm';
|
|
14
|
+
import OAuthForm from './OAuthForm';
|
|
10
15
|
import { ErrorActionContainer } from './style';
|
|
11
16
|
|
|
12
17
|
enum Tab {
|
|
13
18
|
Api = 'api',
|
|
19
|
+
Oauth = 'oauth',
|
|
14
20
|
Password = 'password',
|
|
15
21
|
}
|
|
16
22
|
|
|
@@ -21,27 +27,39 @@ interface InvalidAccessCodeProps {
|
|
|
21
27
|
|
|
22
28
|
const InvalidAccessCode = memo<InvalidAccessCodeProps>(({ id, provider }) => {
|
|
23
29
|
const { t } = useTranslation('error');
|
|
24
|
-
const
|
|
30
|
+
const isEnabledOAuth = useGlobalStore(commonSelectors.enabledOAuthSSO);
|
|
31
|
+
const defaultTab = isEnabledOAuth ? Tab.Oauth : Tab.Password;
|
|
32
|
+
const [mode, setMode] = useState<Tab>(defaultTab);
|
|
25
33
|
|
|
26
34
|
return (
|
|
27
35
|
<ErrorActionContainer>
|
|
28
36
|
<Segmented
|
|
29
37
|
block
|
|
30
38
|
onChange={(value) => setMode(value as Tab)}
|
|
31
|
-
options={
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
options={
|
|
40
|
+
[
|
|
41
|
+
isEnabledOAuth
|
|
42
|
+
? {
|
|
43
|
+
icon: <Icon icon={ScanFace} />,
|
|
44
|
+
label: t('oauth', { ns: 'common' }),
|
|
45
|
+
value: Tab.Oauth,
|
|
46
|
+
}
|
|
47
|
+
: undefined,
|
|
48
|
+
{
|
|
49
|
+
icon: <Icon icon={AsteriskSquare} />,
|
|
50
|
+
label: t('unlock.tabs.password'),
|
|
51
|
+
value: Tab.Password,
|
|
52
|
+
},
|
|
53
|
+
{ icon: <Icon icon={KeySquare} />, label: t('unlock.tabs.apiKey'), value: Tab.Api },
|
|
54
|
+
].filter(Boolean) as SegmentedLabeledOption[]
|
|
55
|
+
}
|
|
39
56
|
style={{ width: '100%' }}
|
|
40
57
|
value={mode}
|
|
41
58
|
/>
|
|
42
59
|
<Flexbox gap={24}>
|
|
43
60
|
{mode === Tab.Password && <AccessCodeForm id={id} />}
|
|
44
61
|
{mode === Tab.Api && <APIKeyForm id={id} provider={provider} />}
|
|
62
|
+
{isEnabledOAuth && mode === Tab.Oauth && <OAuthForm id={id} />}
|
|
45
63
|
</Flexbox>
|
|
46
64
|
</ErrorActionContainer>
|
|
47
65
|
);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Icon } from '@lobehub/ui';
|
|
2
|
+
import { App, Button } from 'antd';
|
|
3
|
+
import { ScanFace } from 'lucide-react';
|
|
4
|
+
import { signIn, signOut, useSession } from 'next-auth/react';
|
|
5
|
+
import { memo, useCallback } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { Center, Flexbox } from 'react-layout-kit';
|
|
8
|
+
|
|
9
|
+
import { useChatStore } from '@/store/chat';
|
|
10
|
+
|
|
11
|
+
import { FormAction } from './style';
|
|
12
|
+
|
|
13
|
+
const OAuthForm = memo<{ id: string }>(({ id }) => {
|
|
14
|
+
const { t } = useTranslation('error');
|
|
15
|
+
|
|
16
|
+
const { data: session, status } = useSession();
|
|
17
|
+
const isOAuthLoggedIn = status === 'authenticated' && session && session.user;
|
|
18
|
+
|
|
19
|
+
const [resend, deleteMessage] = useChatStore((s) => [s.resendMessage, s.deleteMessage]);
|
|
20
|
+
|
|
21
|
+
const { message, modal } = App.useApp();
|
|
22
|
+
|
|
23
|
+
const handleSignOut = useCallback(() => {
|
|
24
|
+
modal.confirm({
|
|
25
|
+
centered: true,
|
|
26
|
+
okButtonProps: { danger: true },
|
|
27
|
+
onOk: () => {
|
|
28
|
+
signOut();
|
|
29
|
+
message.success(t('settingSystem.oauth.signout.success', { ns: 'setting' }));
|
|
30
|
+
},
|
|
31
|
+
title: t('settingSystem.oauth.signout.confirm', { ns: 'setting' }),
|
|
32
|
+
});
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Center gap={16} style={{ maxWidth: 300 }}>
|
|
37
|
+
<FormAction
|
|
38
|
+
avatar={isOAuthLoggedIn ? '✅' : '🕵️♂️'}
|
|
39
|
+
description={
|
|
40
|
+
isOAuthLoggedIn
|
|
41
|
+
? `${t('unlock.oauth.welcome')} ${session?.user?.name}`
|
|
42
|
+
: t('unlock.oauth.description')
|
|
43
|
+
}
|
|
44
|
+
title={isOAuthLoggedIn ? t('unlock.oauth.success') : t('unlock.oauth.title')}
|
|
45
|
+
>
|
|
46
|
+
{isOAuthLoggedIn ? (
|
|
47
|
+
<Button
|
|
48
|
+
block
|
|
49
|
+
icon={<Icon icon={ScanFace} />}
|
|
50
|
+
onClick={handleSignOut}
|
|
51
|
+
style={{ marginTop: 8 }}
|
|
52
|
+
>
|
|
53
|
+
{t('settingSystem.oauth.signout.action', { ns: 'setting' })}
|
|
54
|
+
</Button>
|
|
55
|
+
) : (
|
|
56
|
+
<Button
|
|
57
|
+
block
|
|
58
|
+
icon={<Icon icon={ScanFace} />}
|
|
59
|
+
loading={status === 'loading'}
|
|
60
|
+
onClick={() => signIn('auth0')}
|
|
61
|
+
style={{ marginTop: 8 }}
|
|
62
|
+
type={'primary'}
|
|
63
|
+
>
|
|
64
|
+
{t('oauth', { ns: 'common' })}
|
|
65
|
+
</Button>
|
|
66
|
+
)}
|
|
67
|
+
</FormAction>
|
|
68
|
+
<Flexbox gap={12} width={'100%'}>
|
|
69
|
+
{isOAuthLoggedIn ? (
|
|
70
|
+
<Button
|
|
71
|
+
block
|
|
72
|
+
onClick={() => {
|
|
73
|
+
resend(id);
|
|
74
|
+
deleteMessage(id);
|
|
75
|
+
}}
|
|
76
|
+
style={{ marginTop: 8 }}
|
|
77
|
+
type={'primary'}
|
|
78
|
+
>
|
|
79
|
+
{t('unlock.confirm')}
|
|
80
|
+
</Button>
|
|
81
|
+
) : (
|
|
82
|
+
<Button
|
|
83
|
+
onClick={() => {
|
|
84
|
+
deleteMessage(id);
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{t('unlock.closeMessage')}
|
|
88
|
+
</Button>
|
|
89
|
+
)}
|
|
90
|
+
</Flexbox>
|
|
91
|
+
</Center>
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export default OAuthForm;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { App } from 'antd';
|
|
4
4
|
import { createStyles } from 'antd-style';
|
|
5
5
|
import 'antd/dist/reset.css';
|
|
6
|
+
import { SessionProvider } from 'next-auth/react';
|
|
6
7
|
import dynamic from 'next/dynamic';
|
|
7
8
|
import { FC, PropsWithChildren, memo } from 'react';
|
|
8
9
|
|
|
@@ -47,13 +48,15 @@ interface GlobalLayoutProps extends AppThemeProps {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const GlobalLayout = ({ children, defaultLang, ...theme }: GlobalLayoutProps) => (
|
|
50
|
-
<
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
<SessionProvider basePath="/api/oauth">
|
|
52
|
+
<AppTheme {...theme}>
|
|
53
|
+
<Locale defaultLang={defaultLang}>
|
|
54
|
+
<StoreHydration />
|
|
55
|
+
<Container>{children}</Container>
|
|
56
|
+
<DebugUI />
|
|
57
|
+
</Locale>
|
|
58
|
+
</AppTheme>
|
|
59
|
+
</SessionProvider>
|
|
57
60
|
);
|
|
58
61
|
|
|
59
62
|
export default GlobalLayout;
|
|
@@ -110,6 +110,12 @@ export default {
|
|
|
110
110
|
},
|
|
111
111
|
closeMessage: '关闭提示',
|
|
112
112
|
confirm: '确认并重试',
|
|
113
|
+
oauth: {
|
|
114
|
+
description: '管理员已开启统一登录认证,点击下方按钮登录,即可解锁应用',
|
|
115
|
+
success: '登录成功',
|
|
116
|
+
title: '登录账号',
|
|
117
|
+
welcome: '欢迎你!',
|
|
118
|
+
},
|
|
113
119
|
password: {
|
|
114
120
|
description: '管理员已开启应用加密,输入应用密码后即可解锁应用。密码只需填写一次',
|
|
115
121
|
placeholder: '请输入密码',
|
|
@@ -265,6 +265,22 @@ export default {
|
|
|
265
265
|
placeholder: '请输入访问密码',
|
|
266
266
|
title: '访问密码',
|
|
267
267
|
},
|
|
268
|
+
oauth: {
|
|
269
|
+
info: {
|
|
270
|
+
desc: '已登录',
|
|
271
|
+
title: '账户信息',
|
|
272
|
+
},
|
|
273
|
+
signin: {
|
|
274
|
+
action: '登录',
|
|
275
|
+
desc: '使用 SSO 登录以解锁应用',
|
|
276
|
+
title: '登录账号',
|
|
277
|
+
},
|
|
278
|
+
signout: {
|
|
279
|
+
action: '退出登录',
|
|
280
|
+
confirm: '确认退出?',
|
|
281
|
+
success: '退出登录成功',
|
|
282
|
+
},
|
|
283
|
+
},
|
|
268
284
|
title: '系统设置',
|
|
269
285
|
},
|
|
270
286
|
settingTTS: {
|