From 62e3f62e82f38b12209a22b1deff43cdf93922a0 Mon Sep 17 00:00:00 2001
From: Timothy Carambat <rambat1010@gmail.com>
Date: Fri, 9 Jun 2023 11:27:27 -0700
Subject: [PATCH] 12 auth implementation (#13)

* Add Auth protection for cloud-based or private instances

* skip check on local dev
---
 .../CannotRemoveModal/index.jsx               |  11 +-
 .../ConfirmationModal/index.jsx               |   2 +-
 .../Modals/MangeWorkspace/Directory/index.jsx |   2 +-
 .../Modals/MangeWorkspace/index.jsx           |  10 +-
 frontend/src/components/Modals/Password.jsx   | 119 ++++++++++++++++
 .../Sidebar/ActiveWorkspaces/index.jsx        |   5 +-
 .../components/Sidebar/Placeholder/index.jsx  | 134 ++++++++++++++++++
 frontend/src/models/system.js                 |  30 +++-
 frontend/src/models/workspace.js              |  13 +-
 frontend/src/pages/Main/index.jsx             |  18 +++
 frontend/src/pages/WorkspaceChat/index.jsx    |  22 +++
 frontend/src/utils/request.js                 |   9 ++
 server/.env.example                           |   1 +
 server/endpoints/system.js                    |  36 ++++-
 server/index.js                               |   3 +-
 server/package.json                           |   1 +
 server/utils/http/index.js                    |  20 +++
 server/utils/middleware/validatedRequest.js   |  12 +-
 server/utils/vectorDbProviders/lance/index.js |   7 +-
 19 files changed, 429 insertions(+), 26 deletions(-)
 create mode 100644 frontend/src/components/Modals/Password.jsx
 create mode 100644 frontend/src/components/Sidebar/Placeholder/index.jsx
 create mode 100644 frontend/src/utils/request.js

diff --git a/frontend/src/components/Modals/MangeWorkspace/CannotRemoveModal/index.jsx b/frontend/src/components/Modals/MangeWorkspace/CannotRemoveModal/index.jsx
index a66442fd2..51028aabc 100644
--- a/frontend/src/components/Modals/MangeWorkspace/CannotRemoveModal/index.jsx
+++ b/frontend/src/components/Modals/MangeWorkspace/CannotRemoveModal/index.jsx
@@ -1,10 +1,7 @@
 import React from "react";
 import { titleCase } from "text-case";
 
-export default function CannotRemoveModal({
-  hideModal,
-  vectordb,
-}) {
+export default function CannotRemoveModal({ hideModal, vectordb }) {
   return (
     <dialog
       open={true}
@@ -19,7 +16,11 @@ export default function CannotRemoveModal({
 
           <div className="flex flex-col gap-y-1">
             <p className="text-base mt-4">
-              {titleCase(vectordb)} does not support atomic removal of documents.<br />Unfortunately, you will have to delete the entire workspace to remove this document from being referenced.
+              {titleCase(vectordb)} does not support atomic removal of
+              documents.
+              <br />
+              Unfortunately, you will have to delete the entire workspace to
+              remove this document from being referenced.
             </p>
           </div>
           <div className="flex w-full justify-center items-center mt-4">
diff --git a/frontend/src/components/Modals/MangeWorkspace/ConfirmationModal/index.jsx b/frontend/src/components/Modals/MangeWorkspace/ConfirmationModal/index.jsx
index 6dce42354..bec3beec8 100644
--- a/frontend/src/components/Modals/MangeWorkspace/ConfirmationModal/index.jsx
+++ b/frontend/src/components/Modals/MangeWorkspace/ConfirmationModal/index.jsx
@@ -86,4 +86,4 @@ export default function ConfirmationModal({
       </div>
     </dialog>
   );
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/Modals/MangeWorkspace/Directory/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Directory/index.jsx
index b2eb41250..b444f9793 100644
--- a/frontend/src/components/Modals/MangeWorkspace/Directory/index.jsx
+++ b/frontend/src/components/Modals/MangeWorkspace/Directory/index.jsx
@@ -147,4 +147,4 @@ export default function Directory({
       )}
     </div>
   );
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/Modals/MangeWorkspace/index.jsx b/frontend/src/components/Modals/MangeWorkspace/index.jsx
index 4c3cde12a..675e622ee 100644
--- a/frontend/src/components/Modals/MangeWorkspace/index.jsx
+++ b/frontend/src/components/Modals/MangeWorkspace/index.jsx
@@ -1,5 +1,5 @@
 import React, { useState, useEffect } from "react";
-import { X } from 'react-feather';
+import { X } from "react-feather";
 import System from "../../../models/system";
 import Workspace from "../../../models/workspace";
 import paths from "../../../utils/paths";
@@ -18,7 +18,7 @@ export default function ManageWorkspace({ hideModal = noop, workspace }) {
   const [originalDocuments, setOriginalDocuments] = useState([]);
   const [selectedFiles, setSelectFiles] = useState([]);
   const [vectordb, setVectorDB] = useState(null);
-  const [showingNoRemovalModal, setShowingNoRemovalModal] = useState(false)
+  const [showingNoRemovalModal, setShowingNoRemovalModal] = useState(false);
 
   useEffect(() => {
     async function fetchKeys() {
@@ -29,7 +29,7 @@ export default function ManageWorkspace({ hideModal = noop, workspace }) {
       setDirectories(localFiles);
       setOriginalDocuments([...originalDocs]);
       setSelectFiles([...originalDocs]);
-      setVectorDB(settings?.VectorDB)
+      setVectorDB(settings?.VectorDB);
       setLoading(false);
     }
     fetchKeys();
@@ -99,7 +99,7 @@ export default function ManageWorkspace({ hideModal = noop, workspace }) {
     return isFolder
       ? originalDocuments.some((doc) => doc.includes(filepath.split("/")[0]))
       : originalDocuments.some((doc) => doc.includes(filepath));
-  }
+  };
 
   const toggleSelection = (filepath) => {
     const isFolder = !filepath.includes("/");
@@ -108,7 +108,7 @@ export default function ManageWorkspace({ hideModal = noop, workspace }) {
     if (isSelected(filepath)) {
       // Certain vector DBs do not contain the ability to delete vectors
       // so we cannot remove from these. The user will have to clear the entire workspace.
-      if (['lancedb'].includes(vectordb) && isOriginalDoc(filepath)) {
+      if (["lancedb"].includes(vectordb) && isOriginalDoc(filepath)) {
         setShowingNoRemovalModal(true);
         return false;
       }
diff --git a/frontend/src/components/Modals/Password.jsx b/frontend/src/components/Modals/Password.jsx
new file mode 100644
index 000000000..67f554f84
--- /dev/null
+++ b/frontend/src/components/Modals/Password.jsx
@@ -0,0 +1,119 @@
+import React, { useState, useEffect, useRef } from "react";
+import System from "../../models/system";
+
+export default function PasswordModal() {
+  const [loading, setLoading] = useState(false);
+  const formEl = useRef(null);
+  const [error, setError] = useState(null);
+  const handleLogin = async (e) => {
+    setError(null);
+    setLoading(true);
+    e.preventDefault();
+    const data = {};
+
+    const form = new FormData(formEl.current);
+    for (var [key, value] of form.entries()) data[key] = value;
+    const { valid, token, message } = await System.requestToken(data);
+    if (valid && !!token) {
+      window.localStorage.setItem("anythingllm_authtoken", token);
+      window.location.reload();
+    } else {
+      setError(message);
+      setLoading(false);
+    }
+    setLoading(false);
+  };
+
+  return (
+    <div class="fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-black bg-opacity-50 flex items-center justify-center">
+      <div className="flex fixed top-0 left-0 right-0 w-full h-full" />
+      <div class="relative w-full max-w-2xl max-h-full">
+        <form ref={formEl} onSubmit={handleLogin}>
+          <div class="relative bg-white rounded-lg shadow dark:bg-stone-700">
+            <div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
+              <h3 class="text-xl font-semibold text-gray-900 dark:text-white">
+                This workspace is password protected.
+              </h3>
+            </div>
+            <div class="p-6 space-y-6 flex h-full w-full">
+              <div className="w-full flex flex-col gap-y-4">
+                <div>
+                  <label
+                    htmlFor="password"
+                    class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
+                  >
+                    Workspace Password
+                  </label>
+                  <input
+                    name="password"
+                    type="password"
+                    id="password"
+                    class="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"
+                    required={true}
+                    autoComplete="off"
+                  />
+                </div>
+                {error && (
+                  <p className="text-red-600 dark:text-red-400 text-sm">
+                    Error: {error}
+                  </p>
+                )}
+                <p className="text-gray-800 dark:text-slate-200 text-sm">
+                  You will only have to enter this password once. After
+                  successful login it will be stored in your browser.
+                </p>
+              </div>
+            </div>
+            <div class="flex items-center justify-end p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
+              <button
+                disabled={loading}
+                type="submit"
+                class="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"
+              >
+                {loading ? "Validating..." : "Submit"}
+              </button>
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+}
+
+export function usePasswordModal() {
+  const [requiresAuth, setRequiresAuth] = useState(null);
+  useEffect(() => {
+    async function checkAuthReq() {
+      if (!window) return;
+      if (import.meta.env.DEV) {
+        setRequiresAuth(false);
+      } else {
+        const currentToken = window.localStorage.getItem("anythingllm_authtoken");
+        const settings = await System.keys();
+        const requiresAuth = settings?.RequiresAuth || false;
+
+        // If Auth is disabled - skip check
+        if (!requiresAuth) {
+          setRequiresAuth(requiresAuth);
+          return;
+        }
+
+        if (!!currentToken) {
+          const valid = await System.checkAuth(currentToken);
+          if (!valid) {
+            setRequiresAuth(true);
+            window.localStorage.removeItem("anythingllm_authtoken");
+            return;
+          } else {
+            setRequiresAuth(false);
+            return;
+          }
+        }
+        setRequiresAuth(true);
+      }
+    }
+    checkAuthReq();
+  }, []);
+
+  return { requiresAuth };
+}
diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx
index 67da8086b..9a3cdc586 100644
--- a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx
+++ b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx
@@ -51,10 +51,11 @@ export default function ActiveWorkspaces() {
           >
             <a
               href={isActive ? null : paths.workspace.chat(workspace.slug)}
-              className={`flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center ${isActive
+              className={`flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center ${
+                isActive
                   ? "bg-gray-100 dark:bg-stone-600"
                   : "hover:bg-slate-100 dark:hover:bg-stone-900 "
-                }`}
+              }`}
             >
               <Book className="h-4 w-4" />
               <p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
diff --git a/frontend/src/components/Sidebar/Placeholder/index.jsx b/frontend/src/components/Sidebar/Placeholder/index.jsx
new file mode 100644
index 000000000..5f81c0d4b
--- /dev/null
+++ b/frontend/src/components/Sidebar/Placeholder/index.jsx
@@ -0,0 +1,134 @@
+import React, { useRef } from "react";
+import {
+  BookOpen,
+  Briefcase,
+  Cpu,
+  GitHub,
+  Key,
+  Plus,
+  AlertCircle,
+} from "react-feather";
+import * as Skeleton from "react-loading-skeleton";
+import "react-loading-skeleton/dist/skeleton.css";
+import paths from "../../../utils/paths";
+
+export default function Sidebar() {
+  const sidebarRef = useRef(null);
+
+  // const handleWidthToggle = () => {
+  //   if (!sidebarRef.current) return false;
+  //   sidebarRef.current.classList.add('translate-x-[-100%]')
+  // }
+
+  return (
+    <>
+      <div
+        ref={sidebarRef}
+        style={{ height: "calc(100% - 32px)" }}
+        className="transition-all duration-500 relative m-[16px] rounded-[26px] bg-white dark:bg-black-900 min-w-[15.5%] p-[18px] "
+      >
+        {/* <button onClick={handleWidthToggle} className='absolute -right-[13px] top-[35%] bg-white w-auto h-auto bg-transparent flex items-center'>
+        <svg width="16" height="96" viewBox="0 0 16 96" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#141414"><path d="M2.5 0H3C3 20 15 12 15 32V64C15 84 3 76 3 96H2.5V0Z" fill="black" fill-opacity="0.12" stroke="transparent" stroke-width="0px"></path><path d="M0 0H2.5C2.5 20 14.5 12 14.5 32V64C14.5 84 2.5 76 2.5 96H0V0Z" fill="#141414"></path></svg>
+        <ChevronLeft className='absolute h-4 w-4 text-white mr-1' />
+      </button> */}
+
+        <div className="w-full h-full flex flex-col overflow-x-hidden items-between">
+          {/* Header Information */}
+          <div className="flex w-full items-center justify-between">
+            <p className="text-xl font-base text-slate-600 dark:text-slate-200">
+              AnythingLLM
+            </p>
+            <div className="flex gap-x-2 items-center text-slate-500">
+              <button className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-stone-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200">
+                <Key className="h-4 w-4 " />
+              </button>
+            </div>
+          </div>
+
+          {/* Primary Body */}
+          <div className="h-[100%] flex flex-col w-full justify-between pt-4 overflow-y-hidden">
+            <div className="h-auto sidebar-items dark:sidebar-items">
+              <div className="flex flex-col gap-y-4 h-[65vh] pb-8 overflow-y-scroll no-scroll">
+                <div className="flex gap-x-2 items-center justify-between">
+                  <button className="flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center hover:bg-slate-100 dark:hover:bg-stone-900">
+                    <Plus className="h-4 w-4" />
+                    <p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
+                      New workspace
+                    </p>
+                  </button>
+                </div>
+                <Skeleton.default
+                  height={36}
+                  width="100%"
+                  count={3}
+                  baseColor="#292524"
+                  highlightColor="#4c4948"
+                  enableAnimation={true}
+                />
+              </div>
+            </div>
+            <div>
+              <div className="flex flex-col gap-y-2">
+                <div className="w-full flex items-center justify-start">
+                  <div className="flex w-fit items-center justify-end gap-x-2">
+                    <p className="text-slate-400 leading-loose text-sm">LLM</p>
+                    <div className="flex items-center gap-x-1 border border-red-400 px-2 bg-red-200 rounded-full">
+                      <p className="text-red-700 leading-tight text-sm">
+                        offline
+                      </p>
+                      <AlertCircle className="h-3 w-3 stroke-red-100 fill-red-400" />
+                    </div>
+                  </div>
+                </div>
+                <a
+                  href={paths.hosting()}
+                  target="_blank"
+                  className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 dark:border-transparent rounded-lg text-slate-800 dark:text-slate-200 justify-center items-center hover:bg-slate-100 dark:bg-stone-800 dark:hover:bg-stone-900"
+                >
+                  <Cpu className="h-4 w-4" />
+                  <p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
+                    Managed cloud hosting
+                  </p>
+                </a>
+                <a
+                  href={paths.hosting()}
+                  target="_blank"
+                  className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 dark:border-transparent rounded-lg text-slate-800 dark:text-slate-200 justify-center items-center hover:bg-slate-100  dark:bg-stone-800 dark:hover:bg-stone-900"
+                >
+                  <Briefcase className="h-4 w-4" />
+                  <p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
+                    Enterprise Installation
+                  </p>
+                </a>
+              </div>
+
+              {/* Footer */}
+              <div className="flex items-end justify-between mt-2">
+                <div className="flex gap-x-1 items-center">
+                  <a
+                    href={paths.github()}
+                    className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-slate-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
+                  >
+                    <GitHub className="h-4 w-4 " />
+                  </a>
+                  <a
+                    href={paths.docs()}
+                    className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-slate-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
+                  >
+                    <BookOpen className="h-4 w-4 " />
+                  </a>
+                </div>
+                <a
+                  href={paths.mailToMintplex()}
+                  className="transition-all duration-300 text-xs text-slate-200 dark:text-slate-600 hover:text-blue-600 dark:hover:text-blue-400"
+                >
+                  @MintplexLabs
+                </a>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </>
+  );
+}
diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js
index 4e24ffbaa..a2c3cc9cf 100644
--- a/frontend/src/models/system.js
+++ b/frontend/src/models/system.js
@@ -1,4 +1,5 @@
 import { API_BASE } from "../utils/constants";
+import { baseHeaders } from "../utils/request";
 
 const System = {
   ping: async function () {
@@ -7,7 +8,9 @@ const System = {
       .catch(() => false);
   },
   totalIndexes: async function () {
-    return await fetch(`${API_BASE}/system-vectors`)
+    return await fetch(`${API_BASE}/system/system-vectors`, {
+      headers: baseHeaders(),
+    })
       .then((res) => {
         if (!res.ok) throw new Error("Could not find indexes.");
         return res.json();
@@ -25,7 +28,9 @@ const System = {
       .catch(() => null);
   },
   localFiles: async function () {
-    return await fetch(`${API_BASE}/local-files`)
+    return await fetch(`${API_BASE}/system/local-files`, {
+      headers: baseHeaders(),
+    })
       .then((res) => {
         if (!res.ok) throw new Error("Could not find setup information.");
         return res.json();
@@ -33,6 +38,27 @@ const System = {
       .then((res) => res.localFiles)
       .catch(() => null);
   },
+  checkAuth: async function (currentToken = null) {
+    return await fetch(`${API_BASE}/system/check-token`, {
+      headers: baseHeaders(currentToken),
+    })
+      .then((res) => res.ok)
+      .catch(() => false);
+  },
+  requestToken: async function (body) {
+    return await fetch(`${API_BASE}/request-token`, {
+      method: "POST",
+      body: JSON.stringify({ ...body }),
+    })
+      .then((res) => {
+        if (!res.ok) throw new Error("Could not validate login.");
+        return res.json();
+      })
+      .then((res) => res)
+      .catch((e) => {
+        return { valid: false, message: e.message };
+      });
+  },
 };
 
 export default System;
diff --git a/frontend/src/models/workspace.js b/frontend/src/models/workspace.js
index 16096a92e..11f97437d 100644
--- a/frontend/src/models/workspace.js
+++ b/frontend/src/models/workspace.js
@@ -1,10 +1,12 @@
 import { API_BASE } from "../utils/constants";
+import { baseHeaders } from "../utils/request";
 
 const Workspace = {
   new: async function (data = {}) {
     const { workspace, message } = await fetch(`${API_BASE}/workspace/new`, {
       method: "POST",
       body: JSON.stringify(data),
+      headers: baseHeaders(),
     })
       .then((res) => res.json())
       .catch((e) => {
@@ -19,6 +21,7 @@ const Workspace = {
       {
         method: "POST",
         body: JSON.stringify(changes), // contains 'adds' and 'removes' keys that are arrays of filepaths
+        headers: baseHeaders(),
       }
     )
       .then((res) => res.json())
@@ -29,7 +32,9 @@ const Workspace = {
     return { workspace, message };
   },
   chatHistory: async function (slug) {
-    const history = await fetch(`${API_BASE}/workspace/${slug}/chats`)
+    const history = await fetch(`${API_BASE}/workspace/${slug}/chats`, {
+      headers: baseHeaders(),
+    })
       .then((res) => res.json())
       .then((res) => res.history || [])
       .catch(() => []);
@@ -39,6 +44,7 @@ const Workspace = {
     const chatResult = await fetch(`${API_BASE}/workspace/${slug}/chat`, {
       method: "POST",
       body: JSON.stringify({ message, mode }),
+      headers: baseHeaders(),
     })
       .then((res) => res.json())
       .catch((e) => {
@@ -57,7 +63,9 @@ const Workspace = {
     return workspaces;
   },
   bySlug: async function (slug = "") {
-    const workspace = await fetch(`${API_BASE}/workspace/${slug}`)
+    const workspace = await fetch(`${API_BASE}/workspace/${slug}`, {
+      headers: baseHeaders(),
+    })
       .then((res) => res.json())
       .then((res) => res.workspace)
       .catch(() => null);
@@ -66,6 +74,7 @@ const Workspace = {
   delete: async function (slug) {
     const result = await fetch(`${API_BASE}/workspace/${slug}`, {
       method: "DELETE",
+      headers: baseHeaders(),
     })
       .then((res) => res.ok)
       .catch(() => false);
diff --git a/frontend/src/pages/Main/index.jsx b/frontend/src/pages/Main/index.jsx
index bff97c458..94ae31300 100644
--- a/frontend/src/pages/Main/index.jsx
+++ b/frontend/src/pages/Main/index.jsx
@@ -1,8 +1,26 @@
 import React from "react";
 import DefaultChatContainer from "../../components/DefaultChat";
 import Sidebar from "../../components/Sidebar";
+import SidebarPlaceholder from "../../components/Sidebar/Placeholder";
+import ChatPlaceholder from "../../components/WorkspaceChat/LoadingChat";
+import PasswordModal, {
+  usePasswordModal,
+} from "../../components/Modals/Password";
 
 export default function Main() {
+  const { requiresAuth } = usePasswordModal();
+  if (requiresAuth === null || requiresAuth) {
+    return (
+      <>
+        {requiresAuth && <PasswordModal />}
+        <div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
+          <SidebarPlaceholder />
+          <ChatPlaceholder />
+        </div>
+      </>
+    );
+  }
+
   return (
     <div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
       <Sidebar />
diff --git a/frontend/src/pages/WorkspaceChat/index.jsx b/frontend/src/pages/WorkspaceChat/index.jsx
index b891295ca..45fe5d31f 100644
--- a/frontend/src/pages/WorkspaceChat/index.jsx
+++ b/frontend/src/pages/WorkspaceChat/index.jsx
@@ -3,8 +3,30 @@ import { default as WorkspaceChatContainer } from "../../components/WorkspaceCha
 import Sidebar from "../../components/Sidebar";
 import { useParams } from "react-router-dom";
 import Workspace from "../../models/workspace";
+import SidebarPlaceholder from "../../components/Sidebar/Placeholder";
+import ChatPlaceholder from "../../components/WorkspaceChat/LoadingChat";
+import PasswordModal, {
+  usePasswordModal,
+} from "../../components/Modals/Password";
 
 export default function WorkspaceChat() {
+  const { requiresAuth } = usePasswordModal();
+  if (requiresAuth === null || requiresAuth) {
+    return (
+      <>
+        {requiresAuth && <PasswordModal />}
+        <div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
+          <SidebarPlaceholder />
+          <ChatPlaceholder />
+        </div>
+      </>
+    );
+  }
+
+  return <ShowWorkspaceChat />;
+}
+
+function ShowWorkspaceChat() {
   const { slug } = useParams();
   const [workspace, setWorkspace] = useState(null);
   const [loading, setLoading] = useState(true);
diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js
new file mode 100644
index 000000000..4de7681e9
--- /dev/null
+++ b/frontend/src/utils/request.js
@@ -0,0 +1,9 @@
+// Sets up the base headers for all authenticated requests so that we are able to prevent
+// basic spoofing since a valid token is required and that cannot be spoofed
+export function baseHeaders(providedToken = null) {
+  const token =
+    providedToken || window.localStorage.getItem("anythingllm_authtoken");
+  return {
+    Authorization: token ? `Bearer ${token}` : null,
+  };
+}
diff --git a/server/.env.example b/server/.env.example
index 541134f0c..3934fd6a2 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -18,4 +18,5 @@ PINECONE_INDEX=
 
 # CLOUD DEPLOYMENT VARIRABLES ONLY
 # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting.
+# JWT_SECRET="my-random-string-for-seeding" # Only needed if AUTH_TOKEN is set. Please generate random string at least 12 chars long.
 # STORAGE_DIR= # absolute filesystem path with no trailing slash
\ No newline at end of file
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index f03cb368a..1d225d0bf 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -3,6 +3,7 @@ process.env.NODE_ENV === "development"
   : require("dotenv").config();
 const { viewLocalFiles } = require("../utils/files");
 const { getVectorDbClass } = require("../utils/helpers");
+const { reqBody, makeJWT } = require("../utils/http");
 
 function systemEndpoints(app) {
   if (!app) return;
@@ -15,6 +16,7 @@ function systemEndpoints(app) {
     try {
       const vectorDB = process.env.VECTOR_DB || "pinecone";
       const results = {
+        RequiresAuth: !!process.env.AUTH_TOKEN,
         VectorDB: vectorDB,
         OpenAiKey: !!process.env.OPEN_AI_KEY,
         OpenAiModelPref: process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo",
@@ -38,7 +40,37 @@ function systemEndpoints(app) {
     }
   });
 
-  app.get("/system-vectors", async (_, response) => {
+  app.get("/system/check-token", (_, response) => {
+    response.sendStatus(200).end();
+  });
+
+  app.post("/request-token", (request, response) => {
+    try {
+      const { password } = reqBody(request);
+      if (password !== process.env.AUTH_TOKEN) {
+        response
+          .status(402)
+          .json({
+            valid: false,
+            token: null,
+            message: "Invalid password provided",
+          });
+        return;
+      }
+
+      response.status(200).json({
+        valid: true,
+        token: makeJWT({ p: password }, "30d"),
+        message: null,
+      });
+      return;
+    } catch (e) {
+      console.log(e.message, e);
+      response.sendStatus(500).end();
+    }
+  });
+
+  app.get("/system/system-vectors", async (_, response) => {
     try {
       const VectorDb = getVectorDbClass();
       const vectorCount = await VectorDb.totalIndicies();
@@ -49,7 +81,7 @@ function systemEndpoints(app) {
     }
   });
 
-  app.get("/local-files", async (_, response) => {
+  app.get("/system/local-files", async (_, response) => {
     try {
       const localFiles = await viewLocalFiles();
       response.status(200).json({ localFiles });
diff --git a/server/index.js b/server/index.js
index 648d34c03..3eec6198d 100644
--- a/server/index.js
+++ b/server/index.js
@@ -14,7 +14,6 @@ const { getVectorDbClass } = require("./utils/helpers");
 const app = express();
 
 app.use(cors({ origin: true }));
-app.use(validatedRequest);
 app.use(bodyParser.text());
 app.use(bodyParser.json());
 app.use(
@@ -23,6 +22,8 @@ app.use(
   })
 );
 
+app.use("/system/*", validatedRequest);
+app.use("/workspace/*", validatedRequest);
 systemEndpoints(app);
 workspaceEndpoints(app);
 chatEndpoints(app);
diff --git a/server/package.json b/server/package.json
index 31ae23c63..70f6e3a72 100644
--- a/server/package.json
+++ b/server/package.json
@@ -30,6 +30,7 @@
     "sqlite": "^4.2.1",
     "sqlite3": "^5.1.6",
     "uuid": "^9.0.0",
+    "jsonwebtoken": "^8.5.1",
     "vectordb": "0.1.5-beta"
   },
   "devDependencies": {
diff --git a/server/utils/http/index.js b/server/utils/http/index.js
index e4f2813d4..af42f5de5 100644
--- a/server/utils/http/index.js
+++ b/server/utils/http/index.js
@@ -1,3 +1,9 @@
+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"
     ? JSON.parse(request.body)
@@ -8,7 +14,21 @@ function queryParams(request) {
   return request.query;
 }
 
+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 });
+}
+
+function decodeJWT(jwtToken) {
+  try {
+    return JWT.verify(jwtToken, SECRET);
+  } catch {}
+  return null;
+}
+
 module.exports = {
   reqBody,
   queryParams,
+  makeJWT,
+  decodeJWT,
 };
diff --git a/server/utils/middleware/validatedRequest.js b/server/utils/middleware/validatedRequest.js
index a1d9bf3b6..4e7c519a8 100644
--- a/server/utils/middleware/validatedRequest.js
+++ b/server/utils/middleware/validatedRequest.js
@@ -1,6 +1,13 @@
+const { decodeJWT } = require("../http");
+
 function validatedRequest(request, response, next) {
   // When in development passthrough auth token for ease of development.
-  if (process.env.NODE_ENV === "development" || !process.env.AUTH_TOKEN) {
+  // Or if the user simply did not set an Auth token or JWT Secret
+  if (
+    process.env.NODE_ENV === "development" ||
+    !process.env.AUTH_TOKEN ||
+    !process.env.JWT_SECRET
+  ) {
     next();
     return;
   }
@@ -22,7 +29,8 @@ function validatedRequest(request, response, next) {
     return;
   }
 
-  if (token !== process.env.AUTH_TOKEN) {
+  const { p } = decodeJWT(token);
+  if (p !== process.env.AUTH_TOKEN) {
     response.status(403).json({
       error: "Invalid auth token found.",
     });
diff --git a/server/utils/vectorDbProviders/lance/index.js b/server/utils/vectorDbProviders/lance/index.js
index 8617a423f..e064154fa 100644
--- a/server/utils/vectorDbProviders/lance/index.js
+++ b/server/utils/vectorDbProviders/lance/index.js
@@ -26,8 +26,9 @@ function curateLanceSources(sources = []) {
 }
 
 const LanceDb = {
-  uri: `${!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "./"
-    }lancedb`,
+  uri: `${
+    !!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "./"
+  }lancedb`,
   name: "LanceDb",
   connect: async function () {
     if (process.env.VECTOR_DB !== "lancedb")
@@ -282,4 +283,4 @@ const LanceDb = {
   },
 };
 
-module.exports.LanceDb = LanceDb
+module.exports.LanceDb = LanceDb;