forked from lobehub/lobe-chat
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ feat: support password auth and error (lobehub#22)
* ✨ feat: Server 端支持密码鉴权 & 完善错误码处理逻辑 * 🎨 fix: 修正 package.json 的引入问题 * 💬 style: 优化设置文案 * 💬 style: 优化错误文案 * ✨ feat: 支持 ChatList loading 渲染 * ✨ feat: 新增密码输入或 api key 输入组件 * 🌐 style: 修正 i18n 文案问题 * 🔧 chore: remove error lint rule * ✨ feat: 支持展示 OpenAI 的业务错误内容 * ✨ feat: 完成 OpenAI 报错内容展示优化
- Loading branch information
Showing
29 changed files
with
607 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
const config = require('@lobehub/lint').eslint; | ||
|
||
config.extends.push('plugin:@next/next/recommended'); | ||
//config.extends.push('plugin:@next/next/core-web-vitals'); | ||
|
||
config.rules['unicorn/no-negated-condition'] = 0; | ||
config.rules['unicorn/prefer-type-error'] = 0; | ||
|
||
module.exports = config; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
declare global { | ||
// eslint-disable-next-line @typescript-eslint/no-namespace | ||
namespace NodeJS { | ||
interface ProcessEnv { | ||
ACCESS_CODE?: string; | ||
OPENAI_API_KEY?: string; | ||
OPENAI_PROXY_URL?: string; | ||
} | ||
} | ||
} | ||
|
||
export const getServerConfig = () => { | ||
if (typeof process === 'undefined') { | ||
throw new Error('[Server Config] you are importing a nodejs-only module outside of nodejs'); | ||
} | ||
|
||
return { | ||
ACCESS_CODE: process.env.ACCESS_CODE, | ||
OPENAI_API_KEY: process.env.OPENAI_API_KEY, | ||
OPENAI_PROXY_URL: process.env.OPENAI_PROXY_URL, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
export const OPENAI_SERVICE_ERROR_CODE = 555; | ||
|
||
export const OPENAI_API_KEY_HEADER_KEY = 'X-OPENAI-API-KEY'; | ||
|
||
export const LOBE_CHAT_ACCESS_CODE = 'X-LOBE_CHAT_ACCESS_CODE'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
export default { | ||
response: { | ||
400: '很抱歉,服务器不明白您的请求,请确认您的请求参数是否正确', | ||
401: '很抱歉,服务器拒绝了您的请求,可能是因为您的权限不足或未提供有效的身份验证', | ||
403: '很抱歉,服务器拒绝了您的请求,您没有访问此内容的权限 ', | ||
404: '很抱歉,服务器找不到您请求的页面或资源,请确认您的 URL 是否正确', | ||
429: '很抱歉,您的请求太多,服务器有点累了,请稍后再试', | ||
500: '很抱歉,服务器似乎遇到了一些困难,暂时无法完成您的请求,请稍后再试', | ||
502: '很抱歉,服务器似乎迷失了方向,暂时无法提供服务,请稍后再试', | ||
503: '很抱歉,服务器当前无法处理您的请求,可能是由于过载或正在进行维护,请稍后再试', | ||
504: '很抱歉,服务器没有等到上游服务器的回应,请稍后再试', | ||
|
||
InvalidAccessCode: '密码不正确或为空,请输入正确的访问密码,或者添加自定义 OpenAI API Key', | ||
OpenAIBizError: '请求 OpenAI 服务出错,请根据以下信息排查或重试', | ||
}, | ||
unlock: { | ||
apikey: { | ||
description: '输入你的 OpenAI API Key 即可绕过密码验证。应用不会记录你的 API Key', | ||
title: '使用自定义 API Key', | ||
}, | ||
confirm: '确认并重试', | ||
password: { | ||
description: '管理员已开启应用加密,输入应用密码后即可解锁应用。密码只需填写一次', | ||
title: '输入密码解锁应用', | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { getServerConfig } from '@/config/server'; | ||
import { ErrorType } from '@/types/fetch'; | ||
|
||
interface AuthConfig { | ||
accessCode?: string | null; | ||
apiKey?: string | null; | ||
} | ||
|
||
export const checkAuth = ({ apiKey, accessCode }: AuthConfig) => { | ||
const { ACCESS_CODE } = getServerConfig(); | ||
|
||
// 如果存在 apiKey | ||
if (apiKey) { | ||
return { auth: true }; | ||
} | ||
|
||
// 如果不存在,则检查 accessCode | ||
if (!ACCESS_CODE) return { auth: true }; | ||
|
||
if (accessCode !== ACCESS_CODE) { | ||
return { auth: false, error: ErrorType.InvalidAccessCode }; | ||
} | ||
|
||
return { auth: true }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { ErrorResponse, ErrorType } from '@/types/fetch'; | ||
|
||
const getStatus = (errorType: ErrorType) => { | ||
switch (errorType) { | ||
case ErrorType.InvalidAccessCode: { | ||
return 401; | ||
} | ||
|
||
case ErrorType.OpenAIBizError: { | ||
return 577; | ||
} | ||
} | ||
}; | ||
|
||
export const createErrorResponse = (errorType: ErrorType, body?: any) => { | ||
const statusCode = getStatus(errorType); | ||
|
||
const data: ErrorResponse = { body, errorType }; | ||
|
||
return new Response(JSON.stringify(data), { status: statusCode }); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,23 @@ | ||
import { StreamingTextResponse } from 'ai'; | ||
|
||
import { OPENAI_API_KEY_HEADER_KEY } from '@/const/fetch'; | ||
import { LOBE_CHAT_ACCESS_CODE, OPENAI_API_KEY_HEADER_KEY } from '@/const/fetch'; | ||
import { ErrorType } from '@/types/fetch'; | ||
import { OpenAIStreamPayload } from '@/types/openai'; | ||
|
||
import { checkAuth } from './auth'; | ||
import { createErrorResponse } from './error'; | ||
import { createChatCompletion } from './openai'; | ||
|
||
export const runtime = 'edge'; | ||
|
||
export default async function handler(req: Request) { | ||
const payload = (await req.json()) as OpenAIStreamPayload; | ||
const apiKey = req.headers.get(OPENAI_API_KEY_HEADER_KEY); | ||
const accessCode = req.headers.get(LOBE_CHAT_ACCESS_CODE); | ||
|
||
const result = checkAuth({ accessCode, apiKey }); | ||
|
||
const stream = await createChatCompletion({ OPENAI_API_KEY: apiKey, payload }); | ||
if (!result.auth) { | ||
return createErrorResponse(result.error as ErrorType); | ||
} | ||
|
||
return new StreamingTextResponse(stream); | ||
return createChatCompletion({ OPENAI_API_KEY: apiKey, payload }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
src/pages/chat/[id]/Conversation/ChatList/Error/ApiKeyForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { Avatar } from '@lobehub/ui'; | ||
import { Button, Input } from 'antd'; | ||
import { memo } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Center, Flexbox } from 'react-layout-kit'; | ||
import { shallow } from 'zustand/shallow'; | ||
|
||
import { useSettings } from '@/store/settings'; | ||
|
||
import { useStyles } from './style'; | ||
|
||
const APIKeyForm = memo<{ onConfirm?: () => void }>(({ onConfirm }) => { | ||
const { t } = useTranslation('error'); | ||
const { styles, theme } = useStyles(); | ||
const [apiKey, setSettings] = useSettings( | ||
(s) => [s.settings.OPENAI_API_KEY, s.setSettings], | ||
shallow, | ||
); | ||
|
||
return ( | ||
<Center gap={16} style={{ maxWidth: 300 }}> | ||
<Avatar avatar={'🔑'} background={theme.colorText} gap={12} size={80} /> | ||
<Flexbox style={{ fontSize: 20 }}>{t('unlock.apikey.title')}</Flexbox> | ||
<Flexbox className={styles.desc}>{t('unlock.apikey.description')}</Flexbox> | ||
<Input | ||
onChange={(e) => { | ||
setSettings({ OPENAI_API_KEY: e.target.value }); | ||
}} | ||
placeholder={'sk-*****************************************'} | ||
type={'block'} | ||
value={apiKey} | ||
/> | ||
<Button block onClick={onConfirm} style={{ marginTop: 8 }} type={'primary'}> | ||
{t('unlock.confirm')} | ||
</Button> | ||
</Center> | ||
); | ||
}); | ||
|
||
export default APIKeyForm; |
23 changes: 23 additions & 0 deletions
23
src/pages/chat/[id]/Conversation/ChatList/Error/ErrorActionContainer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { createStyles } from 'antd-style'; | ||
import { ReactNode, memo } from 'react'; | ||
import { Center } from 'react-layout-kit'; | ||
|
||
const useStyles = createStyles(({ css, token }) => ({ | ||
container: css` | ||
background: ${token.colorBgContainer}; | ||
border: 1px solid ${token.colorSplit}; | ||
border-radius: 8px; | ||
`, | ||
})); | ||
|
||
const ErrorActionContainer = memo<{ children: ReactNode }>(({ children }) => { | ||
const { styles } = useStyles(); | ||
|
||
return ( | ||
<Center className={styles.container} gap={24} padding={24}> | ||
{children} | ||
</Center> | ||
); | ||
}); | ||
|
||
export default ErrorActionContainer; |
76 changes: 76 additions & 0 deletions
76
src/pages/chat/[id]/Conversation/ChatList/Error/InvalidAccess.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { Icon } from '@lobehub/ui'; | ||
import { Button, Segmented } from 'antd'; | ||
import { KeySquare, SquareAsterisk } from 'lucide-react'; | ||
import { memo, useState } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Flexbox } from 'react-layout-kit'; | ||
import { shallow } from 'zustand/shallow'; | ||
|
||
import { useSessionStore } from '@/store/session'; | ||
import { useSettings } from '@/store/settings'; | ||
|
||
import OtpInput from '../OTPInput'; | ||
import APIKeyForm from './ApiKeyForm'; | ||
import { ErrorActionContainer, FormAction } from './style'; | ||
|
||
const InvalidAccess = memo<{ id: string }>(({ id }) => { | ||
const { t } = useTranslation('error'); | ||
const [mode, setMode] = useState('password'); | ||
const [password, setSettings] = useSettings((s) => [s.settings.password, s.setSettings], shallow); | ||
const [resend, deleteMessage] = useSessionStore( | ||
(s) => [s.resendMessage, s.deleteMessage], | ||
shallow, | ||
); | ||
|
||
return ( | ||
<ErrorActionContainer> | ||
<Segmented | ||
block | ||
onChange={(value) => setMode(value as string)} | ||
options={[ | ||
{ icon: <Icon icon={SquareAsterisk} />, label: '密码', value: 'password' }, | ||
{ icon: <Icon icon={KeySquare} />, label: 'OpenAI API Key', value: 'api' }, | ||
]} | ||
style={{ width: '100%' }} | ||
value={mode} | ||
/> | ||
<Flexbox gap={24}> | ||
{mode === 'password' ? ( | ||
<> | ||
<FormAction | ||
avatar={'🗳'} | ||
description={t('unlock.password.description')} | ||
title={t('unlock.password.title')} | ||
> | ||
<OtpInput | ||
onChange={(e) => { | ||
setSettings({ password: e }); | ||
}} | ||
validationPattern={/[\dA-Za-z]/} | ||
value={password} | ||
/> | ||
</FormAction> | ||
<Button | ||
onClick={() => { | ||
resend(id); | ||
deleteMessage(id); | ||
}} | ||
type={'primary'} | ||
> | ||
{t('unlock.confirm')} | ||
</Button> | ||
</> | ||
) : ( | ||
<APIKeyForm | ||
onConfirm={() => { | ||
resend(id); | ||
deleteMessage(id); | ||
}} | ||
/> | ||
)} | ||
</Flexbox> | ||
</ErrorActionContainer> | ||
); | ||
}); | ||
|
||
export default InvalidAccess; |
Oops, something went wrong.