From 2c899cf00eb729cc4aad2a13a74d2cabea9e7200 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sun, 26 Mar 2023 06:53:40 +0000 Subject: [PATCH] feat: #2 add access control by access code --- README.md | 20 +++++++++++++ app/api/access.ts | 16 +++++++++++ app/components/settings.tsx | 25 +++++++++++++++-- app/layout.tsx | 26 +++++++++++++---- app/locales/cn.ts | 8 ++++++ app/locales/en.ts | 9 ++++++ app/requests.ts | 26 +++++++++++++++-- app/store/access.ts | 30 ++++++++++++++++++++ app/store/app.ts | 1 + app/store/index.ts | 1 + app/styles/globals.scss | 4 ++- app/utils.ts | 23 +++++++++------ middleware.ts | 30 ++++++++++++++++++++ package.json | 2 ++ tsconfig.json | 2 +- yarn.lock | 56 +++++++++---------------------------- 16 files changed, 216 insertions(+), 63 deletions(-) create mode 100644 app/api/access.ts create mode 100644 app/store/access.ts create mode 100644 middleware.ts diff --git a/README.md b/README.md index bef81a1e..6323a2c5 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ One-Click to deploy your own ChatGPT web UI. 如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。 推荐你按照下列步骤重新部署: + - 删除掉原先的 repo; - fork 本项目; - 前往 vercel 控制台,删除掉原先的 project,然后新建 project,选择你刚刚 fork 出来的项目重新进行部署即可; @@ -65,6 +66,7 @@ One-Click to deploy your own ChatGPT web UI. If you have deployed your own project with just one click following the steps above, you may encounter the issue of "Updates Available" constantly showing up. This is because Vercel will create a new project for you by default instead of forking this project, resulting in the inability to detect updates correctly. We recommend that you follow the steps below to re-deploy: + - Delete the original repo; - Fork this project; - Go to the Vercel dashboard, delete the original project, then create a new project and select the project you just forked to redeploy; @@ -74,6 +76,24 @@ This project will be continuously maintained. If you want to keep the code repos You can star or watch this project or follow author to get release notifictions in time. +## 访问控制 Access Control + +本项目提供有限的权限控制功能,请在环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义控制码: + +``` +code1,code2,code3 +``` + +增加或修改该环境变量后,请重新部署项目使改动生效。 + +This project provides limited access control. Please add an environment variable named `CODE` on the environment variables page. The value should be a custom control code separated by comma like this: + +``` +code1,code2,code3 +``` + +After adding or modifying this environment variable, please redeploy the project for the changes to take effect. + ## 开发 Development 点击下方按钮,开始二次开发: diff --git a/app/api/access.ts b/app/api/access.ts new file mode 100644 index 00000000..13ada214 --- /dev/null +++ b/app/api/access.ts @@ -0,0 +1,16 @@ +import md5 from "spark-md5"; + +export function getAccessCodes(): Set { + const code = process.env.CODE; + + try { + const codes = (code?.split(",") ?? []) + .filter((v) => !!v) + .map((v) => md5.hash(v.trim())); + return new Set(codes); + } catch (e) { + return new Set(); + } +} + +export const ACCESS_CODES = getAccessCodes(); diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 9623c3f9..ec22a1ab 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef, useMemo } from "react"; import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react"; @@ -17,6 +17,7 @@ import { Theme, ALL_MODELS, useUpdateStore, + useAccessStore, } from "../store"; import { Avatar } from "./home"; @@ -38,7 +39,7 @@ function SettingItem(props: {
{props.subTitle}
)} -
{props.children}
+ {props.children} ); } @@ -71,6 +72,12 @@ export function Settings(props: { closeSettings: () => void }) { checkUpdate(); }, []); + const accessStore = useAccessStore(); + const enabledAccessControl = useMemo( + () => accessStore.enabledAccessControl(), + [] + ); + return ( <>
@@ -232,6 +239,20 @@ export function Settings(props: { closeSettings: () => void }) {
+ + { + accessStore.updateCode(e.currentTarget.value); + }} + > + + 0 ? "enabled" : "disabled", + }; + + return ( + <> + {Object.entries(metas).map(([k, v]) => ( + + ))} + + ); +} export default function RootLayout({ children, @@ -26,7 +42,7 @@ export default function RootLayout({ name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" /> - + diff --git a/app/locales/cn.ts b/app/locales/cn.ts index a6ff4556..ab6af587 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -1,5 +1,8 @@ const cn = { WIP: "该功能仍在开发中……", + Error: { + Unauthorized: "现在是未授权状态,请在设置页填写授权码。", + }, ChatItem: { ChatItemCount: (count: number) => `${count} 条对话`, }, @@ -65,6 +68,11 @@ const cn = { Title: "历史消息长度压缩阈值", SubTitle: "当未压缩的历史消息超过该值时,将进行压缩", }, + AccessCode: { + Title: "访问码", + SubTitle: "现在是受控访问状态", + Placeholder: "请输入访问码", + }, Model: "模型 (model)", Temperature: { Title: "随机性 (temperature)", diff --git a/app/locales/en.ts b/app/locales/en.ts index 50597014..85a0caff 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -2,6 +2,10 @@ import type { LocaleType } from "./index"; const en: LocaleType = { WIP: "WIP...", + Error: { + Unauthorized: + "Unauthorized access, please enter access code in settings page.", + }, ChatItem: { ChatItemCount: (count: number) => `${count} messages`, }, @@ -69,6 +73,11 @@ const en: LocaleType = { SubTitle: "Will compress if uncompressed messages length exceeds the value", }, + AccessCode: { + Title: "Access Code", + SubTitle: "Access control enabled", + Placeholder: "Need Access Code", + }, Model: "Model", Temperature: { Title: "Temperature", diff --git a/app/requests.ts b/app/requests.ts index d502dc15..484fbb93 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -1,5 +1,6 @@ import type { ChatRequest, ChatReponse } from "./api/chat/typing"; -import { filterConfig, Message, ModelConfig } from "./store"; +import { filterConfig, Message, ModelConfig, useAccessStore } from "./store"; +import Locale from "./locales"; const TIME_OUT_MS = 30000; @@ -26,6 +27,17 @@ const makeRequestParam = ( }; }; +function getHeaders() { + const accessStore = useAccessStore.getState(); + let headers: Record = {}; + + if (accessStore.enabledAccessControl()) { + headers["access-code"] = accessStore.accessCode; + } + + return headers; +} + export async function requestChat(messages: Message[]) { const req: ChatRequest = makeRequestParam(messages, { filterBot: true }); @@ -33,6 +45,7 @@ export async function requestChat(messages: Message[]) { method: "POST", headers: { "Content-Type": "application/json", + ...getHeaders(), }, body: JSON.stringify(req), }); @@ -69,6 +82,7 @@ export async function requestChatStream( method: "POST", headers: { "Content-Type": "application/json", + ...getHeaders(), }, body: JSON.stringify(req), signal: controller.signal, @@ -82,6 +96,8 @@ export async function requestChatStream( controller.abort(); }; + console.log(res); + if (res.ok) { const reader = res.body?.getReader(); const decoder = new TextDecoder(); @@ -102,14 +118,18 @@ export async function requestChatStream( } } + finish(); + } else if (res.status === 401) { + console.error("Anauthorized"); + responseText = Locale.Error.Unauthorized; finish(); } else { console.error("Stream Error"); options?.onError(new Error("Stream Error")); } } catch (err) { - console.error("NetWork Error"); - options?.onError(new Error("NetWork Error")); + console.error("NetWork Error", err); + options?.onError(err as Error); } } diff --git a/app/store/access.ts b/app/store/access.ts new file mode 100644 index 00000000..4ec2111c --- /dev/null +++ b/app/store/access.ts @@ -0,0 +1,30 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { queryMeta } from "../utils"; + +export interface AccessControlStore { + accessCode: string; + + updateCode: (_: string) => void; + enabledAccessControl: () => boolean; +} + +export const ACCESS_KEY = "access-control"; + +export const useAccessStore = create()( + persist( + (set, get) => ({ + accessCode: "", + enabledAccessControl() { + return queryMeta("access") === "enabled"; + }, + updateCode(code: string) { + set((state) => ({ accessCode: code })); + }, + }), + { + name: ACCESS_KEY, + version: 1, + } + ) +); diff --git a/app/store/app.ts b/app/store/app.ts index 91abb2c8..3c4fcded 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -308,6 +308,7 @@ export const useChatStore = create()( onMessage(content, done) { if (done) { botMessage.streaming = false; + botMessage.content = content; get().onNewMessage(botMessage); } else { botMessage.content = content; diff --git a/app/store/index.ts b/app/store/index.ts index b247a7cd..3bdb58ca 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -1,2 +1,3 @@ export * from "./app"; export * from "./update"; +export * from "./access"; diff --git a/app/styles/globals.scss b/app/styles/globals.scss index fde2239a..e7d35226 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -167,7 +167,8 @@ input[type="range"]::-webkit-slider-thumb:hover { width: 24px; } -input[type="number"] { +input[type="number"], +input[type="text"] { appearance: none; border-radius: 10px; border: var(--border-in-light); @@ -176,6 +177,7 @@ input[type="number"] { background: var(--white); color: var(--black); padding: 0 10px; + max-width: 50%; } div.math { diff --git a/app/utils.ts b/app/utils.ts index bcf45d9c..1fe68896 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -57,20 +57,27 @@ export function selectOrCopy(el: HTMLElement, content: string) { return true; } +export function queryMeta(key: string, defaultValue?: string): string { + let ret: string; + if (document) { + const meta = document.head.querySelector( + `meta[name='${key}']` + ) as HTMLMetaElement; + ret = meta?.content ?? ""; + } else { + ret = defaultValue ?? ""; + } + + return ret; +} + let currentId: string; export function getCurrentCommitId() { if (currentId) { return currentId; } - if (document) { - const meta = document.head.querySelector( - "meta[name='version']" - ) as HTMLMetaElement; - currentId = meta?.content ?? ""; - } else { - currentId = process.env.COMMIT_ID ?? ""; - } + currentId = queryMeta("version"); return currentId; } diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 00000000..afb54c31 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from "next/server"; +import { ACCESS_CODES } from "./app/api/access"; +import md5 from "spark-md5"; + +export const config = { + matcher: ["/api/chat", "/api/chat-stream"], +}; + +export function middleware(req: NextRequest, res: NextResponse) { + const accessCode = req.headers.get("access-code"); + const hashedCode = md5.hash(accessCode ?? "").trim(); + + console.log("[Auth] allowed hashed codes: ", [...ACCESS_CODES]); + console.log("[Auth] got access code:", accessCode); + console.log("[Auth] hashed access code:", hashedCode); + + if (!ACCESS_CODES.has(hashedCode)) { + return NextResponse.json( + { + needAccessCode: true, + hint: "Please go settings page and fill your access code.", + }, + { + status: 401, + } + ); + } + + return NextResponse.next(); +} diff --git a/package.json b/package.json index 4b84ffd3..df74cb96 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@types/react-katex": "^3.0.0", + "@types/spark-md5": "^3.0.2", "@vercel/analytics": "^0.1.11", "cross-env": "^7.0.3", "emoji-picker-react": "^4.4.7", @@ -30,6 +31,7 @@ "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", "sass": "^1.59.2", + "spark-md5": "^3.0.2", "typescript": "4.9.5", "zustand": "^4.3.6" } diff --git a/tsconfig.json b/tsconfig.json index e06a4454..14d18932 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "ES2015", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/yarn.lock b/yarn.lock index 98d87595..7e69c5da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1313,11 +1313,6 @@ dependencies: "@types/ms" "*" -"@types/git-rev-sync@^2.0.0": - version "2.0.0" - resolved "https://registry.npmmirror.com/@types/git-rev-sync/-/git-rev-sync-2.0.0.tgz#9de6e18cb01e65f769de77175bbe93254664023e" - integrity sha512-qGYApbb0m8Ofy3pwYks+kYVIZQAN/cqNucJGbl5O5GpLw9JSzp74rkTWDhPv3brrJfJb5/ixtimLJpo4tfh2QA== - "@types/hast@^2.0.0": version "2.3.4" resolved "https://registry.npmmirror.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc" @@ -1395,6 +1390,11 @@ resolved "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/spark-md5@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.2.tgz#da2e8a778a20335fc4f40b6471c4b0d86b70da55" + integrity sha512-82E/lVRaqelV9qmRzzJ1PKTpyrpnT7mwdneKNJB9hUtypZDMggloDfFUCIqRRx3lYRxteCwXSq9c+W71Vf0QnQ== + "@types/unist@*", "@types/unist@^2.0.0": version "2.0.6" resolved "https://registry.npmmirror.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" @@ -2138,7 +2138,7 @@ escalade@^3.1.1: resolved "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== @@ -2526,15 +2526,6 @@ get-tsconfig@^4.2.0: resolved "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.4.0.tgz#64eee64596668a81b8fce18403f94f245ee0d4e5" integrity sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ== -git-rev-sync@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/git-rev-sync/-/git-rev-sync-3.0.2.tgz#9763c730981187c3419b75dd270088cc5f0e161b" - integrity sha512-Nd5RiYpyncjLv0j6IONy0lGzAqdRXUaBctuGBbrEA2m6Bn4iDrN/9MeQTXuiquw8AEKL9D2BW0nw5m/lQvxqnQ== - dependencies: - escape-string-regexp "1.0.5" - graceful-fs "4.1.15" - shelljs "0.8.5" - glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -2561,7 +2552,7 @@ glob@7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.1.3: +glob@^7.1.3: version "7.2.3" resolved "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -2632,11 +2623,6 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@4.1.15: - version "4.1.15" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" - integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== - graceful-fs@^4.2.4: version "4.2.10" resolved "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -2804,11 +2790,6 @@ internal-slot@^1.0.3, internal-slot@^1.0.4: has "^1.0.3" side-channel "^1.0.4" -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - is-alphabetical@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" @@ -4014,13 +3995,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== - dependencies: - resolve "^1.1.6" - refractor@^4.7.0: version "4.8.1" resolved "https://registry.yarnpkg.com/refractor/-/refractor-4.8.1.tgz#fbdd889333a3d86c9c864479622855c9b38e9d42" @@ -4168,7 +4142,7 @@ resolve-from@^4.0.0: resolved "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@^1.1.6, resolve@^1.14.2, resolve@^1.22.1: +resolve@^1.14.2, resolve@^1.22.1: version "1.22.1" resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -4261,15 +4235,6 @@ shebang-regex@^3.0.0: resolved "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shelljs@0.8.5: - version "0.8.5" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" - integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - side-channel@^1.0.4: version "1.0.4" resolved "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -4304,6 +4269,11 @@ space-separated-tokens@^2.0.0: resolved "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== +spark-md5@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc" + integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw== + stable@^0.1.8: version "0.1.8" resolved "https://registry.npmmirror.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"