feat: #2 add access control by

access code
This commit is contained in:
Yifei Zhang
2023-03-26 06:53:40 +00:00
parent a5b3998304
commit 2c899cf00e
16 changed files with 216 additions and 63 deletions

16
app/api/access.ts Normal file
View File

@@ -0,0 +1,16 @@
import md5 from "spark-md5";
export function getAccessCodes(): Set<string> {
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();

View File

@@ -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: {
<div className={styles["settings-sub-title"]}>{props.subTitle}</div>
)}
</div>
<div>{props.children}</div>
{props.children}
</ListItem>
);
}
@@ -71,6 +72,12 @@ export function Settings(props: { closeSettings: () => void }) {
checkUpdate();
}, []);
const accessStore = useAccessStore();
const enabledAccessControl = useMemo(
() => accessStore.enabledAccessControl(),
[]
);
return (
<>
<div className={styles["window-header"]}>
@@ -232,6 +239,20 @@ export function Settings(props: { closeSettings: () => void }) {
</div>
</List>
<List>
<SettingItem
title={Locale.Settings.AccessCode.Title}
subTitle={Locale.Settings.AccessCode.SubTitle}
>
<input
value={accessStore.accessCode}
type="text"
placeholder={Locale.Settings.AccessCode.Placeholder}
onChange={(e) => {
accessStore.updateCode(e.currentTarget.value);
}}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}

View File

@@ -3,16 +3,32 @@ import "./styles/globals.scss";
import "./styles/markdown.scss";
import "./styles/prism.scss";
import process from "child_process";
import { ACCESS_CODES } from "./api/access";
const COMMIT_ID = process
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
export const metadata = {
title: "ChatGPT Next Web",
description: "Your personal ChatGPT Chat Bot.",
};
const COMMIT_ID = process
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
function Meta() {
const metas = {
version: COMMIT_ID,
access: ACCESS_CODES.size > 0 ? "enabled" : "disabled",
};
return (
<>
{Object.entries(metas).map(([k, v]) => (
<meta name={k} content={v} key={k} />
))}
</>
);
}
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"
/>
<meta name="version" content={COMMIT_ID} />
<Meta />
<link rel="manifest" href="/site.webmanifest"></link>
<link rel="preconnect" href="https://fonts.googleapis.com"></link>
<link rel="preconnect" href="https://fonts.gstatic.com"></link>

View File

@@ -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)",

View File

@@ -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",

View File

@@ -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<string, string> = {};
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);
}
}

30
app/store/access.ts Normal file
View File

@@ -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<AccessControlStore>()(
persist(
(set, get) => ({
accessCode: "",
enabledAccessControl() {
return queryMeta("access") === "enabled";
},
updateCode(code: string) {
set((state) => ({ accessCode: code }));
},
}),
{
name: ACCESS_KEY,
version: 1,
}
)
);

View File

@@ -308,6 +308,7 @@ export const useChatStore = create<ChatStore>()(
onMessage(content, done) {
if (done) {
botMessage.streaming = false;
botMessage.content = content;
get().onNewMessage(botMessage);
} else {
botMessage.content = content;

View File

@@ -1,2 +1,3 @@
export * from "./app";
export * from "./update";
export * from "./access";

View File

@@ -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 {

View File

@@ -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;
}