From fa29003a46a9734f65dcbf6f24249e4d02471b4e Mon Sep 17 00:00:00 2001
From: Sean Hatfield <seanhatfield5@gmail.com>
Date: Mon, 13 Nov 2023 14:51:16 -0800
Subject: [PATCH] Create manager role and limit default role (#351)

* added manager role to options

* block default role from editing workspace settings on workspace and text input box

* block default user from accessing settings at all

* create manager route

* let pass through if in single user mode

* fix permissions for manager and admin roles in settings

* fix settings button for single user and remove unneeded console.logs

* rename routes and paths for clarity

* admin, manager, default roles complete

* remove unneeded comments

* consistency changes

* manage permissions for mum modes

* update sidebar for single-user mode

* update comment on middleware
Modify permission setting for admins

* update render conditional

* Add role usage hint to each role

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
---
 frontend/src/App.jsx                          |  58 ++---
 .../LLMSelection/AnthropicAiOptions/index.jsx |   2 +-
 .../LLMSelection/LMStudioOptions/index.jsx    |   2 +-
 .../Modals/MangeWorkspace/index.jsx           |   7 +-
 .../src/components/PrivateRoute/index.jsx     |  33 ++-
 .../src/components/SettingsSidebar/index.jsx  | 177 +++++++-------
 .../Sidebar/ActiveWorkspaces/index.jsx        |   4 +-
 frontend/src/components/Sidebar/index.jsx     |  62 ++---
 .../ChatContainer/PromptInput/index.jsx       |  15 +-
 .../pages/Admin/Users/NewUserModal/index.jsx  |  12 +-
 .../Users/UserRow/EditUserModal/index.jsx     |  11 +-
 .../src/pages/Admin/Users/UserRow/index.jsx   |  22 +-
 frontend/src/pages/Admin/Users/index.jsx      |  32 +++
 .../pages/GeneralSettings/Security/index.jsx  |   2 +-
 frontend/src/utils/paths.js                   |  51 ++---
 server/endpoints/admin.js                     | 215 ++++++------------
 server/endpoints/system.js                    |  74 +-----
 server/endpoints/workspaces.js                |  60 +++--
 server/models/workspace.js                    |   5 +-
 server/utils/middleware/multiUserProtected.js |  41 ++++
 server/utils/middleware/validatedRequest.js   |  19 +-
 21 files changed, 455 insertions(+), 449 deletions(-)
 create mode 100644 server/utils/middleware/multiUserProtected.js

diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 042fde70f..2b8a645b3 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,7 +1,10 @@
 import React, { lazy, Suspense } from "react";
 import { Routes, Route } from "react-router-dom";
 import { ContextWrapper } from "./AuthContext";
-import PrivateRoute, { AdminRoute } from "./components/PrivateRoute";
+import PrivateRoute, {
+  AdminRoute,
+  ManagerRoute,
+} from "./components/PrivateRoute";
 import { ToastContainer } from "react-toastify";
 import "react-toastify/dist/ReactToastify.css";
 import Login from "./pages/Login";
@@ -48,56 +51,55 @@ export default function App() {
           />
           <Route path="/accept-invite/:code" element={<InvitePage />} />
 
-          {/* General Routes */}
+          {/* Admin */}
           <Route
-            path="/general/llm-preference"
-            element={<PrivateRoute Component={GeneralLLMPreference} />}
+            path="/settings/llm-preference"
+            element={<AdminRoute Component={GeneralLLMPreference} />}
           />
           <Route
-            path="/general/embedding-preference"
-            element={<PrivateRoute Component={GeneralEmbeddingPreference} />}
+            path="/settings/embedding-preference"
+            element={<AdminRoute Component={GeneralEmbeddingPreference} />}
           />
           <Route
-            path="/general/vector-database"
-            element={<PrivateRoute Component={GeneralVectorDatabase} />}
+            path="/settings/vector-database"
+            element={<AdminRoute Component={GeneralVectorDatabase} />}
           />
+          {/* Manager */}
           <Route
-            path="/general/export-import"
-            element={<PrivateRoute Component={GeneralExportImport} />}
+            path="/settings/export-import"
+            element={<ManagerRoute Component={GeneralExportImport} />}
           />
           <Route
-            path="/general/security"
-            element={<PrivateRoute Component={GeneralSecurity} />}
+            path="/settings/security"
+            element={<ManagerRoute Component={GeneralSecurity} />}
           />
           <Route
-            path="/general/appearance"
-            element={<PrivateRoute Component={GeneralAppearance} />}
+            path="/settings/appearance"
+            element={<ManagerRoute Component={GeneralAppearance} />}
           />
           <Route
-            path="/general/api-keys"
-            element={<PrivateRoute Component={GeneralApiKeys} />}
+            path="/settings/api-keys"
+            element={<ManagerRoute Component={GeneralApiKeys} />}
           />
           <Route
-            path="/general/workspace-chats"
-            element={<PrivateRoute Component={GeneralChats} />}
+            path="/settings/workspace-chats"
+            element={<ManagerRoute Component={GeneralChats} />}
           />
-
-          {/* Admin Routes */}
           <Route
-            path="/admin/system-preferences"
-            element={<AdminRoute Component={AdminSystem} />}
+            path="/settings/system-preferences"
+            element={<ManagerRoute Component={AdminSystem} />}
           />
           <Route
-            path="/admin/invites"
-            element={<AdminRoute Component={AdminInvites} />}
+            path="/settings/invites"
+            element={<ManagerRoute Component={AdminInvites} />}
           />
           <Route
-            path="/admin/users"
-            element={<AdminRoute Component={AdminUsers} />}
+            path="/settings/users"
+            element={<ManagerRoute Component={AdminUsers} />}
           />
           <Route
-            path="/admin/workspaces"
-            element={<AdminRoute Component={AdminWorkspaces} />}
+            path="/settings/workspaces"
+            element={<ManagerRoute Component={AdminWorkspaces} />}
           />
           {/* Onboarding Flow */}
           <Route path="/onboarding" element={<OnboardingFlow />} />
diff --git a/frontend/src/components/LLMSelection/AnthropicAiOptions/index.jsx b/frontend/src/components/LLMSelection/AnthropicAiOptions/index.jsx
index 520941057..454ab5f98 100644
--- a/frontend/src/components/LLMSelection/AnthropicAiOptions/index.jsx
+++ b/frontend/src/components/LLMSelection/AnthropicAiOptions/index.jsx
@@ -14,7 +14,7 @@ export default function AnthropicAiOptions({ settings, showAlert = false }) {
             </p>
           </div>
           <a
-            href={paths.general.embeddingPreference()}
+            href={paths.settings.embeddingPreference()}
             className="text-sm md:text-base my-2 underline"
           >
             Manage embedding &rarr;
diff --git a/frontend/src/components/LLMSelection/LMStudioOptions/index.jsx b/frontend/src/components/LLMSelection/LMStudioOptions/index.jsx
index 1f00c070d..883b6e8ef 100644
--- a/frontend/src/components/LLMSelection/LMStudioOptions/index.jsx
+++ b/frontend/src/components/LLMSelection/LMStudioOptions/index.jsx
@@ -14,7 +14,7 @@ export default function LMStudioOptions({ settings, showAlert = false }) {
             </p>
           </div>
           <a
-            href={paths.general.embeddingPreference()}
+            href={paths.settings.embeddingPreference()}
             className="text-sm md:text-base my-2 underline"
           >
             Manage embedding &rarr;
diff --git a/frontend/src/components/Modals/MangeWorkspace/index.jsx b/frontend/src/components/Modals/MangeWorkspace/index.jsx
index 7f9d920bb..d38cc3530 100644
--- a/frontend/src/components/Modals/MangeWorkspace/index.jsx
+++ b/frontend/src/components/Modals/MangeWorkspace/index.jsx
@@ -4,6 +4,7 @@ import { useParams } from "react-router-dom";
 import Workspace from "../../../models/workspace";
 import System from "../../../models/system";
 import { isMobile } from "react-device-detect";
+import useUser from "../../../hooks/useUser";
 
 const DocumentSettings = lazy(() => import("./Documents"));
 const WorkspaceSettings = lazy(() => import("./Settings"));
@@ -117,9 +118,13 @@ const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
 
 export default memo(ManageWorkspace);
 export function useManageWorkspaceModal() {
+  const { user } = useUser();
   const [showing, setShowing] = useState(false);
+
   const showModal = () => {
-    setShowing(true);
+    if (user?.role !== "default") {
+      setShowing(true);
+    }
   };
 
   const hideModal = () => {
diff --git a/frontend/src/components/PrivateRoute/index.jsx b/frontend/src/components/PrivateRoute/index.jsx
index e8db5400d..7ca949b26 100644
--- a/frontend/src/components/PrivateRoute/index.jsx
+++ b/frontend/src/components/PrivateRoute/index.jsx
@@ -14,6 +14,7 @@ function useIsAuthenticated() {
   const [isAuthd, setIsAuthed] = useState(null);
   const [shouldRedirectToOnboarding, setShouldRedirectToOnboarding] =
     useState(false);
+  const [multiUserMode, setMultiUserMode] = useState(false);
 
   useEffect(() => {
     const validateSession = async () => {
@@ -25,6 +26,8 @@ function useIsAuthenticated() {
         AzureOpenAiKey = false,
       } = await System.keys();
 
+      setMultiUserMode(MultiUserMode);
+
       // Check for the onboarding redirect condition
       if (
         !MultiUserMode &&
@@ -77,11 +80,35 @@ function useIsAuthenticated() {
     validateSession();
   }, []);
 
-  return { isAuthd, shouldRedirectToOnboarding };
+  return { isAuthd, shouldRedirectToOnboarding, multiUserMode };
 }
 
+// Allows only admin to access the route and if in single user mode,
+// allows all users to access the route
 export function AdminRoute({ Component }) {
-  const { isAuthd, shouldRedirectToOnboarding } = useIsAuthenticated();
+  const { isAuthd, shouldRedirectToOnboarding, multiUserMode } =
+    useIsAuthenticated();
+  if (isAuthd === null) return <FullScreenLoader />;
+
+  if (shouldRedirectToOnboarding) {
+    return <Navigate to={paths.onboarding()} />;
+  }
+
+  const user = userFromStorage();
+  return isAuthd && (user?.role === "admin" || !multiUserMode) ? (
+    <UserMenu>
+      <Component />
+    </UserMenu>
+  ) : (
+    <Navigate to={paths.home()} />
+  );
+}
+
+// Allows manager and admin to access the route and if in single user mode,
+// allows all users to access the route
+export function ManagerRoute({ Component }) {
+  const { isAuthd, shouldRedirectToOnboarding, multiUserMode } =
+    useIsAuthenticated();
   if (isAuthd === null) return <FullScreenLoader />;
 
   if (shouldRedirectToOnboarding) {
@@ -89,7 +116,7 @@ export function AdminRoute({ Component }) {
   }
 
   const user = userFromStorage();
-  return isAuthd && user?.role === "admin" ? (
+  return isAuthd && (user?.role !== "default" || !multiUserMode) ? (
     <UserMenu>
       <Component />
     </UserMenu>
diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx
index 9b0c25531..887f8af35 100644
--- a/frontend/src/components/SettingsSidebar/index.jsx
+++ b/frontend/src/components/SettingsSidebar/index.jsx
@@ -65,96 +65,84 @@ export default function SettingsSidebar() {
           <div className="h-[100%] flex flex-col w-full justify-between pt-4 overflow-y-hidden">
             <div className="h-auto sidebar-items">
               <div className="flex flex-col gap-y-2 h-[65vh] pb-8 overflow-y-scroll no-scroll">
-                {/* Admin Settings */}
-                {user?.role === "admin" && (
+                {/* Admin/manager Multi-user Settings */}
+                {!!user && user?.role !== "default" && (
                   <>
                     <Option
-                      href={paths.admin.system()}
+                      href={paths.settings.system()}
                       btnText="System Preferences"
                       icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
                     />
                     <Option
-                      href={paths.admin.invites()}
+                      href={paths.settings.invites()}
                       btnText="Invitation"
                       icon={
                         <EnvelopeSimple className="h-5 w-5 flex-shrink-0" />
                       }
                     />
                     <Option
-                      href={paths.admin.users()}
+                      href={paths.settings.users()}
                       btnText="Users"
                       icon={<Users className="h-5 w-5 flex-shrink-0" />}
                     />
                     <Option
-                      href={paths.admin.workspaces()}
+                      href={paths.settings.workspaces()}
                       btnText="Workspaces"
                       icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
                     />
-                    <Option
-                      href={paths.general.chats()}
-                      btnText="Workspace Chat"
-                      icon={
-                        <ChatCenteredText className="h-5 w-5 flex-shrink-0" />
-                      }
-                    />
                   </>
                 )}
 
-                {/* General Settings */}
                 <Option
-                  href={paths.general.appearance()}
+                  href={paths.settings.chats()}
+                  btnText="Workspace Chat"
+                  icon={<ChatCenteredText className="h-5 w-5 flex-shrink-0" />}
+                />
+
+                <Option
+                  href={paths.settings.appearance()}
                   btnText="Appearance"
                   icon={<Eye className="h-5 w-5 flex-shrink-0" />}
                 />
                 <Option
-                  href={paths.general.apiKeys()}
+                  href={paths.settings.apiKeys()}
                   btnText="API Keys"
                   icon={<Key className="h-5 w-5 flex-shrink-0" />}
                 />
+
+                {(!user || user?.role === "admin") && (
+                  <>
+                    <Option
+                      href={paths.settings.llmPreference()}
+                      btnText="LLM Preference"
+                      icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
+                    />
+                    <Option
+                      href={paths.settings.embeddingPreference()}
+                      btnText="Embedding Preference"
+                      icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
+                    />
+                    <Option
+                      href={paths.settings.vectorDatabase()}
+                      btnText="Vector Database"
+                      icon={<Database className="h-5 w-5 flex-shrink-0" />}
+                    />
+                  </>
+                )}
+
                 <Option
-                  href={paths.general.llmPreference()}
-                  btnText="LLM Preference"
-                  icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
-                />
-                <Option
-                  href={paths.general.embeddingPreference()}
-                  btnText="Embedding Preference"
-                  icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
-                />
-                <Option
-                  href={paths.general.vectorDatabase()}
-                  btnText="Vector Database"
-                  icon={<Database className="h-5 w-5 flex-shrink-0" />}
-                />
-                <Option
-                  href={paths.general.exportImport()}
+                  href={paths.settings.exportImport()}
                   btnText="Export or Import"
                   icon={<DownloadSimple className="h-5 w-5 flex-shrink-0" />}
                 />
-                {!user && (
-                  <Option
-                    href={paths.general.chats()}
-                    btnText="Chat History"
-                    icon={
-                      <ChatCenteredText className="h-5 w-5 flex-shrink-0" />
-                    }
-                  />
-                )}
                 <Option
-                  href={paths.general.security()}
+                  href={paths.settings.security()}
                   btnText="Security"
                   icon={<Lock className="h-5 w-5 flex-shrink-0" />}
                 />
               </div>
             </div>
             <div>
-              {/* <div className="flex flex-col gap-y-2">
-                <div className="w-full flex items-center justify-between">
-                  <LLMStatus />
-                  <IndexCount />
-                </div>
-              </div> */}
-
               {/* Footer */}
               <div className="flex justify-center mt-2">
                 <div className="flex space-x-4">
@@ -277,73 +265,70 @@ export function SidebarMobileHeader() {
                   style={{ height: "calc(100vw - -3rem)" }}
                   className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll"
                 >
-                  {user?.role === "admin" && (
-                    <>
-                      <Option
-                        href={paths.admin.system()}
-                        btnText="System Preferences"
-                        icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
-                      />
-                      <Option
-                        href={paths.admin.invites()}
-                        btnText="Invitation"
-                        icon={
-                          <EnvelopeSimple className="h-5 w-5 flex-shrink-0" />
-                        }
-                      />
-                      <Option
-                        href={paths.admin.users()}
-                        btnText="Users"
-                        icon={<Users className="h-5 w-5 flex-shrink-0" />}
-                      />
-                      <Option
-                        href={paths.admin.workspaces()}
-                        btnText="Workspaces"
-                        icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
-                      />
-                    </>
-                  )}
+                  <Option
+                    href={paths.settings.system()}
+                    btnText="System Preferences"
+                    icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
+                  />
+                  <Option
+                    href={paths.settings.invites()}
+                    btnText="Invitation"
+                    icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />}
+                  />
+                  <Option
+                    href={paths.settings.users()}
+                    btnText="Users"
+                    icon={<Users className="h-5 w-5 flex-shrink-0" />}
+                  />
+                  <Option
+                    href={paths.settings.workspaces()}
+                    btnText="Workspaces"
+                    icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
+                  />
 
-                  {/* General Settings */}
                   <Option
-                    href={paths.general.chats()}
+                    href={paths.settings.chats()}
                     btnText="Workspace Chat"
                     icon={
                       <ChatCenteredText className="h-5 w-5 flex-shrink-0" />
                     }
                   />
                   <Option
-                    href={paths.general.appearance()}
+                    href={paths.settings.appearance()}
                     btnText="Appearance"
                     icon={<Eye className="h-5 w-5 flex-shrink-0" />}
                   />
                   <Option
-                    href={paths.general.apiKeys()}
+                    href={paths.settings.apiKeys()}
                     btnText="API Keys"
                     icon={<Key className="h-5 w-5 flex-shrink-0" />}
                   />
+                  {(!user || user?.role === "admin") && (
+                    <>
+                      <Option
+                        href={paths.settings.llmPreference()}
+                        btnText="LLM Preference"
+                        icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
+                      />
+                      <Option
+                        href={paths.settings.embeddingPreference()}
+                        btnText="Embedding Preference"
+                        icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
+                      />
+                      <Option
+                        href={paths.settings.vectorDatabase()}
+                        btnText="Vector Database"
+                        icon={<Database className="h-5 w-5 flex-shrink-0" />}
+                      />
+                    </>
+                  )}
                   <Option
-                    href={paths.general.llmPreference()}
-                    btnText="LLM Preference"
-                    icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
-                  />
-                  <Option
-                    href={paths.general.embeddingPreference()}
-                    btnText="Embedding Preference"
-                    icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
-                  />
-                  <Option
-                    href={paths.general.vectorDatabase()}
-                    btnText="Vector Database"
-                    icon={<Database className="h-5 w-5 flex-shrink-0" />}
-                  />
-                  <Option
-                    href={paths.general.exportImport()}
+                    href={paths.settings.exportImport()}
                     btnText="Export or Import"
                     icon={<DownloadSimple className="h-5 w-5 flex-shrink-0" />}
                   />
                   <Option
-                    href={paths.general.security()}
+                    href={paths.settings.security()}
                     btnText="Security"
                     icon={<Lock className="h-5 w-5 flex-shrink-0" />}
                   />
diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx
index 8c37c0534..b32d2b02a 100644
--- a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx
+++ b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx
@@ -9,6 +9,7 @@ import paths from "../../../utils/paths";
 import { useParams } from "react-router-dom";
 import { GearSix, SquaresFour } from "@phosphor-icons/react";
 import truncate from "truncate";
+import useUser from "../../../hooks/useUser";
 
 export default function ActiveWorkspaces() {
   const { slug } = useParams();
@@ -17,6 +18,7 @@ export default function ActiveWorkspaces() {
   const [workspaces, setWorkspaces] = useState([]);
   const [selectedWs, setSelectedWs] = useState(null);
   const { showing, showModal, hideModal } = useManageWorkspaceModal();
+  const { user } = useUser();
 
   useEffect(() => {
     async function getWorkspaces() {
@@ -90,7 +92,7 @@ export default function ActiveWorkspaces() {
                 >
                   <GearSix
                     weight={settingHover ? "fill" : "regular"}
-                    hidden={!isActive}
+                    hidden={!isActive || user?.role === "default"}
                     className="h-[20px] w-[20px] transition-all duration-300"
                   />
                 </button>
diff --git a/frontend/src/components/Sidebar/index.jsx b/frontend/src/components/Sidebar/index.jsx
index b4d149073..01484dd73 100644
--- a/frontend/src/components/Sidebar/index.jsx
+++ b/frontend/src/components/Sidebar/index.jsx
@@ -15,8 +15,10 @@ import ActiveWorkspaces from "./ActiveWorkspaces";
 import paths from "../../utils/paths";
 import { USER_BACKGROUND_COLOR } from "../../utils/constants";
 import useLogo from "../../hooks/useLogo";
+import useUser from "../../hooks/useUser";
 
 export default function Sidebar() {
+  const { user } = useUser();
   const { logo } = useLogo();
   const sidebarRef = useRef(null);
   const {
@@ -43,25 +45,28 @@ export default function Sidebar() {
                 style={{ objectFit: "contain" }}
               />
             </div>
-            <div className="flex gap-x-2 items-center text-slate-200">
-              {/* <AdminHome /> */}
-              <SettingsButton />
-            </div>
+            {(!user || user?.role !== "default") && (
+              <div className="flex gap-x-2 items-center text-slate-200">
+                <SettingsButton />
+              </div>
+            )}
           </div>
 
           {/* Primary Body */}
           <div className="flex-grow flex flex-col">
             <div className="flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll">
               <div className="flex gap-x-2 items-center justify-between">
-                <button
-                  onClick={showNewWsModal}
-                  className="flex flex-grow w-[75%] h-[44px] gap-x-2 py-[5px] px-4 bg-white rounded-lg text-sidebar justify-center items-center hover:bg-opacity-80 transition-all duration-300"
-                >
-                  <Plus className="h-5 w-5" />
-                  <p className="text-sidebar text-sm font-semibold">
-                    New Workspace
-                  </p>
-                </button>
+                {(!user || user?.role !== "default") && (
+                  <button
+                    onClick={showNewWsModal}
+                    className="flex flex-grow w-[75%] h-[44px] gap-x-2 py-[5px] px-4 bg-white rounded-lg text-sidebar justify-center items-center hover:bg-opacity-80 transition-all duration-300"
+                  >
+                    <Plus className="h-5 w-5" />
+                    <p className="text-sidebar text-sm font-semibold">
+                      New Workspace
+                    </p>
+                  </button>
+                )}
               </div>
               <ActiveWorkspaces />
             </div>
@@ -133,6 +138,7 @@ export function SidebarMobileHeader() {
     showModal: showNewWsModal,
     hideModal: hideNewWsModal,
   } = useNewWorkspaceModal();
+  const { user } = useUser();
 
   useEffect(() => {
     // Darkens the rest of the screen
@@ -197,9 +203,11 @@ export function SidebarMobileHeader() {
                   style={{ objectFit: "contain" }}
                 />
               </div>
-              <div className="flex gap-x-2 items-center text-slate-500 shink-0">
-                <SettingsButton />
-              </div>
+              {(!user || user?.role !== "default") && (
+                <div className="flex gap-x-2 items-center text-slate-500 shink-0">
+                  <SettingsButton />
+                </div>
+              )}
             </div>
 
             {/* Primary Body */}
@@ -210,15 +218,17 @@ export function SidebarMobileHeader() {
                   className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll"
                 >
                   <div className="flex gap-x-2 items-center justify-between">
-                    <button
-                      onClick={showNewWsModal}
-                      className="flex flex-grow w-[75%] h-[44px] gap-x-2 py-[5px] px-4 bg-white rounded-lg text-sidebar justify-center items-center hover:bg-opacity-80 transition-all duration-300"
-                    >
-                      <Plus className="h-5 w-5" />
-                      <p className="text-sidebar text-sm font-semibold">
-                        New Workspace
-                      </p>
-                    </button>
+                    {(!user || user?.role !== "default") && (
+                      <button
+                        onClick={showNewWsModal}
+                        className="flex flex-grow w-[75%] h-[44px] gap-x-2 py-[5px] px-4 bg-white rounded-lg text-sidebar justify-center items-center hover:bg-opacity-80 transition-all duration-300"
+                      >
+                        <Plus className="h-5 w-5" />
+                        <p className="text-sidebar text-sm font-semibold">
+                          New Workspace
+                        </p>
+                      </button>
+                    )}
                   </div>
                   <ActiveWorkspaces />
                 </div>
@@ -266,7 +276,7 @@ export function SidebarMobileHeader() {
 function SettingsButton() {
   return (
     <a
-      href={paths.general.llmPreference()}
+      href={paths.settings.system()}
       className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
     >
       <Wrench className="h-4 w-4" weight="fill" />
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx
index 60134bd98..22dbdf320 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx
@@ -10,6 +10,7 @@ import { isMobile } from "react-device-detect";
 import ManageWorkspace, {
   useManageWorkspaceModal,
 } from "../../../Modals/MangeWorkspace";
+import useUser from "../../../../hooks/useUser";
 
 export default function PromptInput({
   workspace,
@@ -22,6 +23,7 @@ export default function PromptInput({
   const { showing, showModal, hideModal } = useManageWorkspaceModal();
   const formRef = useRef(null);
   const [_, setFocused] = useState(false);
+  const { user } = useUser();
 
   const handleSubmit = (e) => {
     setFocused(false);
@@ -86,11 +88,14 @@ export default function PromptInput({
             </div>
             <div className="flex justify-between py-3.5">
               <div className="flex gap-2">
-                <Gear
-                  onClick={showModal}
-                  className="w-7 h-7 text-white/60 hover:text-white cursor-pointer"
-                  weight="fill"
-                />
+                {user?.role !== "default" && (
+                  <Gear
+                    onClick={showModal}
+                    className="w-7 h-7 text-white/60 hover:text-white cursor-pointer"
+                    weight="fill"
+                  />
+                )}
+
                 <ChatModeSelector workspace={workspace} />
                 {/* <TextT
                   className="w-7 h-7 text-white/30 cursor-not-allowed"
diff --git a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx
index c3d2b64aa..9f2b42aeb 100644
--- a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx
+++ b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx
@@ -1,6 +1,8 @@
 import React, { useState } from "react";
 import { X } from "@phosphor-icons/react";
 import Admin from "../../../../models/admin";
+import { userFromStorage } from "../../../../utils/request";
+import { RoleHintDisplay } from "..";
 
 const DIALOG_ID = `new-user-modal`;
 
@@ -11,6 +13,7 @@ function hideModal() {
 export const NewUserModalId = DIALOG_ID;
 export default function NewUserModal() {
   const [error, setError] = useState(null);
+  const [role, setRole] = useState("default");
   const handleCreate = async (e) => {
     setError(null);
     e.preventDefault();
@@ -22,6 +25,8 @@ export default function NewUserModal() {
     setError(error);
   };
 
+  const user = userFromStorage();
+
   return (
     <dialog id={DIALOG_ID} className="bg-transparent outline-none">
       <div className="relative w-full max-w-2xl max-h-full">
@@ -87,11 +92,16 @@ export default function NewUserModal() {
                     name="role"
                     required={true}
                     defaultValue={"default"}
+                    onChange={(e) => setRole(e.target.value)}
                     className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500"
                   >
                     <option value="default">Default</option>
-                    <option value="admin">Administrator</option>
+                    <option value="manager">Manager </option>
+                    {user?.role === "admin" && (
+                      <option value="admin">Administrator</option>
+                    )}
                   </select>
+                  <RoleHintDisplay role={role} />
                 </div>
                 {error && (
                   <p className="text-red-400 text-sm">Error: {error}</p>
diff --git a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx
index c2e48fdbc..c3b6a939d 100644
--- a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx
+++ b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx
@@ -1,10 +1,12 @@
 import React, { useState } from "react";
 import { X } from "@phosphor-icons/react";
 import Admin from "../../../../../models/admin";
+import { RoleHintDisplay } from "../..";
 
 export const EditUserModalId = (user) => `edit-user-${user.id}-modal`;
 
-export default function EditUserModal({ user }) {
+export default function EditUserModal({ currentUser, user }) {
+  const [role, setRole] = useState(user.role);
   const [error, setError] = useState(null);
 
   const hideModal = () => {
@@ -90,11 +92,16 @@ export default function EditUserModal({ user }) {
                     name="role"
                     required={true}
                     defaultValue={user.role}
+                    onChange={(e) => setRole(e.target.value)}
                     className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border border-gray-500 focus:ring-blue-500 focus:border-blue-500"
                   >
                     <option value="default">Default</option>
-                    <option value="admin">Administrator</option>
+                    <option value="manager">Manager</option>
+                    {currentUser?.role === "admin" && (
+                      <option value="admin">Administrator</option>
+                    )}
                   </select>
+                  <RoleHintDisplay role={role} />
                 </div>
                 {error && (
                   <p className="text-red-400 text-sm">Error: {error}</p>
diff --git a/frontend/src/pages/Admin/Users/UserRow/index.jsx b/frontend/src/pages/Admin/Users/UserRow/index.jsx
index c4dac62cf..5964b6bdf 100644
--- a/frontend/src/pages/Admin/Users/UserRow/index.jsx
+++ b/frontend/src/pages/Admin/Users/UserRow/index.jsx
@@ -40,15 +40,17 @@ export default function UserRow({ currUser, user }) {
         <td className="px-6 py-4">{titleCase(user.role)}</td>
         <td className="px-6 py-4">{user.createdAt}</td>
         <td className="px-6 py-4 flex items-center gap-x-6">
-          <button
-            onClick={() =>
-              document?.getElementById(EditUserModalId(user))?.showModal()
-            }
-            className="font-medium text-white text-opacity-80 rounded-lg hover:text-white px-2 py-1 hover:text-opacity-60 hover:bg-white hover:bg-opacity-10"
-          >
-            <DotsThreeOutline weight="fill" className="h-5 w-5" />
-          </button>
-          {currUser.id !== user.id && (
+          {currUser?.role !== "default" && (
+            <button
+              onClick={() =>
+                document?.getElementById(EditUserModalId(user))?.showModal()
+              }
+              className="font-medium text-white text-opacity-80 rounded-lg hover:text-white px-2 py-1 hover:text-opacity-60 hover:bg-white hover:bg-opacity-10"
+            >
+              <DotsThreeOutline weight="fill" className="h-5 w-5" />
+            </button>
+          )}
+          {currUser?.id !== user.id && currUser?.role !== "default" && (
             <>
               <button
                 onClick={handleSuspend}
@@ -66,7 +68,7 @@ export default function UserRow({ currUser, user }) {
           )}
         </td>
       </tr>
-      <EditUserModal user={user} />
+      <EditUserModal currentUser={currUser} user={user} />
     </>
   );
 }
diff --git a/frontend/src/pages/Admin/Users/index.jsx b/frontend/src/pages/Admin/Users/index.jsx
index adede4d4c..f43739f25 100644
--- a/frontend/src/pages/Admin/Users/index.jsx
+++ b/frontend/src/pages/Admin/Users/index.jsx
@@ -100,3 +100,35 @@ function UsersContainer() {
     </table>
   );
 }
+
+const ROLE_HINT = {
+  default: [
+    "Can only send chats with workspaces they are added to by admin or managers.",
+    "Cannot modify any settings at all.",
+  ],
+  manager: [
+    "Can view all workspaces and modify all settings.",
+    "Cannot modify LLM, vectorDB, embedding, or other connections.",
+  ],
+  admin: [
+    "Highest user level privilege.",
+    "Can see and do everything across the system.",
+  ],
+};
+
+export function RoleHintDisplay({ role }) {
+  return (
+    <div className="flex flex-col gap-y-1 py-1 pb-4">
+      <p className="text-white/60 font-semibold text-sm">Permissions</p>
+      <ul className="flex flex-col gap-y-1 list-disc px-4">
+        {ROLE_HINT[role ?? "default"].map((hints, i) => {
+          return (
+            <li key={i} className="text-xs text-white/60">
+              {hints}
+            </li>
+          );
+        })}
+      </ul>
+    </div>
+  );
+}
diff --git a/frontend/src/pages/GeneralSettings/Security/index.jsx b/frontend/src/pages/GeneralSettings/Security/index.jsx
index f0ecaf7ef..fbf2dbc84 100644
--- a/frontend/src/pages/GeneralSettings/Security/index.jsx
+++ b/frontend/src/pages/GeneralSettings/Security/index.jsx
@@ -55,7 +55,7 @@ function MultiUserMode() {
           window.localStorage.removeItem(AUTH_USER);
           window.localStorage.removeItem(AUTH_TOKEN);
           window.localStorage.removeItem(AUTH_TIMESTAMP);
-          window.location = paths.admin.users();
+          window.location = paths.settings.users();
         }, 2_000);
         return;
       }
diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js
index cbe19795f..bfeb8b1bd 100644
--- a/frontend/src/utils/paths.js
+++ b/frontend/src/utils/paths.js
@@ -39,47 +39,42 @@ export default {
   apiDocs: () => {
     return `${API_BASE}/docs`;
   },
-  general: {
+  settings: {
+    system: () => {
+      return `/settings/system-preferences`;
+    },
+    users: () => {
+      return `/settings/users`;
+    },
+    invites: () => {
+      return `/settings/invites`;
+    },
+    workspaces: () => {
+      return `/settings/workspaces`;
+    },
+    chats: () => {
+      return "/settings/workspace-chats";
+    },
     llmPreference: () => {
-      return "/general/llm-preference";
+      return "/settings/llm-preference";
     },
     embeddingPreference: () => {
-      return "/general/embedding-preference";
+      return "/settings/embedding-preference";
     },
     vectorDatabase: () => {
-      return "/general/vector-database";
+      return "/settings/vector-database";
     },
     exportImport: () => {
-      return "/general/export-import";
+      return "/settings/export-import";
     },
     security: () => {
-      return "/general/security";
+      return "/settings/security";
     },
     appearance: () => {
-      return "/general/appearance";
+      return "/settings/appearance";
     },
     apiKeys: () => {
-      return "/general/api-keys";
-    },
-    chats: () => {
-      return "/general/workspace-chats";
-    },
-  },
-  admin: {
-    system: () => {
-      return `/admin/system-preferences`;
-    },
-    users: () => {
-      return `/admin/users`;
-    },
-    invites: () => {
-      return `/admin/invites`;
-    },
-    workspaces: () => {
-      return `/admin/workspaces`;
-    },
-    chats: () => {
-      return "/admin/workspace-chats";
+      return "/settings/api-keys";
     },
   },
 };
diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js
index 23949d922..26444f5a2 100644
--- a/server/endpoints/admin.js
+++ b/server/endpoints/admin.js
@@ -7,41 +7,37 @@ const { DocumentVectors } = require("../models/vectors");
 const { Workspace } = require("../models/workspace");
 const { WorkspaceChats } = require("../models/workspaceChats");
 const { getVectorDbClass } = require("../utils/helpers");
-const { userFromSession, reqBody } = require("../utils/http");
+const { reqBody, userFromSession } = require("../utils/http");
+const {
+  strictMultiUserRoleValid,
+} = require("../utils/middleware/multiUserProtected");
 const { validatedRequest } = require("../utils/middleware/validatedRequest");
 
 function adminEndpoints(app) {
   if (!app) return;
 
-  app.get("/admin/users", [validatedRequest], async (request, response) => {
-    try {
-      const user = await userFromSession(request, response);
-      if (!user || user?.role !== "admin") {
-        response.sendStatus(401).end();
-        return;
+  app.get(
+    "/admin/users",
+    [validatedRequest, strictMultiUserRoleValid],
+    async (_request, response) => {
+      try {
+        const users = (await User.where()).map((user) => {
+          const { password, ...rest } = user;
+          return rest;
+        });
+        response.status(200).json({ users });
+      } catch (e) {
+        console.error(e);
+        response.sendStatus(500).end();
       }
-      const users = (await User.where()).map((user) => {
-        const { password, ...rest } = user;
-        return rest;
-      });
-      response.status(200).json({ users });
-    } catch (e) {
-      console.error(e);
-      response.sendStatus(500).end();
     }
-  });
+  );
 
   app.post(
     "/admin/users/new",
-    [validatedRequest],
+    [validatedRequest, strictMultiUserRoleValid],
     async (request, response) => {
       try {
-        const user = await userFromSession(request, response);
-        if (!user || user?.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
-
         const newUserParams = reqBody(request);
         const { user: newUser, error } = await User.create(newUserParams);
         response.status(200).json({ user: newUser, error });
@@ -52,34 +48,27 @@ function adminEndpoints(app) {
     }
   );
 
-  app.post("/admin/user/:id", [validatedRequest], async (request, response) => {
-    try {
-      const user = await userFromSession(request, response);
-      if (!user || user?.role !== "admin") {
-        response.sendStatus(401).end();
-        return;
+  app.post(
+    "/admin/user/:id",
+    [validatedRequest, strictMultiUserRoleValid],
+    async (request, response) => {
+      try {
+        const { id } = request.params;
+        const updates = reqBody(request);
+        const { success, error } = await User.update(id, updates);
+        response.status(200).json({ success, error });
+      } catch (e) {
+        console.error(e);
+        response.sendStatus(500).end();
       }
-
-      const { id } = request.params;
-      const updates = reqBody(request);
-      const { success, error } = await User.update(id, updates);
-      response.status(200).json({ success, error });
-    } catch (e) {
-      console.error(e);
-      response.sendStatus(500).end();
     }
-  });
+  );
 
   app.delete(
     "/admin/user/:id",
-    [validatedRequest],
+    [validatedRequest, strictMultiUserRoleValid],
     async (request, response) => {
       try {
-        const user = await userFromSession(request, response);
-        if (!user || user?.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
         const { id } = request.params;
         await User.delete({ id: Number(id) });
         response.status(200).json({ success: true, error: null });
@@ -90,33 +79,26 @@ function adminEndpoints(app) {
     }
   );
 
-  app.get("/admin/invites", [validatedRequest], async (request, response) => {
-    try {
-      const user = await userFromSession(request, response);
-      if (!user || user?.role !== "admin") {
-        response.sendStatus(401).end();
-        return;
+  app.get(
+    "/admin/invites",
+    [validatedRequest, strictMultiUserRoleValid],
+    async (_request, response) => {
+      try {
+        const invites = await Invite.whereWithUsers();
+        response.status(200).json({ invites });
+      } catch (e) {
+        console.error(e);
+        response.sendStatus(500).end();
       }
-
-      const invites = await Invite.whereWithUsers();
-      response.status(200).json({ invites });
-    } catch (e) {
-      console.error(e);
-      response.sendStatus(500).end();
     }
-  });
+  );
 
   app.get(
     "/admin/invite/new",
-    [validatedRequest],
+    [validatedRequest, strictMultiUserRoleValid],
     async (request, response) => {
       try {
         const user = await userFromSession(request, response);
-        if (!user || user?.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
-
         const { invite, error } = await Invite.create(user.id);
         response.status(200).json({ invite, error });
       } catch (e) {
@@ -128,15 +110,9 @@ function adminEndpoints(app) {
 
   app.delete(
     "/admin/invite/:id",
-    [validatedRequest],
+    [validatedRequest, strictMultiUserRoleValid],
     async (request, response) => {
       try {
-        const user = await userFromSession(request, response);
-        if (!user || user?.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
-
         const { id } = request.params;
         const { success, error } = await Invite.deactivate(id);
         response.status(200).json({ success, error });
@@ -149,14 +125,9 @@ function adminEndpoints(app) {
 
   app.get(
     "/admin/workspaces",
-    [validatedRequest],
-    async (request, response) => {
+    [validatedRequest, strictMultiUserRoleValid],
+    async (_request, response) => {
       try {
-        const user = await userFromSession(request, response);
-        if (!user || user?.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
         const workspaces = await Workspace.whereWithUsers();
         response.status(200).json({ workspaces });
       } catch (e) {
@@ -168,14 +139,10 @@ function adminEndpoints(app) {
 
   app.post(
     "/admin/workspaces/new",
-    [validatedRequest],
+    [validatedRequest, strictMultiUserRoleValid],
     async (request, response) => {
       try {
         const user = await userFromSession(request, response);
-        if (!user || user?.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
         const { name } = reqBody(request);
         const { workspace, message: error } = await Workspace.new(
           name,
@@ -191,15 +158,9 @@ function adminEndpoints(app) {
 
   app.post(
     "/admin/workspaces/:workspaceId/update-users",
-    [validatedRequest],
+    [validatedRequest, strictMultiUserRoleValid],
     async (request, response) => {
       try {
-        const user = await userFromSession(request, response);
-        if (!user || user?.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
-
         const { workspaceId } = request.params;
         const { userIds } = reqBody(request);
         const { success, error } = await Workspace.updateUsers(
@@ -216,15 +177,9 @@ function adminEndpoints(app) {
 
   app.delete(
     "/admin/workspaces/:id",
-    [validatedRequest],
+    [validatedRequest, strictMultiUserRoleValid],
     async (request, response) => {
       try {
-        const user = await userFromSession(request, response);
-        if (!user || user?.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
-
         const { id } = request.params;
         const VectorDb = getVectorDbClass();
         const workspace = await Workspace.get({ id: Number(id) });
@@ -253,15 +208,9 @@ function adminEndpoints(app) {
 
   app.get(
     "/admin/system-preferences",
-    [validatedRequest],
-    async (request, response) => {
+    [validatedRequest, strictMultiUserRoleValid],
+    async (_request, response) => {
       try {
-        const user = await userFromSession(request, response);
-        if (!user || user?.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
-
         const settings = {
           users_can_delete_workspaces:
             (await SystemSettings.get({ label: "users_can_delete_workspaces" }))
@@ -284,15 +233,9 @@ function adminEndpoints(app) {
 
   app.post(
     "/admin/system-preferences",
-    [validatedRequest],
+    [validatedRequest, strictMultiUserRoleValid],
     async (request, response) => {
       try {
-        const user = await userFromSession(request, response);
-        if (!user || user?.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
-
         const updates = reqBody(request);
         await SystemSettings.updateSettings(updates);
         response.status(200).json({ success: true, error: null });
@@ -303,39 +246,32 @@ function adminEndpoints(app) {
     }
   );
 
-  app.get("/admin/api-keys", [validatedRequest], async (request, response) => {
-    try {
-      const user = await userFromSession(request, response);
-      if (!user || user?.role !== "admin") {
-        response.sendStatus(401).end();
-        return;
+  app.get(
+    "/admin/api-keys",
+    [validatedRequest, strictMultiUserRoleValid],
+    async (_request, response) => {
+      try {
+        const apiKeys = await ApiKey.whereWithUser({});
+        return response.status(200).json({
+          apiKeys,
+          error: null,
+        });
+      } catch (error) {
+        console.error(error);
+        response.status(500).json({
+          apiKey: null,
+          error: "Could not find an API Keys.",
+        });
       }
-
-      const apiKeys = await ApiKey.whereWithUser({});
-      return response.status(200).json({
-        apiKeys,
-        error: null,
-      });
-    } catch (error) {
-      console.error(error);
-      response.status(500).json({
-        apiKey: null,
-        error: "Could not find an API Keys.",
-      });
     }
-  });
+  );
 
   app.post(
     "/admin/generate-api-key",
-    [validatedRequest],
+    [validatedRequest, strictMultiUserRoleValid],
     async (request, response) => {
       try {
         const user = await userFromSession(request, response);
-        if (!user || user?.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
-
         const { apiKey, error } = await ApiKey.create(user.id);
         return response.status(200).json({
           apiKey,
@@ -350,15 +286,10 @@ function adminEndpoints(app) {
 
   app.delete(
     "/admin/delete-api-key/:id",
-    [validatedRequest],
+    [validatedRequest, strictMultiUserRoleValid],
     async (request, response) => {
       try {
         const { id } = request.params;
-        const user = await userFromSession(request, response);
-        if (!user || user?.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
         await ApiKey.delete({ id: Number(id) });
         return response.status(200).end();
       } catch (e) {
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index b6868ec15..f44403530 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -40,6 +40,7 @@ const { ApiKey } = require("../models/apiKeys");
 const { getCustomModels } = require("../utils/helpers/customModels");
 const { WorkspaceChats } = require("../models/workspaceChats");
 const { Workspace } = require("../models/workspace");
+const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected");
 
 function systemEndpoints(app) {
   if (!app) return;
@@ -244,20 +245,10 @@ function systemEndpoints(app) {
 
   app.post(
     "/system/update-env",
-    [validatedRequest],
+    [validatedRequest, flexUserRoleValid],
     async (request, response) => {
       try {
         const body = reqBody(request);
-
-        // Only admins can update the ENV settings.
-        if (multiUserMode(response)) {
-          const user = await userFromSession(request, response);
-          if (!user || user?.role !== "admin") {
-            response.sendStatus(401).end();
-            return;
-          }
-        }
-
         const { newValues, error } = updateENV(body);
         if (process.env.NODE_ENV === "production") await dumpENV();
         response.status(200).json({ newValues, error });
@@ -426,7 +417,7 @@ function systemEndpoints(app) {
 
   app.post(
     "/system/upload-logo",
-    [validatedRequest],
+    [validatedRequest, flexUserRoleValid],
     handleLogoUploads.single("logo"),
     async (request, response) => {
       if (!request.file || !request.file.originalname) {
@@ -440,13 +431,6 @@ function systemEndpoints(app) {
       }
 
       try {
-        if (
-          response.locals.multiUserMode &&
-          response.locals.user?.role !== "admin"
-        ) {
-          return response.sendStatus(401).end();
-        }
-
         const newFilename = await renameLogoFile(request.file.originalname);
         const existingLogoFilename = await SystemSettings.currentLogoFilename();
         await removeCustomLogo(existingLogoFilename);
@@ -480,16 +464,9 @@ function systemEndpoints(app) {
 
   app.get(
     "/system/remove-logo",
-    [validatedRequest],
-    async (request, response) => {
+    [validatedRequest, flexUserRoleValid],
+    async (_request, response) => {
       try {
-        if (
-          response.locals.multiUserMode &&
-          response.locals.user?.role !== "admin"
-        ) {
-          return response.sendStatus(401).end();
-        }
-
         const currentLogoFilename = await SystemSettings.currentLogoFilename();
         await removeCustomLogo(currentLogoFilename);
         const { success, error } = await SystemSettings.updateSettings({
@@ -517,7 +494,8 @@ function systemEndpoints(app) {
           return response.status(200).json({ canDelete: true });
         }
 
-        if (response.locals.user?.role === "admin") {
+        const user = await userFromSession(request, response);
+        if (["admin", "manager"].includes(user?.role)) {
           return response.status(200).json({ canDelete: true });
         }
 
@@ -548,16 +526,9 @@ function systemEndpoints(app) {
 
   app.post(
     "/system/set-welcome-messages",
-    [validatedRequest],
+    [validatedRequest, flexUserRoleValid],
     async (request, response) => {
       try {
-        if (
-          response.locals.multiUserMode &&
-          response.locals.user?.role !== "admin"
-        ) {
-          return response.sendStatus(401).end();
-        }
-
         const { messages = [] } = reqBody(request);
         if (!Array.isArray(messages)) {
           return response.status(400).json({
@@ -659,16 +630,9 @@ function systemEndpoints(app) {
 
   app.post(
     "/system/workspace-chats",
-    [validatedRequest],
+    [validatedRequest, flexUserRoleValid],
     async (request, response) => {
       try {
-        if (
-          response.locals.multiUserMode &&
-          response.locals.user?.role !== "admin"
-        ) {
-          return response.sendStatus(401).end();
-        }
-
         const { offset = 0, limit = 20 } = reqBody(request);
         const chats = await WorkspaceChats.whereWithData(
           {},
@@ -689,16 +653,9 @@ function systemEndpoints(app) {
 
   app.delete(
     "/system/workspace-chats/:id",
-    [validatedRequest],
+    [validatedRequest, flexUserRoleValid],
     async (request, response) => {
       try {
-        if (
-          response.locals.multiUserMode &&
-          response.locals.user?.role !== "admin"
-        ) {
-          return response.sendStatus(401).end();
-        }
-
         const { id } = request.params;
         await WorkspaceChats.delete({ id: Number(id) });
         response.status(200).json({ success, error });
@@ -711,16 +668,9 @@ function systemEndpoints(app) {
 
   app.get(
     "/system/export-chats",
-    [validatedRequest],
-    async (request, response) => {
+    [validatedRequest, flexUserRoleValid],
+    async (_request, response) => {
       try {
-        if (
-          response.locals.multiUserMode &&
-          response.locals.user?.role !== "admin"
-        ) {
-          return response.sendStatus(401).end();
-        }
-
         const chats = await WorkspaceChats.whereWithData({}, null, null, {
           id: "asc",
         });
diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js
index c32eb4750..de49dba1b 100644
--- a/server/endpoints/workspaces.js
+++ b/server/endpoints/workspaces.js
@@ -11,36 +11,40 @@ const {
   processDocument,
 } = require("../utils/files/documentProcessor");
 const { validatedRequest } = require("../utils/middleware/validatedRequest");
-const { SystemSettings } = require("../models/systemSettings");
 const { Telemetry } = require("../models/telemetry");
+const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected");
 const { handleUploads } = setupMulter();
 
 function workspaceEndpoints(app) {
   if (!app) return;
 
-  app.post("/workspace/new", [validatedRequest], async (request, response) => {
-    try {
-      const user = await userFromSession(request, response);
-      const { name = null, onboardingComplete = false } = reqBody(request);
-      const { workspace, message } = await Workspace.new(name, user?.id);
-      await Telemetry.sendTelemetry(
-        "workspace_created",
-        {
-          multiUserMode: multiUserMode(response),
-          LLMSelection: process.env.LLM_PROVIDER || "openai",
-          VectorDbSelection: process.env.VECTOR_DB || "pinecone",
-        },
-        user?.id
-      );
-      if (onboardingComplete === true)
-        await Telemetry.sendTelemetry("onboarding_complete");
+  app.post(
+    "/workspace/new",
+    [validatedRequest, flexUserRoleValid],
+    async (request, response) => {
+      try {
+        const user = await userFromSession(request, response);
+        const { name = null, onboardingComplete = false } = reqBody(request);
+        const { workspace, message } = await Workspace.new(name, user?.id);
+        await Telemetry.sendTelemetry(
+          "workspace_created",
+          {
+            multiUserMode: multiUserMode(response),
+            LLMSelection: process.env.LLM_PROVIDER || "openai",
+            VectorDbSelection: process.env.VECTOR_DB || "pinecone",
+          },
+          user?.id
+        );
+        if (onboardingComplete === true)
+          await Telemetry.sendTelemetry("onboarding_complete");
 
-      response.status(200).json({ workspace, message });
-    } catch (e) {
-      console.log(e.message, e);
-      response.sendStatus(500).end();
+        response.status(200).json({ workspace, message });
+      } catch (e) {
+        console.log(e.message, e);
+        response.sendStatus(500).end();
+      }
     }
-  });
+  );
 
   app.post(
     "/workspace/:slug/update",
@@ -142,7 +146,7 @@ function workspaceEndpoints(app) {
 
   app.delete(
     "/workspace/:slug",
-    [validatedRequest],
+    [validatedRequest, flexUserRoleValid],
     async (request, response) => {
       try {
         const { slug = "" } = request.params;
@@ -157,16 +161,6 @@ function workspaceEndpoints(app) {
           return;
         }
 
-        if (multiUserMode(response) && user.role !== "admin") {
-          const canDelete =
-            (await SystemSettings.get({ label: "users_can_delete_workspaces" }))
-              ?.value === "true";
-          if (!canDelete) {
-            response.sendStatus(500).end();
-            return;
-          }
-        }
-
         await WorkspaceChats.delete({ workspaceId: Number(workspace.id) });
         await DocumentVectors.deleteForWorkspace(workspace.id);
         await Document.delete({ workspaceId: Number(workspace.id) });
diff --git a/server/models/workspace.js b/server/models/workspace.js
index 059af4c32..9139c25e9 100644
--- a/server/models/workspace.js
+++ b/server/models/workspace.js
@@ -64,7 +64,7 @@ const Workspace = {
   },
 
   getWithUser: async function (user = null, clause = {}) {
-    if (user.role === "admin") return this.get(clause);
+    if (["admin", "manager"].includes(user.role)) return this.get(clause);
 
     try {
       const workspace = await prisma.workspaces.findFirst({
@@ -142,7 +142,8 @@ const Workspace = {
     limit = null,
     orderBy = null
   ) {
-    if (user.role === "admin") return await this.where(clause, limit, orderBy);
+    if (["admin", "manager"].includes(user.role))
+      return await this.where(clause, limit, orderBy);
 
     try {
       const workspaces = await prisma.workspaces.findMany({
diff --git a/server/utils/middleware/multiUserProtected.js b/server/utils/middleware/multiUserProtected.js
new file mode 100644
index 000000000..7de09ac57
--- /dev/null
+++ b/server/utils/middleware/multiUserProtected.js
@@ -0,0 +1,41 @@
+const { SystemSettings } = require("../../models/systemSettings");
+const { userFromSession } = require("../http");
+
+const ROLES = ["admin", "manager"];
+
+// Explicitly check that multi user mode is enabled as well as that the
+// requesting user has the appropriate role to modify or call the URL.
+async function strictMultiUserRoleValid(request, response, next) {
+  const multiUserMode =
+    response.locals?.multiUserMode ?? (await SystemSettings.isMultiUserMode());
+  if (!multiUserMode) return response.sendStatus(401).end();
+
+  const user =
+    response.locals?.user ?? (await userFromSession(request, response));
+  if (!ROLES.includes(user?.role)) return response.sendStatus(401).end();
+
+  next();
+}
+
+// Apply role permission checks IF the current system is in multi-user mode.
+// This is relevant for routes that are shared between MUM and single-user mode.
+// Checks if the requesting user has the appropriate role to modify or call the URL.
+async function flexUserRoleValid(request, response, next) {
+  const multiUserMode =
+    response.locals?.multiUserMode ?? (await SystemSettings.isMultiUserMode());
+  if (!multiUserMode) {
+    next();
+    return;
+  }
+
+  const user =
+    response.locals?.user ?? (await userFromSession(request, response));
+  if (!ROLES.includes(user?.role)) return response.sendStatus(401).end();
+
+  next();
+}
+
+module.exports = {
+  strictMultiUserRoleValid,
+  flexUserRoleValid,
+};
diff --git a/server/utils/middleware/validatedRequest.js b/server/utils/middleware/validatedRequest.js
index 349fa0b6c..275522bb9 100644
--- a/server/utils/middleware/validatedRequest.js
+++ b/server/utils/middleware/validatedRequest.js
@@ -20,7 +20,7 @@ async function validatedRequest(request, response, next) {
   }
 
   if (!process.env.AUTH_TOKEN) {
-    response.status(403).json({
+    response.status(401).json({
       error: "You need to set an AUTH_TOKEN environment variable.",
     });
     return;
@@ -30,7 +30,7 @@ async function validatedRequest(request, response, next) {
   const token = auth ? auth.split(" ")[1] : null;
 
   if (!token) {
-    response.status(403).json({
+    response.status(401).json({
       error: "No auth token found.",
     });
     return;
@@ -38,7 +38,7 @@ async function validatedRequest(request, response, next) {
 
   const { p } = decodeJWT(token);
   if (p !== process.env.AUTH_TOKEN) {
-    response.status(403).json({
+    response.status(401).json({
       error: "Invalid auth token found.",
     });
     return;
@@ -52,7 +52,7 @@ async function validateMultiUserRequest(request, response, next) {
   const token = auth ? auth.split(" ")[1] : null;
 
   if (!token) {
-    response.status(403).json({
+    response.status(401).json({
       error: "No auth token found.",
     });
     return;
@@ -60,7 +60,7 @@ async function validateMultiUserRequest(request, response, next) {
 
   const valid = decodeJWT(token);
   if (!valid || !valid.id) {
-    response.status(403).json({
+    response.status(401).json({
       error: "Invalid auth token.",
     });
     return;
@@ -68,12 +68,19 @@ async function validateMultiUserRequest(request, response, next) {
 
   const user = await User.get({ id: valid.id });
   if (!user) {
-    response.status(403).json({
+    response.status(401).json({
       error: "Invalid auth for user.",
     });
     return;
   }
 
+  if (user.suspended) {
+    response.status(401).json({
+      error: "User is suspended from system",
+    });
+    return;
+  }
+
   response.locals.user = user;
   next();
 }
-- 
GitLab