From 6b6007f9ad1a4ccb6429f9b8de622104cb8b12af Mon Sep 17 00:00:00 2001
From: Timothy Carambat <rambat1010@gmail.com>
Date: Thu, 20 Jul 2023 15:25:47 -0700
Subject: [PATCH] Enable the system owner to be able to update the system wide
 password and secret (#156)

* Enable the system owner to be able to update the system wide password and secret

* lint and cleanup
---
 .../Settings/PasswordProtection/index.jsx     | 141 ++++++++++++++++++
 .../src/components/Modals/Settings/index.jsx  |  11 +-
 frontend/src/models/system.js                 |  12 ++
 server/endpoints/system.js                    |  15 ++
 server/utils/helpers/updateENV.js             |  10 +-
 server/utils/http/index.js                    |  10 +-
 6 files changed, 191 insertions(+), 8 deletions(-)
 create mode 100644 frontend/src/components/Modals/Settings/PasswordProtection/index.jsx

diff --git a/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx b/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx
new file mode 100644
index 000000000..2b6444edb
--- /dev/null
+++ b/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx
@@ -0,0 +1,141 @@
+import React, { useState, useEffect } from "react";
+import System from "../../../../models/system";
+
+const noop = () => false;
+export default function PasswordProtection({ hideModal = noop }) {
+  const [loading, setLoading] = useState(true);
+  const [saving, setSaving] = useState(false);
+  const [success, setSuccess] = useState(false);
+  const [error, setError] = useState(null);
+  const [usePassword, setUsePassword] = useState(false);
+
+  const handleSubmit = async (e) => {
+    e.preventDefault();
+    setSaving(true);
+    setSuccess(false);
+    setError(null);
+
+    const form = new FormData(e.target);
+    const data = {
+      usePassword,
+      newPassword: form.get("password"),
+    };
+
+    const { success, error } = await System.updateSystemPassword(data);
+    if (success) {
+      setSuccess(true);
+      setSaving(false);
+      setTimeout(() => {
+        window.localStorage.removeItem("anythingllm_authToken");
+        window.location.reload();
+      }, 2_000);
+      return;
+    }
+
+    setError(error);
+    setSaving(false);
+  };
+
+  useEffect(() => {
+    async function fetchKeys() {
+      const settings = await System.keys();
+      setUsePassword(settings?.RequiresAuth);
+      setLoading(false);
+    }
+    fetchKeys();
+  }, []);
+
+  return (
+    <div className="relative w-full max-w-2xl max-h-full">
+      <div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
+        <div className="flex items-start justify-between px-6 py-4">
+          <p className="text-gray-800 dark:text-stone-200 text-base ">
+            Protect your AnythingLLM instance with a password. If you forget
+            this there is no recovery method so ensure you save this password.
+          </p>
+        </div>
+        {(error || success) && (
+          <div className="w-full flex px-6">
+            {error && (
+              <div className="w-full bg-red-300 text-red-800 font-semibold px-4 py-2 rounded-lg">
+                {error}
+              </div>
+            )}
+            {success && (
+              <div className="w-full bg-green-300 text-green-800 font-semibold px-4 py-2 rounded-lg">
+                Your page will refresh in a few seconds.
+              </div>
+            )}
+          </div>
+        )}
+        <div className="p-6 space-y-6 flex h-full w-full">
+          {loading ? (
+            <div className="w-full h-full flex items-center justify-center">
+              <p className="text-gray-800 dark:text-gray-200 text-base">
+                loading system settings
+              </p>
+            </div>
+          ) : (
+            <div className="w-full flex flex-col gap-y-4">
+              <form onSubmit={handleSubmit}>
+                <div className="">
+                  <label className="mb-2.5 block font-medium text-black dark:text-white">
+                    Password Protect Instance
+                  </label>
+
+                  <label className="relative inline-flex cursor-pointer items-center">
+                    <input
+                      type="checkbox"
+                      name="use_password"
+                      onClick={() => setUsePassword(!usePassword)}
+                      checked={usePassword}
+                      className="peer sr-only pointer-events-none"
+                    />
+                    <div className="pointer-events-none peer h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-green-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-stone-400 dark:peer-focus:ring-blue-800"></div>
+                  </label>
+                </div>
+                <div className="w-full flex flex-col gap-y-2 my-2">
+                  {usePassword && (
+                    <div>
+                      <label
+                        htmlFor="password"
+                        className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
+                      >
+                        New Password
+                      </label>
+                      <input
+                        name="password"
+                        type="text"
+                        className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
+                        placeholder="Your Instance Password"
+                        minLength={8}
+                        required={true}
+                        autoComplete="off"
+                      />
+                    </div>
+                  )}
+                  <button
+                    disabled={saving}
+                    type="submit"
+                    className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
+                  >
+                    {saving ? "Saving..." : "Save Changes"}
+                  </button>
+                </div>
+              </form>
+            </div>
+          )}
+        </div>
+        <div className="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
+          <button
+            onClick={hideModal}
+            type="button"
+            className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
+          >
+            Close
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/frontend/src/components/Modals/Settings/index.jsx b/frontend/src/components/Modals/Settings/index.jsx
index baa46df4a..e898a7afa 100644
--- a/frontend/src/components/Modals/Settings/index.jsx
+++ b/frontend/src/components/Modals/Settings/index.jsx
@@ -1,11 +1,13 @@
 import React, { useState } from "react";
-import { Archive, Cloud, Key, X } from "react-feather";
+import { Archive, Lock, Key, X } from "react-feather";
 import SystemKeys from "./Keys";
 import ExportOrImportData from "./ExportImport";
+import PasswordProtection from "./PasswordProtection";
 
 const TABS = {
   keys: SystemKeys,
   exportimport: ExportOrImportData,
+  password: PasswordProtection,
 };
 
 const noop = () => false;
@@ -62,6 +64,13 @@ function SettingTabs({ selectedTab, changeTab }) {
           icon={<Archive className="h-4 w-4 flex-shrink-0" />}
           onClick={changeTab}
         />
+        <SettingTab
+          active={selectedTab === "password"}
+          displayName="Password Protection"
+          tabName="password"
+          icon={<Lock className="h-4 w-4 flex-shrink-0" />}
+          onClick={changeTab}
+        />
       </ul>
     </div>
   );
diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js
index 06ca2b92c..7e5f61bfd 100644
--- a/frontend/src/models/system.js
+++ b/frontend/src/models/system.js
@@ -86,6 +86,18 @@ const System = {
         return { newValues: null, error: e.message };
       });
   },
+  updateSystemPassword: async (data) => {
+    return await fetch(`${API_BASE}/system/update-password`, {
+      method: "POST",
+      headers: baseHeaders(),
+      body: JSON.stringify(data),
+    })
+      .then((res) => res.json())
+      .catch((e) => {
+        console.error(e);
+        return { success: false, error: e.message };
+      });
+  },
   deleteDocument: async (name, meta) => {
     return await fetch(`${API_BASE}/system/remove-document`, {
       method: "DELETE",
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index 5193539ca..a39ef3a3b 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -13,6 +13,7 @@ const { getVectorDbClass } = require("../utils/helpers");
 const { updateENV } = require("../utils/helpers/updateENV");
 const { reqBody, makeJWT } = require("../utils/http");
 const { setupDataImports } = require("../utils/files/multer");
+const { v4 } = require("uuid");
 const { handleImports } = setupDataImports();
 
 function systemEndpoints(app) {
@@ -155,6 +156,20 @@ function systemEndpoints(app) {
     }
   });
 
+  app.post("/system/update-password", async (request, response) => {
+    try {
+      const { usePassword, newPassword } = reqBody(request);
+      const { error } = updateENV({
+        AuthToken: usePassword ? newPassword : "",
+        JWTSecret: usePassword ? v4() : "",
+      });
+      response.status(200).json({ success: !error, error });
+    } catch (e) {
+      console.log(e.message, e);
+      response.sendStatus(500).end();
+    }
+  });
+
   app.get("/system/data-export", async (_, response) => {
     try {
       const { filename, error } = await exportData();
diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js
index 4161aec13..54eec1e5b 100644
--- a/server/utils/helpers/updateENV.js
+++ b/server/utils/helpers/updateENV.js
@@ -27,9 +27,15 @@ const KEY_MAPPING = {
     envKey: "PINECONE_INDEX",
     checks: [],
   },
+  AuthToken: {
+    envKey: "AUTH_TOKEN",
+    checks: [],
+  },
+  JWTSecret: {
+    envKey: "JWT_SECRET",
+    checks: [],
+  },
   // Not supported yet.
-  // 'AuthToken': 'AUTH_TOKEN',
-  // 'JWTSecret': 'JWT_SECRET',
   // 'StorageDir': 'STORAGE_DIR',
 };
 
diff --git a/server/utils/http/index.js b/server/utils/http/index.js
index af42f5de5..9fd643b75 100644
--- a/server/utils/http/index.js
+++ b/server/utils/http/index.js
@@ -2,7 +2,6 @@ process.env.NODE_ENV === "development"
   ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` })
   : require("dotenv").config();
 const JWT = require("jsonwebtoken");
-const SECRET = process.env.JWT_SECRET;
 
 function reqBody(request) {
   return typeof request.body === "string"
@@ -15,15 +14,16 @@ function queryParams(request) {
 }
 
 function makeJWT(info = {}, expiry = "30d") {
-  if (!SECRET) throw new Error("Cannot create JWT as JWT_SECRET is unset.");
-  return JWT.sign(info, SECRET, { expiresIn: expiry });
+  if (!process.env.JWT_SECRET)
+    throw new Error("Cannot create JWT as JWT_SECRET is unset.");
+  return JWT.sign(info, process.env.JWT_SECRET, { expiresIn: expiry });
 }
 
 function decodeJWT(jwtToken) {
   try {
-    return JWT.verify(jwtToken, SECRET);
+    return JWT.verify(jwtToken, process.env.JWT_SECRET);
   } catch {}
-  return null;
+  return { p: null };
 }
 
 module.exports = {
-- 
GitLab