refactor: build/runtime/client configs

This commit is contained in:
Yidadaa 2023-04-11 01:21:34 +08:00
parent 7aee53ea05
commit 9b61cb1335
14 changed files with 154 additions and 91 deletions

View File

@ -17,7 +17,6 @@ RUN apk update && apk add --no-cache git
ENV OPENAI_API_KEY=""
ENV CODE=""
ARG DOCKER=true
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules

View File

@ -1,17 +0,0 @@
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();
export const IS_IN_DOCKER = process.env.DOCKER;

View File

@ -26,13 +26,14 @@ import {
import { Avatar } from "./chat";
import Locale, { AllLangs, changeLang, getLang } from "../locales";
import { getCurrentVersion, getEmojiUrl } from "../utils";
import { getEmojiUrl } from "../utils";
import Link from "next/link";
import { UPDATE_URL } from "../constant";
import { SearchService, usePromptStore } from "../store/prompt";
import { requestUsage } from "../requests";
import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range";
import { getClientSideConfig } from "../config/client";
function SettingItem(props: {
title: string;
@ -88,9 +89,9 @@ export function Settings(props: { closeSettings: () => void }) {
const updateStore = useUpdateStore();
const [checkingUpdate, setCheckingUpdate] = useState(false);
const currentId = getCurrentVersion();
const currentVersion = getClientSideConfig()?.version;
const remoteId = updateStore.remoteId;
const hasNewVersion = currentId !== remoteId;
const hasNewVersion = currentVersion !== remoteId;
function checkUpdate(force = false) {
setCheckingUpdate(true);
@ -224,7 +225,7 @@ export function Settings(props: { closeSettings: () => void }) {
</SettingItem>
<SettingItem
title={Locale.Settings.Update.Version(currentId)}
title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
subTitle={
checkingUpdate
? Locale.Settings.Update.IsChecking

27
app/config/build.ts Normal file
View File

@ -0,0 +1,27 @@
const COMMIT_ID: string = (() => {
try {
const childProcess = require("child_process");
return (
childProcess
// .execSync("git describe --tags --abbrev=0")
.execSync("git rev-parse --short HEAD")
.toString()
.trim()
);
} catch (e) {
console.error("[Build Config] No git or not from git repo.");
return "unknown";
}
})();
export const getBuildConfig = () => {
if (typeof process === "undefined") {
throw Error(
"[Server Config] you are importing a nodejs-only module outside of nodejs",
);
}
return {
commitId: COMMIT_ID,
};
};

42
app/config/client.ts Normal file
View File

@ -0,0 +1,42 @@
import { RUNTIME_CONFIG_DOM } from "../constant";
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;
}
export function getClientSideConfig() {
if (typeof window === "undefined") {
throw Error(
"[Client Config] you are importing a browser-only module outside of browser",
);
}
const dom = document.getElementById(RUNTIME_CONFIG_DOM);
if (!dom) {
throw Error("[Config] Dont get config before page loading!");
}
try {
const fromServerConfig = JSON.parse(dom.innerText) as DangerConfig;
const fromBuildConfig = {
version: queryMeta("version"),
};
return {
...fromServerConfig,
...fromBuildConfig,
};
} catch (e) {
console.error("[Config] failed to parse client config");
}
}

42
app/config/server.ts Normal file
View File

@ -0,0 +1,42 @@
import md5 from "spark-md5";
declare global {
namespace NodeJS {
interface ProcessEnv {
OPENAI_API_KEY?: string;
CODE?: string;
PROXY_URL?: string;
VERCEL?: string;
}
}
}
const ACCESS_CODES = (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 getServerSideConfig = () => {
if (typeof process === "undefined") {
throw Error(
"[Server Config] you are importing a nodejs-only module outside of nodejs",
);
}
return {
apiKey: process.env.OPENAI_API_KEY,
code: process.env.CODE,
codes: ACCESS_CODES,
needCode: ACCESS_CODES.size > 0,
proxyUrl: process.env.PROXY_URL,
isVercel: !!process.env.VERCEL,
};
};

View File

@ -5,3 +5,4 @@ export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
export const UPDATE_URL = `${REPO_URL}#keep-updated`;
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";

View File

@ -2,19 +2,9 @@
import "./styles/globals.scss";
import "./styles/markdown.scss";
import "./styles/highlight.scss";
import process from "child_process";
import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
import { getBuildConfig } from "./config/build";
let COMMIT_ID: string | undefined;
try {
COMMIT_ID = process
// .execSync("git describe --tags --abbrev=0")
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
} catch (e) {
console.error("No git or not from git repo.");
}
const buildConfig = getBuildConfig();
export const metadata = {
title: "ChatGPT Next Web",
@ -26,21 +16,6 @@ export const metadata = {
themeColor: "#fafafa",
};
function Meta() {
const metas = {
version: COMMIT_ID ?? "unknown",
access: ACCESS_CODES.size > 0 || IS_IN_DOCKER ? "enabled" : "disabled",
};
return (
<>
{Object.entries(metas).map(([k, v]) => (
<meta name={k} content={v} key={k} />
))}
</>
);
}
export default function RootLayout({
children,
}: {
@ -58,7 +33,7 @@ export default function RootLayout({
content="#151515"
media="(prefers-color-scheme: dark)"
/>
<Meta />
<meta name="version" content={buildConfig.commitId} />
<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,12 +1,29 @@
import { Analytics } from "@vercel/analytics/react";
import { Home } from "./components/home";
import { getServerSideConfig } from "./config/server";
import { RUNTIME_CONFIG_DOM } from "./constant";
const serverConfig = getServerSideConfig();
// Danger! Don not write any secret value here!
// 警告!不要在这里写入任何敏感信息!
const DANGER_CONFIG = {
needCode: serverConfig?.needCode,
};
declare global {
type DangerConfig = typeof DANGER_CONFIG;
}
export default function App() {
return (
<>
<div style={{ display: "none" }} id={RUNTIME_CONFIG_DOM}>
{JSON.stringify(DANGER_CONFIG)}
</div>
<Home />
<Analytics />
{serverConfig?.isVercel && <Analytics />}
</>
);
}

View File

@ -1,6 +1,6 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { queryMeta } from "../utils";
import { getClientSideConfig } from "../config/client";
export interface AccessControlStore {
accessCode: string;
@ -20,7 +20,7 @@ export const useAccessStore = create<AccessControlStore>()(
token: "",
accessCode: "",
enabledAccessControl() {
return queryMeta("access") === "enabled";
return !!getClientSideConfig()?.needCode;
},
updateCode(code: string) {
set((state) => ({ accessCode: code }));
@ -30,7 +30,9 @@ export const useAccessStore = create<AccessControlStore>()(
},
isAuthorized() {
// has token or has code or disabled access control
return !!get().token || !!get().accessCode || !get().enabledAccessControl();
return (
!!get().token || !!get().accessCode || !get().enabledAccessControl()
);
},
}),
{

View File

@ -1,7 +1,7 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { getClientSideConfig } from "../config/client";
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
import { getCurrentVersion } from "../utils";
export interface UpdateStore {
lastUpdate: number;
@ -22,7 +22,7 @@ export const useUpdateStore = create<UpdateStore>()(
const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
const shouldFetch = force || overTenMins;
if (!shouldFetch) {
return getCurrentVersion();
return getClientSideConfig()?.version ?? "";
}
try {
@ -38,7 +38,7 @@ export const useUpdateStore = create<UpdateStore>()(
return remoteId;
} catch (error) {
console.error("[Fetch Upstream Commit Id]", error);
return getCurrentVersion();
return getClientSideConfig()?.version ?? "";
}
},
}),

View File

@ -69,31 +69,6 @@ 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 getCurrentVersion() {
if (currentId) {
return currentId;
}
currentId = queryMeta("version");
return currentId;
}
export function getEmojiUrl(unified: string, style: EmojiStyle) {
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
}

View File

@ -1,21 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { ACCESS_CODES } from "./app/api/access";
import { getServerSideConfig } from "./app/config/server";
import md5 from "spark-md5";
export const config = {
matcher: ["/api/openai", "/api/chat-stream"],
};
const serverConfig = getServerSideConfig();
export function middleware(req: NextRequest) {
const accessCode = req.headers.get("access-code");
const token = req.headers.get("token");
const hashedCode = md5.hash(accessCode ?? "").trim();
console.log("[Auth] allowed hashed codes: ", [...ACCESS_CODES]);
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
console.log("[Auth] got access code:", accessCode);
console.log("[Auth] hashed access code:", hashedCode);
if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode) && !token) {
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
return NextResponse.json(
{
error: true,

View File

@ -8,14 +8,11 @@ const nextConfig = {
config.module.rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"],
}); // 针对 SVG 的处理规则
});
return config;
}
},
output: "standalone",
};
if (process.env.DOCKER) {
nextConfig.output = 'standalone'
}
module.exports = nextConfig;