@lobehub/chat 0.124.3 → 0.126.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 (90) hide show
  1. package/CHANGELOG.md +58 -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 +75 -0
  7. package/docs/Deployment/Environment-Variable.zh-CN.md +76 -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/chat/settings/(desktop)/features/Header.tsx +1 -1
  69. package/src/app/settings/common/Common.tsx +39 -1
  70. package/src/app/settings/common/page.tsx +2 -2
  71. package/src/app/settings/page.tsx +2 -2
  72. package/src/chains/summaryTitle.ts +1 -1
  73. package/src/components/Analytics/Umami.tsx +15 -0
  74. package/src/components/Analytics/index.tsx +4 -1
  75. package/src/config/client.ts +10 -0
  76. package/src/config/server/app.ts +5 -0
  77. package/src/const/auth.ts +2 -0
  78. package/src/const/fetch.ts +5 -1
  79. package/src/features/ChatInput/ActionBar/Clear.tsx +6 -8
  80. package/src/features/Conversation/Error/InvalidAccessCode.tsx +28 -10
  81. package/src/features/Conversation/Error/OAuthForm.tsx +95 -0
  82. package/src/layout/GlobalLayout/index.tsx +10 -7
  83. package/src/locales/default/common.ts +1 -0
  84. package/src/locales/default/error.ts +6 -0
  85. package/src/locales/default/setting.ts +16 -0
  86. package/src/middleware.ts +25 -0
  87. package/src/store/global/slices/common/initialState.ts +0 -1
  88. package/src/store/global/slices/common/selectors.ts +1 -0
  89. package/src/types/settings/index.ts +1 -0
  90. package/tsconfig.json +8 -8
@@ -263,6 +263,22 @@
263
263
  "placeholder": "Nhập mật khẩu truy cập",
264
264
  "title": "Mật khẩu truy cập"
265
265
  },
266
+ "oauth": {
267
+ "info": {
268
+ "desc": "Đã đăng nhập",
269
+ "title": "Thông tin tài khoản"
270
+ },
271
+ "signin": {
272
+ "action": "Đăng nhập",
273
+ "desc": "Đăng nhập bằng SSO để mở khóa ứng dụng",
274
+ "title": "Đăng nhập tài khoản"
275
+ },
276
+ "signout": {
277
+ "action": "Đăng xuất",
278
+ "confirm": "Xác nhận đăng xuất?",
279
+ "success": "Đăng xuất thành công"
280
+ }
281
+ },
266
282
  "title": "Cài đặt hệ thống"
267
283
  },
268
284
  "settingTTS": {
@@ -108,6 +108,7 @@
108
108
  "zhipu": "智谱AI"
109
109
  },
110
110
  "noDescription": "暂无描述",
111
+ "oauth": "SSO 登录",
111
112
  "ok": "确定",
112
113
  "password": "密码",
113
114
  "pin": "置顶",
@@ -94,6 +94,12 @@
94
94
  },
95
95
  "closeMessage": "关闭提示",
96
96
  "confirm": "确认并重试",
97
+ "oauth": {
98
+ "description": "管理员已开启统一登录认证,点击下方按钮登录,即可解锁应用",
99
+ "success": "登录成功",
100
+ "title": "登录账号",
101
+ "welcome": "欢迎你!"
102
+ },
97
103
  "password": {
98
104
  "description": "管理员已开启应用加密,输入应用密码后即可解锁应用。密码只需填写一次",
99
105
  "placeholder": "请输入密码",
@@ -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": {
@@ -108,6 +108,7 @@
108
108
  "zhipu": "智譜AI"
109
109
  },
110
110
  "noDescription": "暫無描述",
111
+ "oauth": "單一登錄(SSO)",
111
112
  "ok": "確定",
112
113
  "password": "密碼",
113
114
  "pin": "置頂",
@@ -94,6 +94,12 @@
94
94
  },
95
95
  "closeMessage": "關閉提示",
96
96
  "confirm": "確認並重試",
