@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +8 -8
  3. package/README.zh-CN.md +8 -8
  4. package/docs/Deployment/Authentication.md +72 -0
  5. package/docs/Deployment/Authentication.zh-CN.md +72 -0
  6. package/docs/Deployment/Environment-Variable.md +50 -0
  7. package/docs/Deployment/Environment-Variable.zh-CN.md +50 -0
  8. package/docs/Development/Authentication.md +85 -0
  9. package/docs/Development/Authentication.zh-CN.md +83 -0
  10. package/locales/ar/common.json +1 -0
  11. package/locales/ar/error.json +6 -0
  12. package/locales/ar/setting.json +16 -0
  13. package/locales/de-DE/common.json +1 -0
  14. package/locales/de-DE/error.json +6 -0
  15. package/locales/de-DE/setting.json +16 -0
  16. package/locales/en-US/common.json +1 -0
  17. package/locales/en-US/error.json +6 -0
  18. package/locales/en-US/setting.json +16 -0
  19. package/locales/es-ES/common.json +1 -0
  20. package/locales/es-ES/error.json +6 -0
  21. package/locales/es-ES/setting.json +16 -0
  22. package/locales/fr-FR/common.json +1 -0
  23. package/locales/fr-FR/error.json +6 -0
  24. package/locales/fr-FR/setting.json +16 -0
  25. package/locales/it-IT/common.json +1 -0
  26. package/locales/it-IT/error.json +6 -0
  27. package/locales/it-IT/setting.json +16 -0
  28. package/locales/ja-JP/common.json +1 -0
  29. package/locales/ja-JP/error.json +6 -0
  30. package/locales/ja-JP/setting.json +16 -0
  31. package/locales/ko-KR/common.json +1 -0
  32. package/locales/ko-KR/error.json +6 -0
  33. package/locales/ko-KR/setting.json +16 -0
  34. package/locales/nl-NL/common.json +1 -0
  35. package/locales/nl-NL/error.json +6 -0
  36. package/locales/nl-NL/setting.json +16 -0
  37. package/locales/pl-PL/common.json +1 -0
  38. package/locales/pl-PL/error.json +6 -0
  39. package/locales/pl-PL/setting.json +16 -0
  40. package/locales/pt-BR/common.json +1 -0
  41. package/locales/pt-BR/error.json +6 -0
  42. package/locales/pt-BR/setting.json +16 -0
  43. package/locales/ru-RU/common.json +1 -0
  44. package/locales/ru-RU/error.json +6 -0
  45. package/locales/ru-RU/setting.json +16 -0
  46. package/locales/tr-TR/common.json +1 -0
  47. package/locales/tr-TR/error.json +6 -0
  48. package/locales/tr-TR/setting.json +16 -0
  49. package/locales/vi-VN/common.json +1 -0
  50. package/locales/vi-VN/error.json +6 -0
  51. package/locales/vi-VN/setting.json +16 -0
  52. package/locales/zh-CN/common.json +1 -0
  53. package/locales/zh-CN/error.json +6 -0
  54. package/locales/zh-CN/setting.json +16 -0
  55. package/locales/zh-TW/common.json +1 -0
  56. package/locales/zh-TW/error.json +6 -0
  57. package/locales/zh-TW/setting.json +16 -0
  58. package/package.json +2 -1
  59. package/src/app/api/auth.ts +7 -1
  60. package/src/app/api/chat/[provider]/route.ts +5 -3
  61. package/src/app/api/chat/auth.ts +11 -3
  62. package/src/app/api/chat/google/route.ts +5 -3
  63. package/src/app/api/config/route.ts +9 -2
  64. package/src/app/api/oauth/[...nextauth]/route.ts +3 -0
  65. package/src/app/api/oauth/next-auth.ts +21 -0
  66. package/src/app/api/openai/createBizOpenAI/index.ts +3 -2
  67. package/src/app/api/plugin/gateway/route.ts +7 -4
  68. package/src/app/settings/common/Common.tsx +39 -1
  69. package/src/app/settings/common/page.tsx +2 -2
  70. package/src/app/settings/page.tsx +2 -2
  71. package/src/chains/summaryTitle.ts +1 -1
  72. package/src/config/server/app.ts +5 -0
  73. package/src/const/auth.ts +2 -0
  74. package/src/const/fetch.ts +5 -1
  75. package/src/features/ChatInput/ActionBar/Clear.tsx +6 -8
  76. package/src/features/Conversation/Error/InvalidAccessCode.tsx +28 -10
  77. package/src/features/Conversation/Error/OAuthForm.tsx +95 -0
  78. package/src/layout/GlobalLayout/index.tsx +10 -7
  79. package/src/locales/default/common.ts +1 -0
  80. package/src/locales/default/error.ts +6 -0
  81. package/src/locales/default/setting.ts +16 -0
  82. package/src/middleware.ts +25 -0
  83. package/src/store/global/slices/common/initialState.ts +0 -1
  84. package/src/store/global/slices/common/selectors.ts +1 -0
  85. package/src/types/settings/index.ts +1 -0
  86. 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.124.3",
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",
@@ -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 { checkPasswordOrUseUserApiKey, getJWTPayload } from '../auth';
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
- checkPasswordOrUseUserApiKey(payload.accessCode, payload.apiKey);
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, {
@@ -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 or if a user API key should be used.
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 checkPasswordOrUseUserApiKey = (accessCode?: string, apiKey?: string) => {
41
- const { ACCESS_CODES } = getServerConfig();
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 { checkPasswordOrUseUserApiKey, getJWTPayload } from '../auth';
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
- checkPasswordOrUseUserApiKey(payload.accessCode, payload.apiKey);
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 { CUSTOM_MODELS, ENABLED_MOONSHOT, ENABLED_ZHIPU, ENABLED_AWS_BEDROCK, ENABLED_GOOGLE } =
11
- getServerConfig();
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,3 @@
1
+ export { GET, POST } from '../next-auth';
2
+
3
+ export const runtime = 'edge'; // optional
@@ -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 } = getOpenAIAuthFromRequest(req);
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 { OpenAIChatMessage, ChatStreamPayload } from '@/types/openai/chat';
4
+ import { ChatStreamPayload, OpenAIChatMessage } from '@/types/openai/chat';
5
5
 
6
6
  export const chainSummaryTitle = async (
7
7
  messages: OpenAIChatMessage[],
@@ -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
@@ -1,5 +1,7 @@
1
1
  export const LOBE_CHAT_AUTH_HEADER = 'X-lobe-chat-auth';
2
2
 
3
+ export const OAUTH_AUTHORIZED = 'X-oauth-authorized';
4
+
3
5
  export const JWT_SECRET_KEY = 'LobeHub · LobeChat';
4
6
  export const NON_HTTP_PREFIX = 'http_nosafe';
5
7
 
@@ -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
- ? void 0
26
- : <HotKeys desc={t('clearCurrentMessages', { ns: 'chat' })} keys={hotkeys} />
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 { AsteriskSquare, KeySquare } from 'lucide-react';
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 [mode, setMode] = useState<Tab>(Tab.Password);
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
- icon: <Icon icon={AsteriskSquare} />,
34
- label: t('unlock.tabs.password'),
35
- value: Tab.Password,
36
- },
37
- { icon: <Icon icon={KeySquare} />, label: t('unlock.tabs.apiKey'), value: Tab.Api },
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
- <AppTheme {...theme}>
51
- <Locale defaultLang={defaultLang}>
52
- <StoreHydration />
53
- <Container>{children}</Container>
54
- <DebugUI />
55
- </Locale>
56
- </AppTheme>
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;
@@ -108,6 +108,7 @@ export default {
108
108
  zhipu: '智谱AI',
109
109
  },
110
110
  noDescription: '暂无描述',
111
+ oauth: 'SSO 登录',
111
112
  ok: '确定',
112
113
  password: '密码',
113
114
  pin: '置顶',
@@ -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: {