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