97
+ "oauth": {
98
+ "description": "管理員已開啟統一登錄認證,點擊下方按鈕登錄,即可解鎖應用",
99
+ "success": "登錄成功",
100
+ "title": "登錄帳號",
101
+ "welcome": "歡迎你!"
102
+ },
97
103
  "password": {
98
104
  "description": "管理員已啟用應用程式加密,輸入應用程式密碼後即可解鎖應用程式。密碼只需填寫一次。",
99
105
  "placeholder": "請輸入密碼",
@@ -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.126.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);
@@ -13,7 +13,7 @@ const Header = memo(() => {
13
13
  return (
14
14
  <ChatHeader
15
15
  left={<ChatHeaderTitle title={t('header.session')} />}
16
- onBackClick={() => router.push(pathString('/chat', { hash: location.hash }))}
16
+ onBackClick={() => router.push(pathString('/chat', { search: location.search }))}
17
17
  right={<HeaderContent />}
18
18
  showBackButton
19
19
  />
@@ -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[],
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+
3
+ import Script from 'next/script';
4
+ import { memo } from 'react';
5
+
6
+ import { getClientConfig } from '@/config/client';
7
+
8
+ const { UMAMI_SCRIPT_URL, UMAMI_WEBSITE_ID } = getClientConfig();
9
+
10
+ const UmamiAnalytics = memo(
11
+ () =>
12
+ UMAMI_WEBSITE_ID && <Script data-website-id={UMAMI_WEBSITE_ID} defer src={UMAMI_SCRIPT_URL} />,
13
+ );
14
+
15
+ export default UmamiAnalytics;
@@ -5,8 +5,10 @@ import { getClientConfig } from '@/config/client';
5
5
  const Vercel = dynamic(() => import('./Vercel'), { ssr: false });
6
6
  const Plausible = dynamic(() => import('./Plausible'), { ssr: false });
7
7
  const Posthog = dynamic(() => import('./Posthog'), { ssr: false });
8
+ const Umami = dynamic(() => import('./Umami'), { ssr: false });
8
9
 
9
- const { ANALYTICS_VERCEL, ANALYTICS_POSTHOG, ANALYTICS_PLAUSIBLE } = getClientConfig();
10
+ const { ANALYTICS_VERCEL, ANALYTICS_POSTHOG, ANALYTICS_PLAUSIBLE, ANALYTICS_UMAMI } =
11
+ getClientConfig();
10
12
 
11
13
  const Analytics = () => {
12
14
  return (
@@ -14,6 +16,7 @@ const Analytics = () => {
14
16
  {ANALYTICS_VERCEL && <Vercel />}
15
17
  {ANALYTICS_PLAUSIBLE && <Plausible />}
16
18
  {ANALYTICS_POSTHOG && <Posthog />}
19
+ {ANALYTICS_UMAMI && <Umami />}
17
20
  </>
18
21
  );
19
22
  };
@@ -24,6 +24,10 @@ declare global {
24
24
  NEXT_PUBLIC_POSTHOG_HOST: string;
25
25
  NEXT_PUBLIC_POSTHOG_DEBUG: string;
26
26
 
27
+ NEXT_PUBLIC_ANALYTICS_UMAMI?: string;
28
+ NEXT_PUBLIC_UMAMI_SCRIPT_URL?: string;
29
+ NEXT_PUBLIC_UMAMI_WEBSITE_ID?: string;
30
+
27
31
  NEXT_PUBLIC_I18N_DEBUG: string;
28
32
  NEXT_PUBLIC_I18N_DEBUG_BROWSER: string;
29
33
  NEXT_PUBLIC_I18N_DEBUG_SERVER: string;
@@ -50,6 +54,12 @@ export const getClientConfig = () => ({
50
54
  POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
51
55
  POSTHOG_DEBUG: process.env.NEXT_PUBLIC_POSTHOG_DEBUG === '1',
52
56
 
57
+ // Umami Analytics
58
+ ANALYTICS_UMAMI: process.env.NEXT_PUBLIC_ANALYTICS_UMAMI === '1',
59
+ UMAMI_SCRIPT_URL:
60
+ process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL || 'https://analytics.umami.is/script.js',
61
+ UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
62
+
53
63
  // i18n debug mode
54
64
  I18N_DEBUG: process.env.NEXT_PUBLIC_I18N_DEBUG === '1',
55
65
  I18N_DEBUG_BROWSER: process.env.NEXT_PUBLIC_I18N_DEBUG_BROWSER === '1',
@@ -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
  );