CVE-2024-0795: Improper Access Control (CWE-284)
value: If an attacked was given access to an instance with the admin or manager role there is no backend authentication that would prevent the attacked from creating a new user with an `admin` role and then be able to use this new account to have elevated privileges on the instance
CWE description: The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.
Published: 2024-03-02T22:15Z
Repository: https://github.com/mintplex-labs/anything-llm
Language: JavaScript
Files changed: frontend/src/App.jsx, frontend/src/components/SettingsSidebar/index.jsx, frontend/src/models/system.js, frontend/src/pages/Admin/Users/UserRow/index.jsx, frontend/src/pages/Admin/Users/index.jsx, server/endpoints/admin.js, server/endpoints/api/admin/index.js, server/endpoints/chat.js, server/endpoints/extensions/index.js, server/endpoints/system.js, server/endpoints/utils.js, server/endpoints/workspaces.js, server/models/workspace.js, server/utils/helpers/admin/index.js, server/utils/helpers/chat/convertTo.js, server/utils/middleware/multiUserProtected.js
Commit date: 2024-01-22 14:14:01 -0800
Commit message: Implement total permission overhaul (#629)

* Implement total permission overhaul
Add explicit permissions on each flex and strict route
Patch issues with role escalation and CRUD of users
Patch permissions on all routes for coverage
Improve middleware to accept role array for clarity

* update comments

* remove permissions to API-keys for manager. Manager could generate API-key and using high-privelege api-key give themselves admin

* update sidebar permissions for multi-user and single user

* update options for mobile sidebar
Diff: diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 7acd2c534..058505d20 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -79,11 +79,11 @@ export default function App() {
                 path="/settings/appearance"
                 element={<ManagerRoute Component={GeneralAppearance} />}
               />
               <Route
                 path="/settings/api-keys"
-                element={<ManagerRoute Component={GeneralApiKeys} />}
+                element={<AdminRoute Component={GeneralApiKeys} />}
               />
               <Route
                 path="/settings/workspace-chats"
                 element={<ManagerRoute Component={GeneralChats} />}
               />
diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx
index 99cf1eb35..851ea5d57 100644
--- a/frontend/src/components/SettingsSidebar/index.jsx
+++ b/frontend/src/components/SettingsSidebar/index.jsx
@@ -60,83 +60,101 @@ export default function SettingsSidebar() {
           </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">
               <div className="flex flex-col gap-y-2 h-[65vh] pb-8 overflow-y-scroll no-scroll">
-                {/* Admin/manager Multi-user Settings */}
-                {!!user && user?.role !== "default" && (
-                  <>
-                    <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" />}
-                    />
-                  </>
-                )}
-
+                <Option
+                  href={paths.settings.system()}
+                  btnText="System Preferences"
+                  icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
+                  user={user}
+                  allowedRole={["admin", "manager"]}
+                />
+                <Option
+                  href={paths.settings.invites()}
+                  btnText="Invitation"
+                  icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />}
+                  user={user}
+                  allowedRole={["admin", "manager"]}
+                />
+                <Option
+                  href={paths.settings.users()}
+                  btnText="Users"
+                  icon={<Users className="h-5 w-5 flex-shrink-0" />}
+                  user={user}
+                  allowedRole={["admin", "manager"]}
+                />
+                <Option
+                  href={paths.settings.workspaces()}
+                  btnText="Workspaces"
+                  icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
+                  user={user}
+                  allowedRole={["admin", "manager"]}
+                />
                 <Option
                   href={paths.settings.chats()}
                   btnText="Workspace Chat"
                   icon={<ChatCenteredText className="h-5 w-5 flex-shrink-0" />}
+                  user={user}
+                  flex={true}
+                  allowedRole={["admin", "manager"]}
                 />
-
                 <Option
                   href={paths.settings.appearance()}
                   btnText="Appearance"
                   icon={<Eye className="h-5 w-5 flex-shrink-0" />}
+                  user={user}
+                  flex={true}
+                  allowedRole={["admin", "manager"]}
                 />
                 <Option
                   href={paths.settings.apiKeys()}
                   btnText="API Keys"
                   icon={<Key className="h-5 w-5 flex-shrink-0" />}
+                  user={user}
+                  flex={true}
+                  allowedRole={["admin"]}
+                />
+                <Option
+                  href={paths.settings.llmPreference()}
+                  btnText="LLM Preference"
+                  icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
+                  user={user}
+                  flex={true}
+                  allowedRole={["admin"]}
+                />
+                <Option
+                  href={paths.settings.embeddingPreference()}
+                  btnText="Embedding Preference"
+                  icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
+                  user={user}
+                  flex={true}
+                  allowedRole={["admin"]}
+                />
+                <Option
+                  href={paths.settings.vectorDatabase()}
+                  btnText="Vector Database"
+                  icon={<Database className="h-5 w-5 flex-shrink-0" />}
+                  user={user}
+                  flex={true}
+                  allowedRole={["admin"]}
+                />
+                <Option
+                  href={paths.settings.dataConnectors.list()}
+                  btnText="Data Connectors"
+                  icon={<Plugs className="h-5 w-5 flex-shrink-0" />}
+                  user={user}
+                  flex={true}
+                  allowedRole={["admin", "manager"]}
                 />
-
-                {(!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.settings.dataConnectors.list()}
-                      btnText="Data Connectors"
-                      icon={<Plugs className="h-5 w-5 flex-shrink-0" />}
-                    />
-                  </>
-                )}
                 <Option
                   href={paths.settings.security()}
                   btnText="Security"
                   icon={<Lock className="h-5 w-5 flex-shrink-0" />}
+                  user={user}
+                  flex={true}
+                  allowedRole={["admin", "manager"]}
                 />
               </div>
             </div>
             <div>
               {/* Footer */}
@@ -263,67 +281,99 @@ export function SidebarMobileHeader() {
                 >
                   <Option
                     href={paths.settings.system()}
                     btnText="System Preferences"
                     icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
+                    user={user}
+                    allowedRole={["admin", "manager"]}
                   />
                   <Option
                     href={paths.settings.invites()}
                     btnText="Invitation"
                     icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />}
+                    user={user}
+                    allowedRole={["admin", "manager"]}
                   />
                   <Option
                     href={paths.settings.users()}
                     btnText="Users"
                     icon={<Users className="h-5 w-5 flex-shrink-0" />}
+                    user={user}
+                    allowedRole={["admin", "manager"]}
                   />
                   <Option
                     href={paths.settings.workspaces()}
                     btnText="Workspaces"
                     icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
+                    user={user}
+                    allowedRole={["admin", "manager"]}
                   />
-
                   <Option
                     href={paths.settings.chats()}
                     btnText="Workspace Chat"
                     icon={
                       <ChatCenteredText className="h-5 w-5 flex-shrink-0" />
                     }
+                    user={user}
+                    flex={true}
+                    allowedRole={["admin", "manager"]}
                   />
                   <Option
                     href={paths.settings.appearance()}
                     btnText="Appearance"
                     icon={<Eye className="h-5 w-5 flex-shrink-0" />}
+                    user={user}
+                    flex={true}
+                    allowedRole={["admin", "manager"]}
                   />
                   <Option
                     href={paths.settings.apiKeys()}
                     btnText="API Keys"
                     icon={<Key className="h-5 w-5 flex-shrink-0" />}
+                    user={user}
+                    flex={true}
+                    allowedRole={["admin"]}
+                  />
+                  <Option
+                    href={paths.settings.llmPreference()}
+                    btnText="LLM Preference"
+                    icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
+                    user={user}
+                    flex={true}
+                    allowedRole={["admin"]}
+                  />
+                  <Option
+                    href={paths.settings.embeddingPreference()}
+                    btnText="Embedding Preference"
+                    icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
+                    user={user}
+                    flex={true}
+                    allowedRole={["admin"]}
+                  />
+                  <Option
+                    href={paths.settings.vectorDatabase()}
+                    btnText="Vector Database"
+                    icon={<Database className="h-5 w-5 flex-shrink-0" />}
+                    user={user}
+                    flex={true}
+                    allowedRole={["admin"]}
+                  />
+                  <Option
+                    href={paths.settings.dataConnectors.list()}
+                    btnText="Data Connectors"
+                    icon={<Plugs className="h-5 w-5 flex-shrink-0" />}
+                    user={user}
+                    flex={true}
+                    allowedRole={["admin", "manager"]}
                   />
-                  {(!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.settings.security()}
                     btnText="Security"
                     icon={<Lock className="h-5 w-5 flex-shrink-0" />}
+                    user={user}
+                    flex={true}
+                    allowedRole={["admin", "manager"]}
                   />
                 </div>
               </div>
               <div>
                 {/* Footer */}
@@ -362,12 +412,25 @@ export function SidebarMobileHeader() {
       </div>
     </>
   );
 }
 
-const Option = ({ btnText, icon, href }) => {
+const Option = ({
+  btnText,
+  icon,
+  href,
+  flex = false,
+  user = null,
+  allowedRole = [],
+}) => {
   const isActive = window.location.pathname === href;
+
+  // Option only for multi-user
+  if (!flex && !allowedRole.includes(user?.role)) return null;
+
+  // Option is dual-mode, but user exists, we need to check permissions
+  if (flex && !!user && !allowedRole.includes(user?.role)) return null;
   return (
     <div className="flex gap-x-2 items-center justify-between text-white">
       <a
         href={href}
         className={`
diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js
index e504fcb26..596348ede 100644
--- a/frontend/src/models/system.js
+++ b/frontend/src/models/system.js
@@ -207,10 +207,11 @@ const System = {
   },
   fetchPfp: async function (id) {
     return await fetch(`${API_BASE}/system/pfp/${id}`, {
       method: "GET",
       cache: "no-cache",
+      headers: baseHeaders(),
     })
       .then((res) => {
         if (res.ok && res.status !== 204) return res.blob();
         throw new Error("Failed to fetch pfp.");
       })
@@ -281,10 +282,11 @@ const System = {
   },
   getWelcomeMessages: async function () {
     return await fetch(`${API_BASE}/system/welcome-messages`, {
       method: "GET",
       cache: "no-cache",
+      headers: baseHeaders(),
     })
       .then((res) => {
         if (!res.ok) throw new Error("Could not fetch welcome messages.");
         return res.json();
       })
diff --git a/frontend/src/pages/Admin/Users/UserRow/index.jsx b/frontend/src/pages/Admin/Users/UserRow/index.jsx
index d734759fc..cd1c47732 100644
--- a/frontend/src/pages/Admin/Users/UserRow/index.jsx
+++ b/frontend/src/pages/Admin/Users/UserRow/index.jsx
@@ -1,33 +1,56 @@
 import { useRef, useState } from "react";
 import { titleCase } from "text-case";
 import Admin from "@/models/admin";
 import EditUserModal, { EditUserModalId } from "./EditUserModal";
 import { DotsThreeOutline } from "@phosphor-icons/react";
+import showToast from "@/utils/toast";
+
+const ModMap = {
+  admin: ["admin", "manager", "default"],
+  manager: ["manager", "default"],
+  default: [],
+};
 
 export default function UserRow({ currUser, user }) {
   const rowRef = useRef(null);
+  const canModify = ModMap[currUser?.role || "default"].includes(user.role);
   const [suspended, setSuspended] = useState(user.suspended === 1);
   const handleSuspend = async () => {
     if (
       !window.confirm(
         `Are you sure you want to suspend ${user.username}?\nAfter you do this they will be logged out and unable to log back into this instance of AnythingLLM until unsuspended by an admin.`
       )
     )
       return false;
-    setSuspended(!suspended);
-    await Admin.updateUser(user.id, { suspended: suspended ? 0 : 1 });
+
+    const { success, error } = await Admin.updateUser(user.id, {
+      suspended: suspended ? 0 : 1,
+    });
+    if (!success) showToast(error, "error", { clear: true });
+    if (success) {
+      showToast(
+        `User ${!suspended ? "has been suspended" : "is no longer suspended"}.`,
+        "success",
+        { clear: true }
+      );
+      setSuspended(!suspended);
+    }
   };
   const handleDelete = async () => {
     if (
       !window.confirm(
         `Are you sure you want to delete ${user.username}?\nAfter you do this they will be logged out and unable to use this instance of AnythingLLM.\n\nThis action is irreversible.`
       )
     )
       return false;
-    rowRef?.current?.remove();
-    await Admin.deleteUser(user.id);
+    const { success, error } = await Admin.deleteUser(user.id);
+    if (!success) showToast(error, "error", { clear: true });
+    if (success) {
+      rowRef?.current?.remove();
+      showToast("User deleted from system.", "success", { clear: true });
+    }
   };
 
   return (
     <>
       <tr
@@ -38,21 +61,21 @@ export default function UserRow({ currUser, user }) {
           {user.username}
         </th>
         <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">
-          {currUser?.role !== "default" && (
+          {canModify && (
             <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" && (
+          {currUser?.id !== user.id && canModify && (
             <>
               <button
                 onClick={handleSuspend}
                 className="font-medium text-orange-600 dark:text-orange-300 px-2 py-1 rounded-lg hover:bg-orange-50 hover:dark:bg-orange-800 hover:dark:bg-opacity-20"
               >
diff --git a/frontend/src/pages/Admin/Users/index.jsx b/frontend/src/pages/Admin/Users/index.jsx
index b59bbe576..fb8f50080 100644
--- a/frontend/src/pages/Admin/Users/index.jsx
+++ b/frontend/src/pages/Admin/Users/index.jsx
@@ -103,11 +103,12 @@ 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.",
+    "Can view, create, and delete any workspaces and modify workspace-specific settings.",
+    "Can create, update and invite new users to the instance.",
     "Cannot modify LLM, vectorDB, embedding, or other connections.",
   ],
   admin: [
     "Highest user level privilege.",
     "Can see and do everything across the system.",
diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js
index b32de3b3e..b107a11b4 100644
--- a/server/endpoints/admin.js
+++ b/server/endpoints/admin.js
@@ -5,22 +5,28 @@ const { SystemSettings } = require("../models/systemSettings");
 const { User } = require("../models/user");
 const { DocumentVectors } = require("../models/vectors");
 const { Workspace } = require("../models/workspace");
 const { WorkspaceChats } = require("../models/workspaceChats");
 const { getVectorDbClass } = require("../utils/helpers");
+const {
+  validRoleSelection,
+  canModifyAdmin,
+  validCanModify,
+} = require("../utils/helpers/admin");
 const { reqBody, userFromSession } = require("../utils/http");
 const {
   strictMultiUserRoleValid,
+  ROLES,
 } = require("../utils/middleware/multiUserProtected");
 const { validatedRequest } = require("../utils/middleware/validatedRequest");
 
 function adminEndpoints(app) {
   if (!app) return;
 
   app.get(
     "/admin/users",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (_request, response) => {
       try {
         const users = (await User.where()).map((user) => {
           const { password, ...rest } = user;
           return rest;
@@ -33,14 +39,24 @@ function adminEndpoints(app) {
     }
   );
 
   app.post(
     "/admin/users/new",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
+        const currUser = await userFromSession(request, response);
         const newUserParams = reqBody(request);
+        const roleValidation = validRoleSelection(currUser, newUserParams);
+
+        if (!roleValidation.valid) {
+          response
+            .status(200)
+            .json({ user: null, error: roleValidation.error });
+          return;
+        }
+
         const { user: newUser, error } = await User.create(newUserParams);
         response.status(200).json({ user: newUser, error });
       } catch (e) {
         console.error(e);
         response.sendStatus(500).end();
@@ -48,33 +64,38 @@ function adminEndpoints(app) {
     }
   );
 
   app.post(
     "/admin/user/:id",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
+        const currUser = await userFromSession(request, response);
         const { id } = request.params;
         const updates = reqBody(request);
         const user = await User.get({ id: Number(id) });
 
-        // Check to make sure with this update that includes a role change to
-        // something other than admin that we still have at least one admin left.
-        if (
-          updates.hasOwnProperty("role") && // has admin prop to change
-          updates.role !== "admin" && // and we are changing to non-admin
-          user.role === "admin" // and they currently are an admin
-        ) {
-          const adminCount = await User.count({ role: "admin" });
-          if (adminCount - 1 <= 0) {
-            response.status(200).json({
-              success: false,
-              error:
-                "No system admins will remain if you do this. Update failed.",
-            });
-            return;
-          }
+        const canModify = validCanModify(currUser, user);
+        if (!canModify.valid) {
+          response.status(200).json({ success: false, error: canModify.error });
+          return;
+        }
+
+        const roleValidation = validRoleSelection(currUser, updates);
+        if (!roleValidation.valid) {
+          response
+            .status(200)
+            .json({ success: false, error: roleValidation.error });
+          return;
+        }
+
+        const validAdminRoleModification = await canModifyAdmin(user, updates);
+        if (!validAdminRoleModification.valid) {
+          response
+            .status(200)
+            .json({ success: false, error: validAdminRoleModification.error });
+          return;
         }
 
         const { success, error } = await User.update(id, updates);
         response.status(200).json({ success, error });
       } catch (e) {
@@ -84,14 +105,23 @@ function adminEndpoints(app) {
     }
   );
 
   app.delete(
     "/admin/user/:id",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
+        const currUser = await userFromSession(request, response);
         const { id } = request.params;
+        const user = await User.get({ id: Number(id) });
+
+        const canModify = validCanModify(currUser, user);
+        if (!canModify.valid) {
+          response.status(200).json({ success: false, error: canModify.error });
+          return;
+        }
+
         await User.delete({ id: Number(id) });
         response.status(200).json({ success: true, error: null });
       } catch (e) {
         console.error(e);
         response.sendStatus(500).end();
@@ -99,11 +129,11 @@ function adminEndpoints(app) {
     }
   );
 
   app.get(
     "/admin/invites",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (_request, response) => {
       try {
         const invites = await Invite.whereWithUsers();
         response.status(200).json({ invites });
       } catch (e) {
@@ -113,11 +143,11 @@ function adminEndpoints(app) {
     }
   );
 
   app.get(
     "/admin/invite/new",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const user = await userFromSession(request, response);
         const { invite, error } = await Invite.create(user.id);
         response.status(200).json({ invite, error });
@@ -128,11 +158,11 @@ function adminEndpoints(app) {
     }
   );
 
   app.delete(
     "/admin/invite/:id",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const { id } = request.params;
         const { success, error } = await Invite.deactivate(id);
         response.status(200).json({ success, error });
@@ -143,11 +173,11 @@ function adminEndpoints(app) {
     }
   );
 
   app.get(
     "/admin/workspaces",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (_request, response) => {
       try {
         const workspaces = await Workspace.whereWithUsers();
         response.status(200).json({ workspaces });
       } catch (e) {
@@ -157,11 +187,11 @@ function adminEndpoints(app) {
     }
   );
 
   app.post(
     "/admin/workspaces/new",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const user = await userFromSession(request, response);
         const { name } = reqBody(request);
         const { workspace, message: error } = await Workspace.new(
@@ -176,11 +206,11 @@ function adminEndpoints(app) {
     }
   );
 
   app.post(
     "/admin/workspaces/:workspaceId/update-users",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const { workspaceId } = request.params;
         const { userIds } = reqBody(request);
         const { success, error } = await Workspace.updateUsers(
@@ -195,11 +225,11 @@ function adminEndpoints(app) {
     }
   );
 
   app.delete(
     "/admin/workspaces/:id",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const { id } = request.params;
         const VectorDb = getVectorDbClass();
         const workspace = await Workspace.get({ id: Number(id) });
@@ -226,11 +256,11 @@ function adminEndpoints(app) {
     }
   );
 
   app.get(
     "/admin/system-preferences",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (_request, response) => {
       try {
         const settings = {
           users_can_delete_workspaces:
             (await SystemSettings.get({ label: "users_can_delete_workspaces" }))
@@ -251,11 +281,11 @@ function adminEndpoints(app) {
     }
   );
 
   app.post(
     "/admin/system-preferences",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const updates = reqBody(request);
         await SystemSettings.updateSettings(updates);
         response.status(200).json({ success: true, error: null });
@@ -266,11 +296,11 @@ function adminEndpoints(app) {
     }
   );
 
   app.get(
     "/admin/api-keys",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
     async (_request, response) => {
       try {
         const apiKeys = await ApiKey.whereWithUser({});
         return response.status(200).json({
           apiKeys,
@@ -286,11 +316,11 @@ function adminEndpoints(app) {
     }
   );
 
   app.post(
     "/admin/generate-api-key",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
     async (request, response) => {
       try {
         const user = await userFromSession(request, response);
         const { apiKey, error } = await ApiKey.create(user.id);
         return response.status(200).json({
@@ -304,11 +334,11 @@ function adminEndpoints(app) {
     }
   );
 
   app.delete(
     "/admin/delete-api-key/:id",
-    [validatedRequest, strictMultiUserRoleValid],
+    [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
     async (request, response) => {
       try {
         const { id } = request.params;
         await ApiKey.delete({ id: Number(id) });
         return response.status(200).end();
diff --git a/server/endpoints/api/admin/index.js b/server/endpoints/api/admin/index.js
index ebe662c69..1f2a5bae7 100644
--- a/server/endpoints/api/admin/index.js
+++ b/server/endpoints/api/admin/index.js
@@ -1,10 +1,11 @@
 const { Invite } = require("../../../models/invite");
 const { SystemSettings } = require("../../../models/systemSettings");
 const { User } = require("../../../models/user");
 const { Workspace } = require("../../../models/workspace");
 const { WorkspaceChats } = require("../../../models/workspaceChats");
+const { canModifyAdmin } = require("../../../utils/helpers/admin");
 const { multiUserMode, reqBody } = require("../../../utils/http");
 const { validApiKey } = require("../../../utils/middleware/validApiKey");
 
 function apiAdminEndpoints(app) {
   if (!app) return;
@@ -196,27 +197,17 @@ function apiAdminEndpoints(app) {
       }
 
       const { id } = request.params;
       const updates = reqBody(request);
       const user = await User.get({ id: Number(id) });
+      const validAdminRoleModification = await canModifyAdmin(user, updates);
 
-      // Check to make sure with this update that includes a role change to
-      // something other than admin that we still have at least one admin left.
-      if (
-        updates.hasOwnProperty("role") && // has admin prop to change
-        updates.role !== "admin" && // and we are changing to non-admin
-        user.role === "admin" // and they currently are an admin
-      ) {
-        const adminCount = await User.count({ role: "admin" });
-        if (adminCount - 1 <= 0) {
-          response.status(200).json({
-            success: false,
-            error:
-              "No system admins will remain if you do this. Update failed.",
-          });
-          return;
-        }
+      if (!validAdminRoleModification.valid) {
+        response
+          .status(200)
+          .json({ success: false, error: validAdminRoleModification.error });
+        return;
       }
 
       const { success, error } = await User.update(id, updates);
       response.status(200).json({ success, error });
     } catch (e) {
diff --git a/server/endpoints/chat.js b/server/endpoints/chat.js
index adfec0ec3..23739084a 100644
--- a/server/endpoints/chat.js
+++ b/server/endpoints/chat.js
@@ -8,17 +8,21 @@ const { Telemetry } = require("../models/telemetry");
 const {
   streamChatWithWorkspace,
   writeResponseChunk,
   VALID_CHAT_MODE,
 } = require("../utils/chats/stream");
+const {
+  ROLES,
+  flexUserRoleValid,
+} = require("../utils/middleware/multiUserProtected");
 
 function chatEndpoints(app) {
   if (!app) return;
 
   app.post(
     "/workspace/:slug/stream-chat",
-    [validatedRequest],
+    [validatedRequest, flexUserRoleValid([ROLES.all])],
     async (request, response) => {
       try {
         const user = await userFromSession(request, response);
         const { slug } = request.params;
         const { message, mode = "query" } = reqBody(request);
@@ -50,11 +54,11 @@ function chatEndpoints(app) {
         response.setHeader("Content-Type", "text/event-stream");
         response.setHeader("Access-Control-Allow-Origin", "*");
         response.setHeader("Connection", "keep-alive");
         response.flushHeaders();
 
-        if (multiUserMode(response) && user.role !== "admin") {
+        if (multiUserMode(response) && user.role !== ROLES.admin) {
           const limitMessagesSetting = await SystemSettings.get({
             label: "limit_user_messages",
           });
           const limitMessages = limitMessagesSetting?.value === "true";
 
diff --git a/server/endpoints/extensions/index.js b/server/endpoints/extensions/index.js
index 1b3770374..a2c884a01 100644
--- a/server/endpoints/extensions/index.js
+++ b/server/endpoints/extensions/index.js
@@ -2,19 +2,20 @@ const { Telemetry } = require("../../models/telemetry");
 const {
   forwardExtensionRequest,
 } = require("../../utils/files/documentProcessor");
 const {
   flexUserRoleValid,
+  ROLES,
 } = require("../../utils/middleware/multiUserProtected");
 const { validatedRequest } = require("../../utils/middleware/validatedRequest");
 
 function extensionEndpoints(app) {
   if (!app) return;
 
   app.post(
     "/ext/github/branches",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const responseFromProcessor = await forwardExtensionRequest({
           endpoint: "/ext/github-repo/branches",
           method: "POST",
@@ -28,11 +29,11 @@ function extensionEndpoints(app) {
     }
   );
 
   app.post(
     "/ext/github/repo",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const responseFromProcessor = await forwardExtensionRequest({
           endpoint: "/ext/github-repo",
           method: "POST",
@@ -49,11 +50,11 @@ function extensionEndpoints(app) {
     }
   );
 
   app.post(
     "/ext/youtube/transcript",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const responseFromProcessor = await forwardExtensionRequest({
           endpoint: "/ext/youtube-transcript",
           method: "POST",
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index 3f11bf5e0..14aa22e04 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -37,14 +37,19 @@ const {
 const { Telemetry } = require("../models/telemetry");
 const { WelcomeMessages } = require("../models/welcomeMessages");
 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");
+const {
+  flexUserRoleValid,
+  ROLES,
+} = require("../utils/middleware/multiUserProtected");
 const { fetchPfp, determinePfpFilepath } = require("../utils/files/pfp");
-const { convertToCSV, convertToJSON, convertToJSONL } = require("./utils");
+const {
+  prepareWorkspaceChatsForExport,
+  exportChatsAsType,
+} = require("../utils/helpers/chat/convertTo");
 
 function systemEndpoints(app) {
   if (!app) return;
 
   app.get("/ping", (_, response) => {
@@ -273,19 +278,13 @@ function systemEndpoints(app) {
     }
   );
 
   app.post(
     "/system/update-env",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.admin])],
     async (request, response) => {
       try {
-        const user = await userFromSession(request, response);
-        if (!!user && user.role !== "admin") {
-          response.sendStatus(401).end();
-          return;
-        }
-
         const body = reqBody(request);
         const { newValues, error } = await updateENV(body);
         if (process.env.NODE_ENV === "production") await dumpENV();
         response.status(200).json({ newValues, error });
       } catch (e) {
@@ -339,11 +338,11 @@ function systemEndpoints(app) {
         }
 
         const { user, error } = await User.create({
           username,
           password,
-          role: "admin",
+          role: ROLES.admin,
         });
         await SystemSettings.updateSettings({
           multi_user_mode: true,
           users_can_delete_workspaces: false,
           limit_user_messages: false,
@@ -372,21 +371,21 @@ function systemEndpoints(app) {
         response.sendStatus(500).end();
       }
     }
   );
 
-  app.get("/system/multi-user-mode", async (request, response) => {
+  app.get("/system/multi-user-mode", async (_, response) => {
     try {
       const multiUserMode = await SystemSettings.isMultiUserMode();
       response.status(200).json({ multiUserMode });
     } catch (e) {
       console.log(e.message, e);
       response.sendStatus(500).end();
     }
   });
 
-  app.get("/system/logo", async function (request, response) {
+  app.get("/system/logo", async function (_, response) {
     try {
       const defaultFilename = getDefaultFilename();
       const logoPath = await determineLogoFilepath(defaultFilename);
       const { found, buffer, size, mime } = fetchLogo(logoPath);
       if (!found) {
@@ -407,60 +406,65 @@ function systemEndpoints(app) {
       console.error("Error processing the logo request:", error);
       response.status(500).json({ message: "Internal server error" });
     }
   });
 
-  app.get("/system/pfp/:id", async function (request, response) {
-    try {
-      const { id } = request.params;
-      const pfpPath = await determinePfpFilepath(id);
+  app.get(
+    "/system/pfp/:id",
+    [validatedRequest, flexUserRoleValid([ROLES.all])],
+    async function (request, response) {
+      try {
+        const { id } = request.params;
+        const pfpPath = await determinePfpFilepath(id);
 
-      if (!pfpPath) {
-        response.sendStatus(204).end();
-        return;
-      }
+        if (!pfpPath) {
+          response.sendStatus(204).end();
+          return;
+        }
 
-      const { found, buffer, size, mime } = fetchPfp(pfpPath);
-      if (!found) {
-        response.sendStatus(204).end();
+        const { found, buffer, size, mime } = fetchPfp(pfpPath);
+        if (!found) {
+          response.sendStatus(204).end();
+          return;
+        }
+
+        response.writeHead(200, {
+          "Content-Type": mime || "image/png",
+          "Content-Disposition": `attachment; filename=${path.basename(
+            pfpPath
+          )}`,
+          "Content-Length": size,
+        });
+        response.end(Buffer.from(buffer, "base64"));
         return;
+      } catch (error) {
+        console.error("Error processing the logo request:", error);
+        response.status(500).json({ message: "Internal server error" });
       }
-
-      response.writeHead(200, {
-        "Content-Type": mime || "image/png",
-        "Content-Disposition": `attachment; filename=${path.basename(pfpPath)}`,
-        "Content-Length": size,
-      });
-      response.end(Buffer.from(buffer, "base64"));
-      return;
-    } catch (error) {
-      console.error("Error processing the logo request:", error);
-      response.status(500).json({ message: "Internal server error" });
     }
-  });
+  );
 
   app.post(
     "/system/upload-pfp",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.all])],
     handlePfpUploads.single("file"),
     async function (request, response) {
       try {
         const user = await userFromSession(request, response);
         const uploadedFileName = request.randomFileName;
-
         if (!uploadedFileName) {
           return response.status(400).json({ message: "File upload failed." });
         }
 
         const userRecord = await User.get({ id: user.id });
-        const oldPfpFilename = normalizePath(userRecord.pfpFilename);
+        const oldPfpFilename = userRecord.pfpFilename;
 
         console.log("oldPfpFilename", oldPfpFilename);
         if (oldPfpFilename) {
           const oldPfpPath = path.join(
             __dirname,
-            `../storage/assets/pfp/${oldPfpFilename}`
+            `../storage/assets/pfp/${normalizePath(userRecord.pfpFilename)}`
           );
 
           if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
         }
 
@@ -480,21 +484,22 @@ function systemEndpoints(app) {
     }
   );
 
   app.delete(
     "/system/remove-pfp",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.all])],
     async function (request, response) {
       try {
         const user = await userFromSession(request, response);
         const userRecord = await User.get({ id: user.id });
-        const oldPfpFilename = normalizePath(userRecord.pfpFilename);
+        const oldPfpFilename = userRecord.pfpFilename;
+
         console.log("oldPfpFilename", oldPfpFilename);
         if (oldPfpFilename) {
           const oldPfpPath = path.join(
             __dirname,
-            `../storage/assets/pfp/${oldPfpFilename}`
+            `../storage/assets/pfp/${normalizePath(oldPfpFilename)}`
           );
 
           if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
         }
 
@@ -514,11 +519,11 @@ function systemEndpoints(app) {
     }
   );
 
   app.post(
     "/system/upload-logo",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     handleLogoUploads.single("logo"),
     async (request, response) => {
       if (!request.file || !request.file.originalname) {
         return response.status(400).json({ message: "No logo file provided." });
       }
@@ -548,11 +553,11 @@ function systemEndpoints(app) {
         response.status(500).json({ message: "Error uploading the logo." });
       }
     }
   );
 
-  app.get("/system/is-default-logo", async (request, response) => {
+  app.get("/system/is-default-logo", async (_, response) => {
     try {
       const currentLogoFilename = await SystemSettings.currentLogoFilename();
       const isDefaultLogo = currentLogoFilename === LOGO_FILENAME;
       response.status(200).json({ isDefaultLogo });
     } catch (error) {
@@ -561,11 +566,11 @@ function systemEndpoints(app) {
     }
   });
 
   app.get(
     "/system/remove-logo",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     async (_request, response) => {
       try {
         const currentLogoFilename = await SystemSettings.currentLogoFilename();
         await removeCustomLogo(currentLogoFilename);
         const { success, error } = await SystemSettings.updateSettings({
@@ -592,11 +597,11 @@ function systemEndpoints(app) {
         if (!response.locals.multiUserMode) {
           return response.status(200).json({ canDelete: true });
         }
 
         const user = await userFromSession(request, response);
-        if (["admin", "manager"].includes(user?.role)) {
+        if ([ROLES.admin, ROLES.manager].includes(user?.role)) {
           return response.status(200).json({ canDelete: true });
         }
 
         const canDelete = await SystemSettings.canDeleteWorkspaces();
         response.status(200).json({ canDelete });
@@ -609,25 +614,29 @@ function systemEndpoints(app) {
         });
       }
     }
   );
 
-  app.get("/system/welcome-messages", async function (request, response) {
-    try {
-      const welcomeMessages = await WelcomeMessages.getMessages();
-      response.status(200).json({ success: true, welcomeMessages });
-    } catch (error) {
-      console.error("Error fetching welcome messages:", error);
-      response
-        .status(500)
-        .json({ success: false, message: "Internal server error" });
+  app.get(
+    "/system/welcome-messages",
+    [validatedRequest, flexUserRoleValid([ROLES.all])],
+    async function (_, response) {
+      try {
+        const welcomeMessages = await WelcomeMessages.getMessages();
+        response.status(200).json({ success: true, welcomeMessages });
+      } catch (error) {
+        console.error("Error fetching welcome messages:", error);
+        response
+          .status(500)
+          .json({ success: false, message: "Internal server error" });
+      }
     }
-  });
+  );
 
   app.post(
     "/system/set-welcome-messages",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const { messages = [] } = reqBody(request);
         if (!Array.isArray(messages)) {
           return response.status(400).json({
@@ -731,11 +740,11 @@ function systemEndpoints(app) {
     }
   );
 
   app.post(
     "/system/workspace-chats",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const { offset = 0, limit = 20 } = reqBody(request);
         const chats = await WorkspaceChats.whereWithData(
           {},
@@ -754,11 +763,11 @@ function systemEndpoints(app) {
     }
   );
 
   app.delete(
     "/system/workspace-chats/:id",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const { id } = request.params;
         await WorkspaceChats.delete({ id: Number(id) });
         response.sendStatus(200).end();
@@ -769,92 +778,27 @@ function systemEndpoints(app) {
     }
   );
 
   app.get(
     "/system/export-chats",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.manager, ROLES.admin])],
     async (request, response) => {
       try {
         const { type = "jsonl" } = request.query;
-        const chats = await WorkspaceChats.whereWithData({}, null, null, {
-          id: "asc",
-        });
-        const workspaceIds = [
-          ...new Set(chats.map((chat) => chat.workspaceId)),
-        ];
-
-        const workspacesWithPrompts = await Promise.all(
-          workspaceIds.map((id) => Workspace.get({ id: Number(id) }))
-        );
-
-        const workspacePromptsMap = workspacesWithPrompts.reduce(
-          (acc, workspace) => {
-            acc[workspace.id] = workspace.openAiPrompt;
-            return acc;
-          },
-          {}
-        );
-
-        const workspaceChatsMap = chats.reduce((acc, chat) => {
-          const { prompt, response, workspaceId } = chat;
-          const responseJson = JSON.parse(response);
-
-          if (!acc[workspaceId]) {
-            acc[workspaceId] = {
-              messages: [
-                {
-                  role: "system",
-                  content:
-                    workspacePromptsMap[workspaceId] ||
-                    "Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.",
-                },
-              ],
-            };
-          }
-
-          acc[workspaceId].messages.push(
-            {
-              role: "user",
-              content: prompt,
-            },
-            {
-              role: "assistant",
-              content: responseJson.text,
-            }
-          );
-
-          return acc;
-        }, {});
-
-        let output;
-        switch (type.toLowerCase()) {
-          case "json": {
-            response.setHeader("Content-Type", "application/json");
-            output = await convertToJSON(workspaceChatsMap);
-            break;
-          }
-          case "csv": {
-            response.setHeader("Content-Type", "text/csv");
-            output = await convertToCSV(workspaceChatsMap);
-            break;
-          }
-          // JSONL default
-          default: {
-            response.setHeader("Content-Type", "application/jsonl");
-            output = await convertToJSONL(workspaceChatsMap);
-            break;
-          }
-        }
-
-        response.status(200).send(output);
+        const chats = await prepareWorkspaceChatsForExport();
+        const { contentType, data } = await exportChatsAsType(chats, type);
+        response.setHeader("Content-Type", contentType);
+        response.status(200).send(data);
       } catch (e) {
         console.error(e);
         response.sendStatus(500).end();
       }
     }
   );
 
+  // Used for when a user in multi-user updates their own profile
+  // from the UI.
   app.post("/system/user", [validatedRequest], async (request, response) => {
     try {
       const sessionUser = await userFromSession(request, response);
       const { username, password } = reqBody(request);
       const id = Number(sessionUser.id);
diff --git a/server/endpoints/utils.js b/server/endpoints/utils.js
index 3c639b3e5..e9a7d0caa 100644
--- a/server/endpoints/utils.js
+++ b/server/endpoints/utils.js
@@ -1,7 +1,29 @@
 const { SystemSettings } = require("../models/systemSettings");
 
+function utilEndpoints(app) {
+  if (!app) return;
+
+  app.get("/utils/metrics", async (_, response) => {
+    try {
+      const metrics = {
+        online: true,
+        version: getGitVersion(),
+        mode: (await SystemSettings.isMultiUserMode())
+          ? "multi-user"
+          : "single-user",
+        vectorDB: process.env.VECTOR_DB || "lancedb",
+        storage: await getDiskStorage(),
+      };
+      response.status(200).json(metrics);
+    } catch (e) {
+      console.error(e);
+      response.sendStatus(500).end();
+    }
+  });
+}
+
 function getGitVersion() {
   try {
     return require("child_process")
       .execSync("git rev-parse HEAD")
       .toString()
@@ -30,62 +52,9 @@ async function getDiskStorage() {
       capacity: null,
     };
   }
 }
 
-async function convertToCSV(workspaceChatsMap) {
-  const rows = ["role,content"];
-  for (const workspaceChats of Object.values(workspaceChatsMap)) {
-    for (const message of workspaceChats.messages) {
-      // Escape double quotes and wrap content in double quotes
-      const escapedContent = `"${message.content
-        .replace(/"/g, '""')
-        .replace(/\n/g, " ")}"`;
-      rows.push(`${message.role},${escapedContent}`);
-    }
-  }
-  return rows.join("\n");
-}
-
-async function convertToJSON(workspaceChatsMap) {
-  const allMessages = [].concat.apply(
-    [],
-    Object.values(workspaceChatsMap).map((workspace) => workspace.messages)
-  );
-  return JSON.stringify(allMessages);
-}
-
-async function convertToJSONL(workspaceChatsMap) {
-  return Object.values(workspaceChatsMap)
-    .map((workspaceChats) => JSON.stringify(workspaceChats))
-    .join("\n");
-}
-
-function utilEndpoints(app) {
-  if (!app) return;
-
-  app.get("/utils/metrics", async (_, response) => {
-    try {
-      const metrics = {
-        online: true,
-        version: getGitVersion(),
-        mode: (await SystemSettings.isMultiUserMode())
-          ? "multi-user"
-          : "single-user",
-        vectorDB: process.env.VECTOR_DB || "lancedb",
-        storage: await getDiskStorage(),
-      };
-      response.status(200).json(metrics);
-    } catch (e) {
-      console.error(e);
-      response.sendStatus(500).end();
-    }
-  });
-}
-
 module.exports = {
   utilEndpoints,
   getGitVersion,
-  convertToCSV,
-  convertToJSON,
-  convertToJSONL,
 };
diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js
index 7119297f6..25e391036 100644
--- a/server/endpoints/workspaces.js
+++ b/server/endpoints/workspaces.js
@@ -11,19 +11,22 @@ const {
   processDocument,
   processLink,
 } = require("../utils/files/documentProcessor");
 const { validatedRequest } = require("../utils/middleware/validatedRequest");
 const { Telemetry } = require("../models/telemetry");
-const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected");
+const {
+  flexUserRoleValid,
+  ROLES,
+} = require("../utils/middleware/multiUserProtected");
 const { handleUploads } = setupMulter();
 
 function workspaceEndpoints(app) {
   if (!app) return;
 
   app.post(
     "/workspace/new",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     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);
@@ -48,11 +51,11 @@ function workspaceEndpoints(app) {
     }
   );
 
   app.post(
     "/workspace/:slug/update",
-    [validatedRequest],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const user = await userFromSession(request, response);
         const { slug = null } = request.params;
         const data = reqBody(request);
@@ -77,10 +80,11 @@ function workspaceEndpoints(app) {
     }
   );
 
   app.post(
     "/workspace/:slug/upload",
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     handleUploads.single("file"),
     async function (request, response) {
       const { originalname } = request.file;
       const processingOnline = await checkProcessorAlive();
 
@@ -109,11 +113,11 @@ function workspaceEndpoints(app) {
     }
   );
 
   app.post(
     "/workspace/:slug/upload-link",
-    [validatedRequest],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       const { link = "" } = reqBody(request);
       const processingOnline = await checkProcessorAlive();
 
       if (!processingOnline) {
@@ -141,11 +145,11 @@ function workspaceEndpoints(app) {
     }
   );
 
   app.post(
     "/workspace/:slug/update-embeddings",
-    [validatedRequest],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const user = await userFromSession(request, response);
         const { slug = null } = request.params;
         const { adds = [], deletes = [] } = reqBody(request);
@@ -180,11 +184,11 @@ function workspaceEndpoints(app) {
     }
   );
 
   app.delete(
     "/workspace/:slug",
-    [validatedRequest, flexUserRoleValid],
+    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
     async (request, response) => {
       try {
         const { slug = "" } = request.params;
         const user = await userFromSession(request, response);
         const VectorDb = getVectorDbClass();
@@ -213,42 +217,50 @@ function workspaceEndpoints(app) {
         response.sendStatus(500).end();
       }
     }
   );
 
-  app.get("/workspaces", [validatedRequest], async (request, response) => {
-    try {
-      const user = await userFromSession(request, response);
-      const workspaces = multiUserMode(response)
-        ? await Workspace.whereWithUser(user)
-        : await Workspace.where();
+  app.get(
+    "/workspaces",
+    [validatedRequest, flexUserRoleValid([ROLES.all])],
+    async (request, response) => {
+      try {
+        const user = await userFromSession(request, response);
+        const workspaces = multiUserMode(response)
+          ? await Workspace.whereWithUser(user)
+          : await Workspace.where();
 
-      response.status(200).json({ workspaces });
-    } catch (e) {
-      console.log(e.message, e);
-      response.sendStatus(500).end();
+        response.status(200).json({ workspaces });
+      } catch (e) {
+        console.log(e.message, e);
+        response.sendStatus(500).end();
+      }
     }
-  });
+  );
 
-  app.get("/workspace/:slug", [validatedRequest], async (request, response) => {
-    try {
-      const { slug } = request.params;
-      const user = await userFromSession(request, response);
-      const workspace = multiUserMode(response)
-        ? await Workspace.getWithUser(user, { slug })
-        : await Workspace.get({ slug });
+  app.get(
+    "/workspace/:slug",
+    [validatedRequest, flexUserRoleValid([ROLES.all])],
+    async (request, response) => {
+      try {
+        const { slug } = request.params;
+        const user = await userFromSession(request, response);
+        const workspace = multiUserMode(response)
+          ? await Workspace.getWithUser(user, { slug })
+          : await Workspace.get({ slug });
 
-      response.status(200).json({ workspace });
-    } catch (e) {
-      console.log(e.message, e);
-      response.sendStatus(500).end();
+        response.status(200).json({ workspace });
+      } catch (e) {
+        console.log(e.message, e);
+        response.sendStatus(500).end();
+      }
     }
-  });
+  );
 
   app.get(
     "/workspace/:slug/chats",
-    [validatedRequest],
+    [validatedRequest, flexUserRoleValid([ROLES.all])],
     async (request, response) => {
       try {
         const { slug } = request.params;
         const user = await userFromSession(request, response);
         const workspace = multiUserMode(response)
diff --git a/server/models/workspace.js b/server/models/workspace.js
index 9169d193d..c8e1247ee 100644
--- a/server/models/workspace.js
+++ b/server/models/workspace.js
@@ -1,9 +1,10 @@
 const prisma = require("../utils/prisma");
 const slugify = require("slugify");
 const { Document } = require("./documents");
 const { WorkspaceUser } = require("./workspaceUsers");
+const { ROLES } = require("../utils/middleware/multiUserProtected");
 
 const Workspace = {
   writable: [
     // Used for generic updates so we can validate keys in request body
     "name",
@@ -64,11 +65,12 @@ const Workspace = {
       return { workspace: null, message: error.message };
     }
   },
 
   getWithUser: async function (user = null, clause = {}) {
-    if (["admin", "manager"].includes(user.role)) return this.get(clause);
+    if ([ROLES.admin, ROLES.manager].includes(user.role))
+      return this.get(clause);
 
     try {
       const workspace = await prisma.workspaces.findFirst({
         where: {
           ...clause,
@@ -142,11 +144,11 @@ const Workspace = {
     user,
     clause = {},
     limit = null,
     orderBy = null
   ) {
-    if (["admin", "manager"].includes(user.role))
+    if ([ROLES.admin, ROLES.manager].includes(user.role))
       return await this.where(clause, limit, orderBy);
 
     try {
       const workspaces = await prisma.workspaces.findMany({
         where: {
diff --git a/server/utils/helpers/admin/index.js b/server/utils/helpers/admin/index.js
new file mode 100644
index 000000000..87a012784
--- /dev/null
+++ b/server/utils/helpers/admin/index.js
@@ -0,0 +1,52 @@
+const { User } = require("../../../models/user");
+const { ROLES } = require("../../middleware/multiUserProtected");
+
+// When a user is updating or creating a user in multi-user, we need to check if they
+// are allowed to do this and that the new or existing user will be at or below their permission level.
+// the user executing this function should be an admin or manager.
+function validRoleSelection(currentUser = {}, newUserParams = {}) {
+  if (!newUserParams.hasOwnProperty("role"))
+    return { valid: true, error: null }; // not updating role, so skip.
+  if (currentUser.role === ROLES.admin) return { valid: true, error: null };
+  if (currentUser.role === ROLES.manager) {
+    const validRoles = [ROLES.manager, ROLES.default];
+    if (!validRoles.includes(newUserParams.role))
+      return { valid: false, error: "Invalid role selection for user." };
+    return { valid: true, error: null };
+  }
+  return { valid: false, error: "Invalid condition for caller." };
+}
+
+// Check to make sure with this update that includes a role change to an existing admin to a non-admin
+// that we still have at least one admin left or else they will lock themselves out.
+async function canModifyAdmin(userToModify, updates) {
+  // if updates don't include role property or the user being modified isn't an admin currently - skip.
+  if (!updates.hasOwnProperty("role")) return { valid: true, error: null };
+  if (userToModify.role !== ROLES.admin) return { valid: true, error: null };
+
+  const adminCount = await User.count({ role: ROLES.admin });
+  if (adminCount - 1 <= 0)
+    return {
+      valid: false,
+      error: "No system admins will remain if you do this. Update failed.",
+    };
+  return { valid: true, error: null };
+}
+
+function validCanModify(currentUser, existingUser) {
+  if (currentUser.role === ROLES.admin) return { valid: true, error: null };
+  if (currentUser.role === ROLES.manager) {
+    const validRoles = [ROLES.manager, ROLES.default];
+    if (!validRoles.includes(existingUser.role))
+      return { valid: false, error: "Cannot perform that action on user." };
+    return { valid: true, error: null };
+  }
+
+  return { valid: false, error: "Invalid condition for caller." };
+}
+
+module.exports = {
+  validCanModify,
+  validRoleSelection,
+  canModifyAdmin,
+};
diff --git a/server/utils/helpers/chat/convertTo.js b/server/utils/helpers/chat/convertTo.js
new file mode 100644
index 000000000..4dc3955e9
--- /dev/null
+++ b/server/utils/helpers/chat/convertTo.js
@@ -0,0 +1,113 @@
+// Helpers that convert workspace chats to some supported format
+// for external use by the user.
+
+const { Workspace } = require("../../../models/workspace");
+const { WorkspaceChats } = require("../../../models/workspaceChats");
+
+// Todo: make this more useful for export by adding other columns about workspace, user, time, etc for post-filtering.
+async function convertToCSV(workspaceChatsMap) {
+  const rows = ["role,content"];
+  for (const workspaceChats of Object.values(workspaceChatsMap)) {
+    for (const message of workspaceChats.messages) {
+      // Escape double quotes and wrap content in double quotes
+      const escapedContent = `"${message.content
+        .replace(/"/g, '""')
+        .replace(/\n/g, " ")}"`;
+      rows.push(`${message.role},${escapedContent}`);
+    }
+  }
+  return rows.join("\n");
+}
+
+async function convertToJSON(workspaceChatsMap) {
+  const allMessages = [].concat.apply(
+    [],
+    Object.values(workspaceChatsMap).map((workspace) => workspace.messages)
+  );
+  return JSON.stringify(allMessages);
+}
+
+async function convertToJSONL(workspaceChatsMap) {
+  return Object.values(workspaceChatsMap)
+    .map((workspaceChats) => JSON.stringify(workspaceChats))
+    .join("\n");
+}
+
+async function prepareWorkspaceChatsForExport() {
+  const chats = await WorkspaceChats.whereWithData({}, null, null, {
+    id: "asc",
+  });
+  const workspaceIds = [...new Set(chats.map((chat) => chat.workspaceId))];
+
+  const workspacesWithPrompts = await Promise.all(
+    workspaceIds.map((id) => Workspace.get({ id: Number(id) }))
+  );
+
+  const workspacePromptsMap = workspacesWithPrompts.reduce((acc, workspace) => {
+    acc[workspace.id] = workspace.openAiPrompt;
+    return acc;
+  }, {});
+
+  const workspaceChatsMap = chats.reduce((acc, chat) => {
+    const { prompt, response, workspaceId } = chat;
+    const responseJson = JSON.parse(response);
+
+    if (!acc[workspaceId]) {
+      acc[workspaceId] = {
+        messages: [
+          {
+            role: "system",
+            content:
+              workspacePromptsMap[workspaceId] ||
+              "Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.",
+          },
+        ],
+      };
+    }
+
+    acc[workspaceId].messages.push(
+      {
+        role: "user",
+        content: prompt,
+      },
+      {
+        role: "assistant",
+        content: responseJson.text,
+      }
+    );
+
+    return acc;
+  }, {});
+
+  return workspaceChatsMap;
+}
+
+const exportMap = {
+  json: {
+    contentType: "application/json",
+    func: convertToJSON,
+  },
+  csv: {
+    contentType: "text/csv",
+    func: convertToCSV,
+  },
+  jsonl: {
+    contentType: "application/jsonl",
+    func: convertToJSONL,
+  },
+};
+
+async function exportChatsAsType(workspaceChatsMap, format = "jsonl") {
+  const { contentType, func } = exportMap.hasOwnProperty(format)
+    ? exportMap[format]
+    : exportMap.jsonl;
+  return {
+    contentType,
+    data: await func(workspaceChatsMap),
+  };
+}
+
+module.exports = {
+  prepareWorkspaceChatsForExport,
+  exportChatsAsType,
+};
diff --git a/server/utils/middleware/multiUserProtected.js b/server/utils/middleware/multiUserProtected.js
index 7de09ac57..f8a28c962 100644
--- a/server/utils/middleware/multiUserProtected.js
+++ b/server/utils/middleware/multiUserProtected.js
@@ -1,41 +1,71 @@
 const { SystemSettings } = require("../../models/systemSettings");
 const { userFromSession } = require("../http");
-
-const ROLES = ["admin", "manager"];
+const ROLES = {
+  all: "<all>",
+  admin: "admin",
+  manager: "manager",
+  default: "default",
+};
+const DEFAULT_ROLES = [ROLES.admin, ROLES.admin];
 
 // 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();
+function strictMultiUserRoleValid(allowedRoles = DEFAULT_ROLES) {
+  return async (request, response, next) => {
+    // If the access-control is allowable for all - skip validations and continue;
+    if (allowedRoles.includes(ROLES.all)) {
+      next();
+      return;
+    }
 
-  const user =
-    response.locals?.user ?? (await userFromSession(request, response));
-  if (!ROLES.includes(user?.role)) return response.sendStatus(401).end();
+    const multiUserMode =
+      response.locals?.multiUserMode ??
+      (await SystemSettings.isMultiUserMode());
+    if (!multiUserMode) return response.sendStatus(401).end();
 
-  next();
+    const user =
+      response.locals?.user ?? (await userFromSession(request, response));
+    if (allowedRoles.includes(user?.role)) {
+      next();
+      return;
+    }
+    return response.sendStatus(401).end();
+  };
 }
 
 // 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;
-  }
+function flexUserRoleValid(allowedRoles = DEFAULT_ROLES) {
+  return async (request, response, next) => {
+    // If the access-control is allowable for all - skip validations and continue;
+    // It does not matter if multi-user or not.
+    if (allowedRoles.includes(ROLES.all)) {
+      next();
+      return;
+    }
 
-  const user =
-    response.locals?.user ?? (await userFromSession(request, response));
-  if (!ROLES.includes(user?.role)) return response.sendStatus(401).end();
+    // Bypass if not in multi-user mode
+    const multiUserMode =
+      response.locals?.multiUserMode ??
+      (await SystemSettings.isMultiUserMode());
+    if (!multiUserMode) {
+      next();
+      return;
+    }
 
-  next();
+    const user =
+      response.locals?.user ?? (await userFromSession(request, response));
+    if (allowedRoles.includes(user?.role)) {
+      next();
+      return;
+    }
+    return response.sendStatus(401).end();
+  };
 }
 
 module.exports = {
+  ROLES,
   strictMultiUserRoleValid,
   flexUserRoleValid,
 };

Vulnerable code:                 element={<ManagerRoute Component={GeneralApiKeys} />}
                {/* Admin/manager Multi-user Settings */}
                {!!user && user?.role !== "default" && (
                  <>
                    <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" />}
                    />
                  </>
                )}



                {(!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.settings.dataConnectors.list()}
                      btnText="Data Connectors"
                      icon={<Plugs 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" />}
                      />
                    </>
                  )}
const Option = ({ btnText, icon, href }) => {
    setSuspended(!suspended);
    await Admin.updateUser(user.id, { suspended: suspended ? 0 : 1 });
    rowRef?.current?.remove();
    await Admin.deleteUser(user.id);
          {currUser?.role !== "default" && (
          {currUser?.id !== user.id && currUser?.role !== "default" && (
    "Can view all workspaces and modify all settings.",
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
        // Check to make sure with this update that includes a role change to
        // something other than admin that we still have at least one admin left.
        if (
          updates.hasOwnProperty("role") && // has admin prop to change
          updates.role !== "admin" && // and we are changing to non-admin
          user.role === "admin" // and they currently are an admin
        ) {
          const adminCount = await User.count({ role: "admin" });
          if (adminCount - 1 <= 0) {
            response.status(200).json({
              success: false,
              error:
                "No system admins will remain if you do this. Update failed.",
            });
            return;
          }
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
      // Check to make sure with this update that includes a role change to
      // something other than admin that we still have at least one admin left.
      if (
        updates.hasOwnProperty("role") && // has admin prop to change
        updates.role !== "admin" && // and we are changing to non-admin
        user.role === "admin" // and they currently are an admin
      ) {
        const adminCount = await User.count({ role: "admin" });
        if (adminCount - 1 <= 0) {
          response.status(200).json({
            success: false,
            error:
              "No system admins will remain if you do this. Update failed.",
          });
          return;
        }
    [validatedRequest],
        if (multiUserMode(response) && user.role !== "admin") {
    [validatedRequest, flexUserRoleValid],
    [validatedRequest, flexUserRoleValid],
    [validatedRequest, flexUserRoleValid],
const { Workspace } = require("../models/workspace");
const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected");
const { convertToCSV, convertToJSON, convertToJSONL } = require("./utils");
    [validatedRequest, flexUserRoleValid],
        const user = await userFromSession(request, response);
        if (!!user && user.role !== "admin") {
          response.sendStatus(401).end();
          return;
        }

          role: "admin",
  app.get("/system/multi-user-mode", async (request, response) => {
  app.get("/system/logo", async function (request, response) {
  app.get("/system/pfp/:id", async function (request, response) {
    try {
      const { id } = request.params;
      const pfpPath = await determinePfpFilepath(id);
      if (!pfpPath) {
        response.sendStatus(204).end();
        return;
      }
      const { found, buffer, size, mime } = fetchPfp(pfpPath);
      if (!found) {
        response.sendStatus(204).end();

      response.writeHead(200, {
        "Content-Type": mime || "image/png",
        "Content-Disposition": `attachment; filename=${path.basename(pfpPath)}`,
        "Content-Length": size,
      });
      response.end(Buffer.from(buffer, "base64"));
      return;
    } catch (error) {
      console.error("Error processing the logo request:", error);
      response.status(500).json({ message: "Internal server error" });
  });
    [validatedRequest, flexUserRoleValid],

        const oldPfpFilename = normalizePath(userRecord.pfpFilename);
            `../storage/assets/pfp/${oldPfpFilename}`
    [validatedRequest, flexUserRoleValid],
        const oldPfpFilename = normalizePath(userRecord.pfpFilename);
            `../storage/assets/pfp/${oldPfpFilename}`
    [validatedRequest, flexUserRoleValid],
  app.get("/system/is-default-logo", async (request, response) => {
    [validatedRequest, flexUserRoleValid],
        if (["admin", "manager"].includes(user?.role)) {
  app.get("/system/welcome-messages", async function (request, response) {
    try {
      const welcomeMessages = await WelcomeMessages.getMessages();
      response.status(200).json({ success: true, welcomeMessages });
    } catch (error) {
      console.error("Error fetching welcome messages:", error);
      response
        .status(500)
        .json({ success: false, message: "Internal server error" });
  });
    [validatedRequest, flexUserRoleValid],
    [validatedRequest, flexUserRoleValid],
    [validatedRequest, flexUserRoleValid],
    [validatedRequest, flexUserRoleValid],
        const chats = await WorkspaceChats.whereWithData({}, null, null, {
          id: "asc",
        });
        const workspaceIds = [
          ...new Set(chats.map((chat) => chat.workspaceId)),
        ];

        const workspacesWithPrompts = await Promise.all(
          workspaceIds.map((id) => Workspace.get({ id: Number(id) }))
        );

        const workspacePromptsMap = workspacesWithPrompts.reduce(
          (acc, workspace) => {
            acc[workspace.id] = workspace.openAiPrompt;
            return acc;
          },
          {}
        );

        const workspaceChatsMap = chats.reduce((acc, chat) => {
          const { prompt, response, workspaceId } = chat;
          const responseJson = JSON.parse(response);

          if (!acc[workspaceId]) {
            acc[workspaceId] = {
              messages: [
                {
                  role: "system",
                  content:
                    workspacePromptsMap[workspaceId] ||
                    "Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.",
                },
              ],
            };
          }

          acc[workspaceId].messages.push(
            {
              role: "user",
              content: prompt,
            },
            {
              role: "assistant",
              content: responseJson.text,
            }
          );

          return acc;
        }, {});

        let output;
        switch (type.toLowerCase()) {
          case "json": {
            response.setHeader("Content-Type", "application/json");
            output = await convertToJSON(workspaceChatsMap);
            break;
          }
          case "csv": {
            response.setHeader("Content-Type", "text/csv");
            output = await convertToCSV(workspaceChatsMap);
            break;
          }
          // JSONL default
          default: {
            response.setHeader("Content-Type", "application/jsonl");
            output = await convertToJSONL(workspaceChatsMap);
            break;
          }
        }

        response.status(200).send(output);
async function convertToCSV(workspaceChatsMap) {
  const rows = ["role,content"];
  for (const workspaceChats of Object.values(workspaceChatsMap)) {
    for (const message of workspaceChats.messages) {
      // Escape double quotes and wrap content in double quotes
      const escapedContent = `"${message.content
        .replace(/"/g, '""')
        .replace(/\n/g, " ")}"`;
      rows.push(`${message.role},${escapedContent}`);
    }
  }
  return rows.join("\n");
}

async function convertToJSON(workspaceChatsMap) {
  const allMessages = [].concat.apply(
    [],
    Object.values(workspaceChatsMap).map((workspace) => workspace.messages)
  );
  return JSON.stringify(allMessages);
}

async function convertToJSONL(workspaceChatsMap) {
  return Object.values(workspaceChatsMap)
    .map((workspaceChats) => JSON.stringify(workspaceChats))
    .join("\n");
}

function utilEndpoints(app) {
  if (!app) return;

  app.get("/utils/metrics", async (_, response) => {
    try {
      const metrics = {
        online: true,
        version: getGitVersion(),
        mode: (await SystemSettings.isMultiUserMode())
          ? "multi-user"
          : "single-user",
        vectorDB: process.env.VECTOR_DB || "lancedb",
        storage: await getDiskStorage(),
      };
      response.status(200).json(metrics);
    } catch (e) {
      console.error(e);
      response.sendStatus(500).end();
    }
  });
}

  convertToCSV,
  convertToJSON,
  convertToJSONL,
const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected");
    [validatedRequest, flexUserRoleValid],
    [validatedRequest],
    [validatedRequest],
    [validatedRequest],
    [validatedRequest, flexUserRoleValid],
  app.get("/workspaces", [validatedRequest], async (request, response) => {
    try {
      const user = await userFromSession(request, response);
      const workspaces = multiUserMode(response)
        ? await Workspace.whereWithUser(user)
        : await Workspace.where();
      response.status(200).json({ workspaces });
    } catch (e) {
      console.log(e.message, e);
      response.sendStatus(500).end();
  });
  app.get("/workspace/:slug", [validatedRequest], async (request, response) => {
    try {
      const { slug } = request.params;
      const user = await userFromSession(request, response);
      const workspace = multiUserMode(response)
        ? await Workspace.getWithUser(user, { slug })
        : await Workspace.get({ slug });
      response.status(200).json({ workspace });
    } catch (e) {
      console.log(e.message, e);
      response.sendStatus(500).end();
  });
    [validatedRequest],
    if (["admin", "manager"].includes(user.role)) return this.get(clause);
    if (["admin", "manager"].includes(user.role))

const ROLES = ["admin", "manager"];
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();
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();
Fixed code:                 element={<AdminRoute Component={GeneralApiKeys} />}
                <Option
                  href={paths.settings.system()}
                  btnText="System Preferences"
                  icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
                  user={user}
                  allowedRole={["admin", "manager"]}
                />
                <Option
                  href={paths.settings.invites()}
                  btnText="Invitation"
                  icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />}
                  user={user}
                  allowedRole={["admin", "manager"]}
                />
                <Option
                  href={paths.settings.users()}
                  btnText="Users"
                  icon={<Users className="h-5 w-5 flex-shrink-0" />}
                  user={user}
                  allowedRole={["admin", "manager"]}
                />
                <Option
                  href={paths.settings.workspaces()}
                  btnText="Workspaces"
                  icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
                  user={user}
                  allowedRole={["admin", "manager"]}
                />
                  user={user}
                  flex={true}
                  allowedRole={["admin", "manager"]}
                  user={user}
                  flex={true}
                  allowedRole={["admin", "manager"]}
                  user={user}
                  flex={true}
                  allowedRole={["admin"]}
                />
                <Option
                  href={paths.settings.llmPreference()}
                  btnText="LLM Preference"
                  icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
                  user={user}
                  flex={true}
                  allowedRole={["admin"]}
                />
                <Option
                  href={paths.settings.embeddingPreference()}
                  btnText="Embedding Preference"
                  icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
                  user={user}
                  flex={true}
                  allowedRole={["admin"]}
                />
                <Option
                  href={paths.settings.vectorDatabase()}
                  btnText="Vector Database"
                  icon={<Database className="h-5 w-5 flex-shrink-0" />}
                  user={user}
                  flex={true}
                  allowedRole={["admin"]}
                />
                <Option
                  href={paths.settings.dataConnectors.list()}
                  btnText="Data Connectors"
                  icon={<Plugs className="h-5 w-5 flex-shrink-0" />}
                  user={user}
                  flex={true}
                  allowedRole={["admin", "manager"]}
                  user={user}
                  flex={true}
                  allowedRole={["admin", "manager"]}
                    user={user}
                    allowedRole={["admin", "manager"]}
                    user={user}
                    allowedRole={["admin", "manager"]}
                    user={user}
                    allowedRole={["admin", "manager"]}
                    user={user}
                    allowedRole={["admin", "manager"]}
                    user={user}
                    flex={true}
                    allowedRole={["admin", "manager"]}
                    user={user}
                    flex={true}
                    allowedRole={["admin", "manager"]}
                    user={user}
                    flex={true}
                    allowedRole={["admin"]}
                  />
                  <Option
                    href={paths.settings.llmPreference()}
                    btnText="LLM Preference"
                    icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
                    user={user}
                    flex={true}
                    allowedRole={["admin"]}
                  />
                  <Option
                    href={paths.settings.embeddingPreference()}
                    btnText="Embedding Preference"
                    icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
                    user={user}
                    flex={true}
                    allowedRole={["admin"]}
                  />
                  <Option
                    href={paths.settings.vectorDatabase()}
                    btnText="Vector Database"
                    icon={<Database className="h-5 w-5 flex-shrink-0" />}
                    user={user}
                    flex={true}
                    allowedRole={["admin"]}
                  />
                  <Option
                    href={paths.settings.dataConnectors.list()}
                    btnText="Data Connectors"
                    icon={<Plugs className="h-5 w-5 flex-shrink-0" />}
                    user={user}
                    flex={true}
                    allowedRole={["admin", "manager"]}
                    user={user}
                    flex={true}
                    allowedRole={["admin", "manager"]}
const Option = ({
  btnText,
  icon,
  href,
  flex = false,
  user = null,
  allowedRole = [],
}) => {

  // Option only for multi-user
  if (!flex && !allowedRole.includes(user?.role)) return null;

  // Option is dual-mode, but user exists, we need to check permissions
  if (flex && !!user && !allowedRole.includes(user?.role)) return null;
      headers: baseHeaders(),
      headers: baseHeaders(),
import showToast from "@/utils/toast";

const ModMap = {
  admin: ["admin", "manager", "default"],
  manager: ["manager", "default"],
  default: [],
};
  const canModify = ModMap[currUser?.role || "default"].includes(user.role);

    const { success, error } = await Admin.updateUser(user.id, {
      suspended: suspended ? 0 : 1,
    });
    if (!success) showToast(error, "error", { clear: true });
    if (success) {
      showToast(
        `User ${!suspended ? "has been suspended" : "is no longer suspended"}.`,
        "success",
        { clear: true }
      );
      setSuspended(!suspended);
    }
    const { success, error } = await Admin.deleteUser(user.id);
    if (!success) showToast(error, "error", { clear: true });
    if (success) {
      rowRef?.current?.remove();
      showToast("User deleted from system.", "success", { clear: true });
    }
          {canModify && (
          {currUser?.id !== user.id && canModify && (
    "Can view, create, and delete any workspaces and modify workspace-specific settings.",
    "Can create, update and invite new users to the instance.",
const {
  validRoleSelection,
  canModifyAdmin,
  validCanModify,
} = require("../utils/helpers/admin");
  ROLES,
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
        const currUser = await userFromSession(request, response);
        const roleValidation = validRoleSelection(currUser, newUserParams);

        if (!roleValidation.valid) {
          response
            .status(200)
            .json({ user: null, error: roleValidation.error });
          return;
        }

    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
        const currUser = await userFromSession(request, response);
        const canModify = validCanModify(currUser, user);
        if (!canModify.valid) {
          response.status(200).json({ success: false, error: canModify.error });
          return;
        }

        const roleValidation = validRoleSelection(currUser, updates);
        if (!roleValidation.valid) {
          response
            .status(200)
            .json({ success: false, error: roleValidation.error });
          return;
        }

        const validAdminRoleModification = await canModifyAdmin(user, updates);
        if (!validAdminRoleModification.valid) {
          response
            .status(200)
            .json({ success: false, error: validAdminRoleModification.error });
          return;
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
        const currUser = await userFromSession(request, response);
        const user = await User.get({ id: Number(id) });

        const canModify = validCanModify(currUser, user);
        if (!canModify.valid) {
          response.status(200).json({ success: false, error: canModify.error });
          return;
        }

    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
const { canModifyAdmin } = require("../../../utils/helpers/admin");
      const validAdminRoleModification = await canModifyAdmin(user, updates);
      if (!validAdminRoleModification.valid) {
        response
          .status(200)
          .json({ success: false, error: validAdminRoleModification.error });
        return;
const {
  ROLES,
  flexUserRoleValid,
} = require("../utils/middleware/multiUserProtected");
    [validatedRequest, flexUserRoleValid([ROLES.all])],
        if (multiUserMode(response) && user.role !== ROLES.admin) {
  ROLES,
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
const {
  flexUserRoleValid,
  ROLES,
} = require("../utils/middleware/multiUserProtected");
const {
  prepareWorkspaceChatsForExport,
  exportChatsAsType,
} = require("../utils/helpers/chat/convertTo");
    [validatedRequest, flexUserRoleValid([ROLES.admin])],
          role: ROLES.admin,
  app.get("/system/multi-user-mode", async (_, response) => {
  app.get("/system/logo", async function (_, response) {
  app.get(
    "/system/pfp/:id",
    [validatedRequest, flexUserRoleValid([ROLES.all])],
    async function (request, response) {
      try {
        const { id } = request.params;
        const pfpPath = await determinePfpFilepath(id);
        if (!pfpPath) {
          response.sendStatus(204).end();
          return;
        }
        const { found, buffer, size, mime } = fetchPfp(pfpPath);
        if (!found) {
          response.sendStatus(204).end();
          return;
        }

        response.writeHead(200, {
          "Content-Type": mime || "image/png",
          "Content-Disposition": `attachment; filename=${path.basename(
            pfpPath
          )}`,
          "Content-Length": size,
        });
        response.end(Buffer.from(buffer, "base64"));
      } catch (error) {
        console.error("Error processing the logo request:", error);
        response.status(500).json({ message: "Internal server error" });
  );
    [validatedRequest, flexUserRoleValid([ROLES.all])],
        const oldPfpFilename = userRecord.pfpFilename;
            `../storage/assets/pfp/${normalizePath(userRecord.pfpFilename)}`
    [validatedRequest, flexUserRoleValid([ROLES.all])],
        const oldPfpFilename = userRecord.pfpFilename;

            `../storage/assets/pfp/${normalizePath(oldPfpFilename)}`
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  app.get("/system/is-default-logo", async (_, response) => {
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
        if ([ROLES.admin, ROLES.manager].includes(user?.role)) {
  app.get(
    "/system/welcome-messages",
    [validatedRequest, flexUserRoleValid([ROLES.all])],
    async function (_, response) {
      try {
        const welcomeMessages = await WelcomeMessages.getMessages();
        response.status(200).json({ success: true, welcomeMessages });
      } catch (error) {
        console.error("Error fetching welcome messages:", error);
        response
          .status(500)
          .json({ success: false, message: "Internal server error" });
      }
  );
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.manager, ROLES.admin])],
        const chats = await prepareWorkspaceChatsForExport();
        const { contentType, data } = await exportChatsAsType(chats, type);
        response.setHeader("Content-Type", contentType);
        response.status(200).send(data);
  // Used for when a user in multi-user updates their own profile
  // from the UI.
function utilEndpoints(app) {
  if (!app) return;

  app.get("/utils/metrics", async (_, response) => {
    try {
      const metrics = {
        online: true,
        version: getGitVersion(),
        mode: (await SystemSettings.isMultiUserMode())
          ? "multi-user"
          : "single-user",
        vectorDB: process.env.VECTOR_DB || "lancedb",
        storage: await getDiskStorage(),
      };
      response.status(200).json(metrics);
    } catch (e) {
      console.error(e);
      response.sendStatus(500).end();
    }
  });
}

const {
  flexUserRoleValid,
  ROLES,
} = require("../utils/middleware/multiUserProtected");
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  app.get(
    "/workspaces",
    [validatedRequest, flexUserRoleValid([ROLES.all])],
    async (request, response) => {
      try {
        const user = await userFromSession(request, response);
        const workspaces = multiUserMode(response)
          ? await Workspace.whereWithUser(user)
          : await Workspace.where();
        response.status(200).json({ workspaces });
      } catch (e) {
        console.log(e.message, e);
        response.sendStatus(500).end();
      }
  );
  app.get(
    "/workspace/:slug",
    [validatedRequest, flexUserRoleValid([ROLES.all])],
    async (request, response) => {
      try {
        const { slug } = request.params;
        const user = await userFromSession(request, response);
        const workspace = multiUserMode(response)
          ? await Workspace.getWithUser(user, { slug })
          : await Workspace.get({ slug });
        response.status(200).json({ workspace });
      } catch (e) {
        console.log(e.message, e);
        response.sendStatus(500).end();
      }
  );
    [validatedRequest, flexUserRoleValid([ROLES.all])],
const { ROLES } = require("../utils/middleware/multiUserProtected");
    if ([ROLES.admin, ROLES.manager].includes(user.role))
      return this.get(clause);
    if ([ROLES.admin, ROLES.manager].includes(user.role))
const { User } = require("../../../models/user");
const { ROLES } = require("../../middleware/multiUserProtected");

// When a user is updating or creating a user in multi-user, we need to check if they
// are allowed to do this and that the new or existing user will be at or below their permission level.
// the user executing this function should be an admin or manager.
function validRoleSelection(currentUser = {}, newUserParams = {}) {
  if (!newUserParams.hasOwnProperty("role"))
    return { valid: true, error: null }; // not updating role, so skip.
  if (currentUser.role === ROLES.admin) return { valid: true, error: null };
  if (currentUser.role === ROLES.manager) {
    const validRoles = [ROLES.manager, ROLES.default];
    if (!validRoles.includes(newUserParams.role))
      return { valid: false, error: "Invalid role selection for user." };
    return { valid: true, error: null };
  }
  return { valid: false, error: "Invalid condition for caller." };
}

// Check to make sure with this update that includes a role change to an existing admin to a non-admin
// that we still have at least one admin left or else they will lock themselves out.
async function canModifyAdmin(userToModify, updates) {
  // if updates don't include role property or the user being modified isn't an admin currently - skip.
  if (!updates.hasOwnProperty("role")) return { valid: true, error: null };
  if (userToModify.role !== ROLES.admin) return { valid: true, error: null };

  const adminCount = await User.count({ role: ROLES.admin });
  if (adminCount - 1 <= 0)
    return {
      valid: false,
      error: "No system admins will remain if you do this. Update failed.",
    };
  return { valid: true, error: null };
}

function validCanModify(currentUser, existingUser) {
  if (currentUser.role === ROLES.admin) return { valid: true, error: null };
  if (currentUser.role === ROLES.manager) {
    const validRoles = [ROLES.manager, ROLES.default];
    if (!validRoles.includes(existingUser.role))
      return { valid: false, error: "Cannot perform that action on user." };
    return { valid: true, error: null };
  }

  return { valid: false, error: "Invalid condition for caller." };
}

module.exports = {
  validCanModify,
  validRoleSelection,
  canModifyAdmin,
};
// Helpers that convert workspace chats to some supported format
// for external use by the user.

const { Workspace } = require("../../../models/workspace");
const { WorkspaceChats } = require("../../../models/workspaceChats");

// Todo: make this more useful for export by adding other columns about workspace, user, time, etc for post-filtering.
async function convertToCSV(workspaceChatsMap) {
  const rows = ["role,content"];
  for (const workspaceChats of Object.values(workspaceChatsMap)) {
    for (const message of workspaceChats.messages) {
      // Escape double quotes and wrap content in double quotes
      const escapedContent = `"${message.content
        .replace(/"/g, '""')
        .replace(/\n/g, " ")}"`;
      rows.push(`${message.role},${escapedContent}`);
    }
  }
  return rows.join("\n");
}

async function convertToJSON(workspaceChatsMap) {
  const allMessages = [].concat.apply(
    [],
    Object.values(workspaceChatsMap).map((workspace) => workspace.messages)
  );
  return JSON.stringify(allMessages);
}

async function convertToJSONL(workspaceChatsMap) {
  return Object.values(workspaceChatsMap)
    .map((workspaceChats) => JSON.stringify(workspaceChats))
    .join("\n");
}

async function prepareWorkspaceChatsForExport() {
  const chats = await WorkspaceChats.whereWithData({}, null, null, {
    id: "asc",
  });
  const workspaceIds = [...new Set(chats.map((chat) => chat.workspaceId))];

  const workspacesWithPrompts = await Promise.all(
    workspaceIds.map((id) => Workspace.get({ id: Number(id) }))
  );

  const workspacePromptsMap = workspacesWithPrompts.reduce((acc, workspace) => {
    acc[workspace.id] = workspace.openAiPrompt;
    return acc;
  }, {});

  const workspaceChatsMap = chats.reduce((acc, chat) => {
    const { prompt, response, workspaceId } = chat;
    const responseJson = JSON.parse(response);

    if (!acc[workspaceId]) {
      acc[workspaceId] = {
        messages: [
          {
            role: "system",
            content:
              workspacePromptsMap[workspaceId] ||
              "Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.",
          },
        ],
      };
    }

    acc[workspaceId].messages.push(
      {
        role: "user",
        content: prompt,
      },
      {
        role: "assistant",
        content: responseJson.text,
      }
    );

    return acc;
  }, {});

  return workspaceChatsMap;
}

const exportMap = {
  json: {
    contentType: "application/json",
    func: convertToJSON,
  },
  csv: {
    contentType: "text/csv",
    func: convertToCSV,
  },
  jsonl: {
    contentType: "application/jsonl",
    func: convertToJSONL,
  },
};

async function exportChatsAsType(workspaceChatsMap, format = "jsonl") {
  const { contentType, func } = exportMap.hasOwnProperty(format)
    ? exportMap[format]
    : exportMap.jsonl;
  return {
    contentType,
    data: await func(workspaceChatsMap),
  };
}

module.exports = {
  prepareWorkspaceChatsForExport,
  exportChatsAsType,
};
const ROLES = {
  all: "<all>",
  admin: "admin",
  manager: "manager",
  default: "default",
};
const DEFAULT_ROLES = [ROLES.admin, ROLES.admin];
function strictMultiUserRoleValid(allowedRoles = DEFAULT_ROLES) {
  return async (request, response, next) => {
    // If the access-control is allowable for all - skip validations and continue;
    if (allowedRoles.includes(ROLES.all)) {
      next();
      return;
    }
    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 (allowedRoles.includes(user?.role)) {
      next();
      return;
    }
    return response.sendStatus(401).end();
  };
function flexUserRoleValid(allowedRoles = DEFAULT_ROLES) {
  return async (request, response, next) => {
    // If the access-control is allowable for all - skip validations and continue;
    // It does not matter if multi-user or not.
    if (allowedRoles.includes(ROLES.all)) {
      next();
      return;
    }
    // Bypass if not in multi-user mode
    const multiUserMode =
      response.locals?.multiUserMode ??
      (await SystemSettings.isMultiUserMode());
    if (!multiUserMode) {
      next();
      return;
    }
    const user =
      response.locals?.user ?? (await userFromSession(request, response));
    if (allowedRoles.includes(user?.role)) {
      next();
      return;
    }
    return response.sendStatus(401).end();
  };
  ROLES,