Browse Source

refactor: build/runtime/client configs

Yidadaa 2 years ago
parent
commit
9b61cb1335
14 changed files with 154 additions and 91 deletions
  1. 0 1
      Dockerfile
  2. 0 17
      app/api/access.ts
  3. 5 4
      app/components/settings.tsx
  4. 27 0
      app/config/build.ts
  5. 42 0
      app/config/client.ts
  6. 42 0
      app/config/server.ts
  7. 1 0
      app/constant.ts
  8. 3 28
      app/layout.tsx
  9. 18 1
      app/page.tsx
  10. 5 3
      app/store/access.ts
  11. 3 3
      app/store/update.ts
  12. 0 25
      app/utils.ts
  13. 5 3
      middleware.ts
  14. 3 6
      next.config.js

+ 0 - 1
Dockerfile

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

+ 0 - 17
app/api/access.ts

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

+ 5 - 4
app/components/settings.tsx

@@ -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 - 0
app/config/build.ts

@@ -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 - 0
app/config/client.ts

@@ -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 - 0
app/config/server.ts

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

+ 1 - 0
app/constant.ts

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

+ 3 - 28
app/layout.tsx

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

+ 18 - 1
app/page.tsx

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

+ 5 - 3
app/store/access.ts

@@ -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()
+        );
       },
     }),
     {

+ 3 - 3
app/store/update.ts

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

+ 0 - 25
app/utils.ts

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

+ 5 - 3
middleware.ts

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

+ 3 - 6
next.config.js

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