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>
This commit is contained in:
Sean Hatfield 2023-11-13 14:51:16 -08:00 committed by GitHub
parent 7fcf29d769
commit fa29003a46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 462 additions and 456 deletions

View File

@ -1,7 +1,10 @@
import React, { lazy, Suspense } from "react"; import React, { lazy, Suspense } from "react";
import { Routes, Route } from "react-router-dom"; import { Routes, Route } from "react-router-dom";
import { ContextWrapper } from "./AuthContext"; import { ContextWrapper } from "./AuthContext";
import PrivateRoute, { AdminRoute } from "./components/PrivateRoute"; import PrivateRoute, {
AdminRoute,
ManagerRoute,
} from "./components/PrivateRoute";
import { ToastContainer } from "react-toastify"; import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css"; import "react-toastify/dist/ReactToastify.css";
import Login from "./pages/Login"; import Login from "./pages/Login";
@ -48,56 +51,55 @@ export default function App() {
/> />
<Route path="/accept-invite/:code" element={<InvitePage />} /> <Route path="/accept-invite/:code" element={<InvitePage />} />
{/* General Routes */} {/* Admin */}
<Route <Route
path="/general/llm-preference" path="/settings/llm-preference"
element={<PrivateRoute Component={GeneralLLMPreference} />} element={<AdminRoute Component={GeneralLLMPreference} />}
/> />
<Route <Route
path="/general/embedding-preference" path="/settings/embedding-preference"
element={<PrivateRoute Component={GeneralEmbeddingPreference} />} element={<AdminRoute Component={GeneralEmbeddingPreference} />}
/> />
<Route <Route
path="/general/vector-database" path="/settings/vector-database"
element={<PrivateRoute Component={GeneralVectorDatabase} />} element={<AdminRoute Component={GeneralVectorDatabase} />}
/>
{/* Manager */}
<Route
path="/settings/export-import"
element={<ManagerRoute Component={GeneralExportImport} />}
/> />
<Route <Route
path="/general/export-import" path="/settings/security"
element={<PrivateRoute Component={GeneralExportImport} />} element={<ManagerRoute Component={GeneralSecurity} />}
/> />
<Route <Route
path="/general/security" path="/settings/appearance"
element={<PrivateRoute Component={GeneralSecurity} />} element={<ManagerRoute Component={GeneralAppearance} />}
/> />
<Route <Route
path="/general/appearance" path="/settings/api-keys"
element={<PrivateRoute Component={GeneralAppearance} />} element={<ManagerRoute Component={GeneralApiKeys} />}
/> />
<Route <Route
path="/general/api-keys" path="/settings/workspace-chats"
element={<PrivateRoute Component={GeneralApiKeys} />} element={<ManagerRoute Component={GeneralChats} />}
/> />
<Route <Route
path="/general/workspace-chats" path="/settings/system-preferences"
element={<PrivateRoute Component={GeneralChats} />} element={<ManagerRoute Component={AdminSystem} />}
/>
{/* Admin Routes */}
<Route
path="/admin/system-preferences"
element={<AdminRoute Component={AdminSystem} />}
/> />
<Route <Route
path="/admin/invites" path="/settings/invites"
element={<AdminRoute Component={AdminInvites} />} element={<ManagerRoute Component={AdminInvites} />}
/> />
<Route <Route
path="/admin/users" path="/settings/users"
element={<AdminRoute Component={AdminUsers} />} element={<ManagerRoute Component={AdminUsers} />}
/> />
<Route <Route
path="/admin/workspaces" path="/settings/workspaces"
element={<AdminRoute Component={AdminWorkspaces} />} element={<ManagerRoute Component={AdminWorkspaces} />}
/> />
{/* Onboarding Flow */} {/* Onboarding Flow */}
<Route path="/onboarding" element={<OnboardingFlow />} /> <Route path="/onboarding" element={<OnboardingFlow />} />

View File

@ -14,7 +14,7 @@ export default function AnthropicAiOptions({ settings, showAlert = false }) {
</p> </p>
</div> </div>
<a <a
href={paths.general.embeddingPreference()} href={paths.settings.embeddingPreference()}
className="text-sm md:text-base my-2 underline" className="text-sm md:text-base my-2 underline"
> >
Manage embedding &rarr; Manage embedding &rarr;

View File

@ -14,7 +14,7 @@ export default function LMStudioOptions({ settings, showAlert = false }) {
</p> </p>
</div> </div>
<a <a
href={paths.general.embeddingPreference()} href={paths.settings.embeddingPreference()}
className="text-sm md:text-base my-2 underline" className="text-sm md:text-base my-2 underline"
> >
Manage embedding &rarr; Manage embedding &rarr;

View File

@ -4,6 +4,7 @@ import { useParams } from "react-router-dom";
import Workspace from "../../../models/workspace"; import Workspace from "../../../models/workspace";
import System from "../../../models/system"; import System from "../../../models/system";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import useUser from "../../../hooks/useUser";
const DocumentSettings = lazy(() => import("./Documents")); const DocumentSettings = lazy(() => import("./Documents"));
const WorkspaceSettings = lazy(() => import("./Settings")); const WorkspaceSettings = lazy(() => import("./Settings"));
@ -117,9 +118,13 @@ const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
export default memo(ManageWorkspace); export default memo(ManageWorkspace);
export function useManageWorkspaceModal() { export function useManageWorkspaceModal() {
const { user } = useUser();
const [showing, setShowing] = useState(false); const [showing, setShowing] = useState(false);
const showModal = () => { const showModal = () => {
setShowing(true); if (user?.role !== "default") {
setShowing(true);
}
}; };
const hideModal = () => { const hideModal = () => {

View File

@ -14,6 +14,7 @@ function useIsAuthenticated() {
const [isAuthd, setIsAuthed] = useState(null); const [isAuthd, setIsAuthed] = useState(null);
const [shouldRedirectToOnboarding, setShouldRedirectToOnboarding] = const [shouldRedirectToOnboarding, setShouldRedirectToOnboarding] =
useState(false); useState(false);
const [multiUserMode, setMultiUserMode] = useState(false);
useEffect(() => { useEffect(() => {
const validateSession = async () => { const validateSession = async () => {
@ -25,6 +26,8 @@ function useIsAuthenticated() {
AzureOpenAiKey = false, AzureOpenAiKey = false,
} = await System.keys(); } = await System.keys();
setMultiUserMode(MultiUserMode);
// Check for the onboarding redirect condition // Check for the onboarding redirect condition
if ( if (
!MultiUserMode && !MultiUserMode &&
@ -77,11 +80,14 @@ function useIsAuthenticated() {
validateSession(); 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 }) { export function AdminRoute({ Component }) {
const { isAuthd, shouldRedirectToOnboarding } = useIsAuthenticated(); const { isAuthd, shouldRedirectToOnboarding, multiUserMode } =
useIsAuthenticated();
if (isAuthd === null) return <FullScreenLoader />; if (isAuthd === null) return <FullScreenLoader />;
if (shouldRedirectToOnboarding) { if (shouldRedirectToOnboarding) {
@ -89,7 +95,28 @@ export function AdminRoute({ Component }) {
} }
const user = userFromStorage(); const user = userFromStorage();
return isAuthd && user?.role === "admin" ? ( 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) {
return <Navigate to={paths.onboarding()} />;
}
const user = userFromStorage();
return isAuthd && (user?.role !== "default" || !multiUserMode) ? (
<UserMenu> <UserMenu>
<Component /> <Component />
</UserMenu> </UserMenu>

View File

@ -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-[100%] flex flex-col w-full justify-between pt-4 overflow-y-hidden">
<div className="h-auto sidebar-items"> <div className="h-auto sidebar-items">
<div className="flex flex-col gap-y-2 h-[65vh] pb-8 overflow-y-scroll no-scroll"> <div className="flex flex-col gap-y-2 h-[65vh] pb-8 overflow-y-scroll no-scroll">
{/* Admin Settings */} {/* Admin/manager Multi-user Settings */}
{user?.role === "admin" && ( {!!user && user?.role !== "default" && (
<> <>
<Option <Option
href={paths.admin.system()} href={paths.settings.system()}
btnText="System Preferences" btnText="System Preferences"
icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />} icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
/> />
<Option <Option
href={paths.admin.invites()} href={paths.settings.invites()}
btnText="Invitation" btnText="Invitation"
icon={ icon={
<EnvelopeSimple className="h-5 w-5 flex-shrink-0" /> <EnvelopeSimple className="h-5 w-5 flex-shrink-0" />
} }
/> />
<Option <Option
href={paths.admin.users()} href={paths.settings.users()}
btnText="Users" btnText="Users"
icon={<Users className="h-5 w-5 flex-shrink-0" />} icon={<Users className="h-5 w-5 flex-shrink-0" />}
/> />
<Option <Option
href={paths.admin.workspaces()} href={paths.settings.workspaces()}
btnText="Workspaces" btnText="Workspaces"
icon={<BookOpen className="h-5 w-5 flex-shrink-0" />} 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 <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" btnText="Appearance"
icon={<Eye className="h-5 w-5 flex-shrink-0" />} icon={<Eye className="h-5 w-5 flex-shrink-0" />}
/> />
<Option <Option
href={paths.general.apiKeys()} href={paths.settings.apiKeys()}
btnText="API Keys" btnText="API Keys"
icon={<Key className="h-5 w-5 flex-shrink-0" />} 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 <Option
href={paths.general.llmPreference()} href={paths.settings.exportImport()}
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()}
btnText="Export or Import" btnText="Export or Import"
icon={<DownloadSimple className="h-5 w-5 flex-shrink-0" />} 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 <Option
href={paths.general.security()} href={paths.settings.security()}
btnText="Security" btnText="Security"
icon={<Lock className="h-5 w-5 flex-shrink-0" />} icon={<Lock className="h-5 w-5 flex-shrink-0" />}
/> />
</div> </div>
</div> </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 */} {/* Footer */}
<div className="flex justify-center mt-2"> <div className="flex justify-center mt-2">
<div className="flex space-x-4"> <div className="flex space-x-4">
@ -277,73 +265,70 @@ export function SidebarMobileHeader() {
style={{ height: "calc(100vw - -3rem)" }} style={{ height: "calc(100vw - -3rem)" }}
className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll" 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" />}
/>
</>
)}
{/* General Settings */}
<Option <Option
href={paths.general.chats()} 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.chats()}
btnText="Workspace Chat" btnText="Workspace Chat"
icon={ icon={
<ChatCenteredText className="h-5 w-5 flex-shrink-0" /> <ChatCenteredText className="h-5 w-5 flex-shrink-0" />
} }
/> />
<Option <Option
href={paths.general.appearance()} href={paths.settings.appearance()}
btnText="Appearance" btnText="Appearance"
icon={<Eye className="h-5 w-5 flex-shrink-0" />} icon={<Eye className="h-5 w-5 flex-shrink-0" />}
/> />
<Option <Option
href={paths.general.apiKeys()} href={paths.settings.apiKeys()}
btnText="API Keys" btnText="API Keys"
icon={<Key className="h-5 w-5 flex-shrink-0" />} 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 <Option
href={paths.general.llmPreference()} href={paths.settings.exportImport()}
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()}
btnText="Export or Import" btnText="Export or Import"
icon={<DownloadSimple className="h-5 w-5 flex-shrink-0" />} icon={<DownloadSimple className="h-5 w-5 flex-shrink-0" />}
/> />
<Option <Option
href={paths.general.security()} href={paths.settings.security()}
btnText="Security" btnText="Security"
icon={<Lock className="h-5 w-5 flex-shrink-0" />} icon={<Lock className="h-5 w-5 flex-shrink-0" />}
/> />

View File

@ -9,6 +9,7 @@ import paths from "../../../utils/paths";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { GearSix, SquaresFour } from "@phosphor-icons/react"; import { GearSix, SquaresFour } from "@phosphor-icons/react";
import truncate from "truncate"; import truncate from "truncate";
import useUser from "../../../hooks/useUser";
export default function ActiveWorkspaces() { export default function ActiveWorkspaces() {
const { slug } = useParams(); const { slug } = useParams();
@ -17,6 +18,7 @@ export default function ActiveWorkspaces() {
const [workspaces, setWorkspaces] = useState([]); const [workspaces, setWorkspaces] = useState([]);
const [selectedWs, setSelectedWs] = useState(null); const [selectedWs, setSelectedWs] = useState(null);
const { showing, showModal, hideModal } = useManageWorkspaceModal(); const { showing, showModal, hideModal } = useManageWorkspaceModal();
const { user } = useUser();
useEffect(() => { useEffect(() => {
async function getWorkspaces() { async function getWorkspaces() {
@ -90,7 +92,7 @@ export default function ActiveWorkspaces() {
> >
<GearSix <GearSix
weight={settingHover ? "fill" : "regular"} weight={settingHover ? "fill" : "regular"}
hidden={!isActive} hidden={!isActive || user?.role === "default"}
className="h-[20px] w-[20px] transition-all duration-300" className="h-[20px] w-[20px] transition-all duration-300"
/> />
</button> </button>

View File

@ -15,8 +15,10 @@ import ActiveWorkspaces from "./ActiveWorkspaces";
import paths from "../../utils/paths"; import paths from "../../utils/paths";
import { USER_BACKGROUND_COLOR } from "../../utils/constants"; import { USER_BACKGROUND_COLOR } from "../../utils/constants";
import useLogo from "../../hooks/useLogo"; import useLogo from "../../hooks/useLogo";
import useUser from "../../hooks/useUser";
export default function Sidebar() { export default function Sidebar() {
const { user } = useUser();
const { logo } = useLogo(); const { logo } = useLogo();
const sidebarRef = useRef(null); const sidebarRef = useRef(null);
const { const {
@ -43,25 +45,28 @@ export default function Sidebar() {
style={{ objectFit: "contain" }} style={{ objectFit: "contain" }}
/> />
</div> </div>
<div className="flex gap-x-2 items-center text-slate-200"> {(!user || user?.role !== "default") && (
{/* <AdminHome /> */} <div className="flex gap-x-2 items-center text-slate-200">
<SettingsButton /> <SettingsButton />
</div> </div>
)}
</div> </div>
{/* Primary Body */} {/* Primary Body */}
<div className="flex-grow flex flex-col"> <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 flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll">
<div className="flex gap-x-2 items-center justify-between"> <div className="flex gap-x-2 items-center justify-between">
<button {(!user || user?.role !== "default") && (
onClick={showNewWsModal} <button
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" 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"> <Plus className="h-5 w-5" />
New Workspace <p className="text-sidebar text-sm font-semibold">
</p> New Workspace
</button> </p>
</button>
)}
</div> </div>
<ActiveWorkspaces /> <ActiveWorkspaces />
</div> </div>
@ -133,6 +138,7 @@ export function SidebarMobileHeader() {
showModal: showNewWsModal, showModal: showNewWsModal,
hideModal: hideNewWsModal, hideModal: hideNewWsModal,
} = useNewWorkspaceModal(); } = useNewWorkspaceModal();
const { user } = useUser();
useEffect(() => { useEffect(() => {
// Darkens the rest of the screen // Darkens the rest of the screen
@ -197,9 +203,11 @@ export function SidebarMobileHeader() {
style={{ objectFit: "contain" }} style={{ objectFit: "contain" }}
/> />
</div> </div>
<div className="flex gap-x-2 items-center text-slate-500 shink-0"> {(!user || user?.role !== "default") && (
<SettingsButton /> <div className="flex gap-x-2 items-center text-slate-500 shink-0">
</div> <SettingsButton />
</div>
)}
</div> </div>
{/* Primary Body */} {/* Primary Body */}
@ -210,15 +218,17 @@ export function SidebarMobileHeader() {
className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll" className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll"
> >
<div className="flex gap-x-2 items-center justify-between"> <div className="flex gap-x-2 items-center justify-between">
<button {(!user || user?.role !== "default") && (
onClick={showNewWsModal} <button
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" 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"> <Plus className="h-5 w-5" />
New Workspace <p className="text-sidebar text-sm font-semibold">
</p> New Workspace
</button> </p>
</button>
)}
</div> </div>
<ActiveWorkspaces /> <ActiveWorkspaces />
</div> </div>
@ -266,7 +276,7 @@ export function SidebarMobileHeader() {
function SettingsButton() { function SettingsButton() {
return ( return (
<a <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" 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" /> <Wrench className="h-4 w-4" weight="fill" />

View File

@ -10,6 +10,7 @@ import { isMobile } from "react-device-detect";
import ManageWorkspace, { import ManageWorkspace, {
useManageWorkspaceModal, useManageWorkspaceModal,
} from "../../../Modals/MangeWorkspace"; } from "../../../Modals/MangeWorkspace";
import useUser from "../../../../hooks/useUser";
export default function PromptInput({ export default function PromptInput({
workspace, workspace,
@ -22,6 +23,7 @@ export default function PromptInput({
const { showing, showModal, hideModal } = useManageWorkspaceModal(); const { showing, showModal, hideModal } = useManageWorkspaceModal();
const formRef = useRef(null); const formRef = useRef(null);
const [_, setFocused] = useState(false); const [_, setFocused] = useState(false);
const { user } = useUser();
const handleSubmit = (e) => { const handleSubmit = (e) => {
setFocused(false); setFocused(false);
@ -86,11 +88,14 @@ export default function PromptInput({
</div> </div>
<div className="flex justify-between py-3.5"> <div className="flex justify-between py-3.5">
<div className="flex gap-2"> <div className="flex gap-2">
<Gear {user?.role !== "default" && (
onClick={showModal} <Gear
className="w-7 h-7 text-white/60 hover:text-white cursor-pointer" onClick={showModal}
weight="fill" className="w-7 h-7 text-white/60 hover:text-white cursor-pointer"
/> weight="fill"
/>
)}
<ChatModeSelector workspace={workspace} /> <ChatModeSelector workspace={workspace} />
{/* <TextT {/* <TextT
className="w-7 h-7 text-white/30 cursor-not-allowed" className="w-7 h-7 text-white/30 cursor-not-allowed"

View File

@ -1,6 +1,8 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { X } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react";
import Admin from "../../../../models/admin"; import Admin from "../../../../models/admin";
import { userFromStorage } from "../../../../utils/request";
import { RoleHintDisplay } from "..";
const DIALOG_ID = `new-user-modal`; const DIALOG_ID = `new-user-modal`;
@ -11,6 +13,7 @@ function hideModal() {
export const NewUserModalId = DIALOG_ID; export const NewUserModalId = DIALOG_ID;
export default function NewUserModal() { export default function NewUserModal() {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [role, setRole] = useState("default");
const handleCreate = async (e) => { const handleCreate = async (e) => {
setError(null); setError(null);
e.preventDefault(); e.preventDefault();
@ -22,6 +25,8 @@ export default function NewUserModal() {
setError(error); setError(error);
}; };
const user = userFromStorage();
return ( return (
<dialog id={DIALOG_ID} className="bg-transparent outline-none"> <dialog id={DIALOG_ID} className="bg-transparent outline-none">
<div className="relative w-full max-w-2xl max-h-full"> <div className="relative w-full max-w-2xl max-h-full">
@ -87,11 +92,16 @@ export default function NewUserModal() {
name="role" name="role"
required={true} required={true}
defaultValue={"default"} 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" 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="default">Default</option>
<option value="admin">Administrator</option> <option value="manager">Manager </option>
{user?.role === "admin" && (
<option value="admin">Administrator</option>
)}
</select> </select>
<RoleHintDisplay role={role} />
</div> </div>
{error && ( {error && (
<p className="text-red-400 text-sm">Error: {error}</p> <p className="text-red-400 text-sm">Error: {error}</p>

View File

@ -1,10 +1,12 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { X } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react";
import Admin from "../../../../../models/admin"; import Admin from "../../../../../models/admin";
import { RoleHintDisplay } from "../..";
export const EditUserModalId = (user) => `edit-user-${user.id}-modal`; 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 [error, setError] = useState(null);
const hideModal = () => { const hideModal = () => {
@ -90,11 +92,16 @@ export default function EditUserModal({ user }) {
name="role" name="role"
required={true} required={true}
defaultValue={user.role} 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" 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="default">Default</option>
<option value="admin">Administrator</option> <option value="manager">Manager</option>
{currentUser?.role === "admin" && (
<option value="admin">Administrator</option>
)}
</select> </select>
<RoleHintDisplay role={role} />
</div> </div>
{error && ( {error && (
<p className="text-red-400 text-sm">Error: {error}</p> <p className="text-red-400 text-sm">Error: {error}</p>

View File

@ -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">{titleCase(user.role)}</td>
<td className="px-6 py-4">{user.createdAt}</td> <td className="px-6 py-4">{user.createdAt}</td>
<td className="px-6 py-4 flex items-center gap-x-6"> <td className="px-6 py-4 flex items-center gap-x-6">
<button {currUser?.role !== "default" && (
onClick={() => <button
document?.getElementById(EditUserModalId(user))?.showModal() 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" }
> 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> <DotsThreeOutline weight="fill" className="h-5 w-5" />
{currUser.id !== user.id && ( </button>
)}
{currUser?.id !== user.id && currUser?.role !== "default" && (
<> <>
<button <button
onClick={handleSuspend} onClick={handleSuspend}
@ -66,7 +68,7 @@ export default function UserRow({ currUser, user }) {
)} )}
</td> </td>
</tr> </tr>
<EditUserModal user={user} /> <EditUserModal currentUser={currUser} user={user} />
</> </>
); );
} }

View File

@ -100,3 +100,35 @@ function UsersContainer() {
</table> </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>
);
}

View File

@ -55,7 +55,7 @@ function MultiUserMode() {
window.localStorage.removeItem(AUTH_USER); window.localStorage.removeItem(AUTH_USER);
window.localStorage.removeItem(AUTH_TOKEN); window.localStorage.removeItem(AUTH_TOKEN);
window.localStorage.removeItem(AUTH_TIMESTAMP); window.localStorage.removeItem(AUTH_TIMESTAMP);
window.location = paths.admin.users(); window.location = paths.settings.users();
}, 2_000); }, 2_000);
return; return;
} }

View File

@ -39,47 +39,42 @@ export default {
apiDocs: () => { apiDocs: () => {
return `${API_BASE}/docs`; return `${API_BASE}/docs`;
}, },
general: { settings: {
llmPreference: () => {
return "/general/llm-preference";
},
embeddingPreference: () => {
return "/general/embedding-preference";
},
vectorDatabase: () => {
return "/general/vector-database";
},
exportImport: () => {
return "/general/export-import";
},
security: () => {
return "/general/security";
},
appearance: () => {
return "/general/appearance";
},
apiKeys: () => {
return "/general/api-keys";
},
chats: () => {
return "/general/workspace-chats";
},
},
admin: {
system: () => { system: () => {
return `/admin/system-preferences`; return `/settings/system-preferences`;
}, },
users: () => { users: () => {
return `/admin/users`; return `/settings/users`;
}, },
invites: () => { invites: () => {
return `/admin/invites`; return `/settings/invites`;
}, },
workspaces: () => { workspaces: () => {
return `/admin/workspaces`; return `/settings/workspaces`;
}, },
chats: () => { chats: () => {
return "/admin/workspace-chats"; return "/settings/workspace-chats";
},
llmPreference: () => {
return "/settings/llm-preference";
},
embeddingPreference: () => {
return "/settings/embedding-preference";
},
vectorDatabase: () => {
return "/settings/vector-database";
},
exportImport: () => {
return "/settings/export-import";
},
security: () => {
return "/settings/security";
},
appearance: () => {
return "/settings/appearance";
},
apiKeys: () => {
return "/settings/api-keys";
}, },
}, },
}; };

View File

@ -7,41 +7,37 @@ const { DocumentVectors } = require("../models/vectors");
const { Workspace } = require("../models/workspace"); const { Workspace } = require("../models/workspace");
const { WorkspaceChats } = require("../models/workspaceChats"); const { WorkspaceChats } = require("../models/workspaceChats");
const { getVectorDbClass } = require("../utils/helpers"); 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"); const { validatedRequest } = require("../utils/middleware/validatedRequest");
function adminEndpoints(app) { function adminEndpoints(app) {
if (!app) return; if (!app) return;
app.get("/admin/users", [validatedRequest], async (request, response) => { app.get(
try { "/admin/users",
const user = await userFromSession(request, response); [validatedRequest, strictMultiUserRoleValid],
if (!user || user?.role !== "admin") { async (_request, response) => {
response.sendStatus(401).end(); try {
return; 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( app.post(
"/admin/users/new", "/admin/users/new",
[validatedRequest], [validatedRequest, strictMultiUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response);
if (!user || user?.role !== "admin") {
response.sendStatus(401).end();
return;
}
const newUserParams = reqBody(request); const newUserParams = reqBody(request);
const { user: newUser, error } = await User.create(newUserParams); const { user: newUser, error } = await User.create(newUserParams);
response.status(200).json({ user: newUser, error }); response.status(200).json({ user: newUser, error });
@ -52,34 +48,27 @@ function adminEndpoints(app) {
} }
); );
app.post("/admin/user/:id", [validatedRequest], async (request, response) => { app.post(
try { "/admin/user/:id",
const user = await userFromSession(request, response); [validatedRequest, strictMultiUserRoleValid],
if (!user || user?.role !== "admin") { async (request, response) => {
response.sendStatus(401).end(); try {
return; 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( app.delete(
"/admin/user/:id", "/admin/user/:id",
[validatedRequest], [validatedRequest, strictMultiUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response);
if (!user || user?.role !== "admin") {
response.sendStatus(401).end();
return;
}
const { id } = request.params; const { id } = request.params;
await User.delete({ id: Number(id) }); await User.delete({ id: Number(id) });
response.status(200).json({ success: true, error: null }); response.status(200).json({ success: true, error: null });
@ -90,33 +79,26 @@ function adminEndpoints(app) {
} }
); );
app.get("/admin/invites", [validatedRequest], async (request, response) => { app.get(
try { "/admin/invites",
const user = await userFromSession(request, response); [validatedRequest, strictMultiUserRoleValid],
if (!user || user?.role !== "admin") { async (_request, response) => {
response.sendStatus(401).end(); try {
return; 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( app.get(
"/admin/invite/new", "/admin/invite/new",
[validatedRequest], [validatedRequest, strictMultiUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
if (!user || user?.role !== "admin") {
response.sendStatus(401).end();
return;
}
const { invite, error } = await Invite.create(user.id); const { invite, error } = await Invite.create(user.id);
response.status(200).json({ invite, error }); response.status(200).json({ invite, error });
} catch (e) { } catch (e) {
@ -128,15 +110,9 @@ function adminEndpoints(app) {
app.delete( app.delete(
"/admin/invite/:id", "/admin/invite/:id",
[validatedRequest], [validatedRequest, strictMultiUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response);
if (!user || user?.role !== "admin") {
response.sendStatus(401).end();
return;
}
const { id } = request.params; const { id } = request.params;
const { success, error } = await Invite.deactivate(id); const { success, error } = await Invite.deactivate(id);
response.status(200).json({ success, error }); response.status(200).json({ success, error });
@ -149,14 +125,9 @@ function adminEndpoints(app) {
app.get( app.get(
"/admin/workspaces", "/admin/workspaces",
[validatedRequest], [validatedRequest, strictMultiUserRoleValid],
async (request, response) => { async (_request, response) => {
try { try {
const user = await userFromSession(request, response);
if (!user || user?.role !== "admin") {
response.sendStatus(401).end();
return;
}
const workspaces = await Workspace.whereWithUsers(); const workspaces = await Workspace.whereWithUsers();
response.status(200).json({ workspaces }); response.status(200).json({ workspaces });
} catch (e) { } catch (e) {
@ -168,14 +139,10 @@ function adminEndpoints(app) {
app.post( app.post(
"/admin/workspaces/new", "/admin/workspaces/new",
[validatedRequest], [validatedRequest, strictMultiUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
if (!user || user?.role !== "admin") {
response.sendStatus(401).end();
return;
}
const { name } = reqBody(request); const { name } = reqBody(request);
const { workspace, message: error } = await Workspace.new( const { workspace, message: error } = await Workspace.new(
name, name,
@ -191,15 +158,9 @@ function adminEndpoints(app) {
app.post( app.post(
"/admin/workspaces/:workspaceId/update-users", "/admin/workspaces/:workspaceId/update-users",
[validatedRequest], [validatedRequest, strictMultiUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response);
if (!user || user?.role !== "admin") {
response.sendStatus(401).end();
return;
}
const { workspaceId } = request.params; const { workspaceId } = request.params;
const { userIds } = reqBody(request); const { userIds } = reqBody(request);
const { success, error } = await Workspace.updateUsers( const { success, error } = await Workspace.updateUsers(
@ -216,15 +177,9 @@ function adminEndpoints(app) {
app.delete( app.delete(
"/admin/workspaces/:id", "/admin/workspaces/:id",
[validatedRequest], [validatedRequest, strictMultiUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response);
if (!user || user?.role !== "admin") {
response.sendStatus(401).end();
return;
}
const { id } = request.params; const { id } = request.params;
const VectorDb = getVectorDbClass(); const VectorDb = getVectorDbClass();
const workspace = await Workspace.get({ id: Number(id) }); const workspace = await Workspace.get({ id: Number(id) });
@ -253,15 +208,9 @@ function adminEndpoints(app) {
app.get( app.get(
"/admin/system-preferences", "/admin/system-preferences",
[validatedRequest], [validatedRequest, strictMultiUserRoleValid],
async (request, response) => { async (_request, response) => {
try { try {
const user = await userFromSession(request, response);
if (!user || user?.role !== "admin") {
response.sendStatus(401).end();
return;
}
const settings = { const settings = {
users_can_delete_workspaces: users_can_delete_workspaces:
(await SystemSettings.get({ label: "users_can_delete_workspaces" })) (await SystemSettings.get({ label: "users_can_delete_workspaces" }))
@ -284,15 +233,9 @@ function adminEndpoints(app) {
app.post( app.post(
"/admin/system-preferences", "/admin/system-preferences",
[validatedRequest], [validatedRequest, strictMultiUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response);
if (!user || user?.role !== "admin") {
response.sendStatus(401).end();
return;
}
const updates = reqBody(request); const updates = reqBody(request);
await SystemSettings.updateSettings(updates); await SystemSettings.updateSettings(updates);
response.status(200).json({ success: true, error: null }); response.status(200).json({ success: true, error: null });
@ -303,39 +246,32 @@ function adminEndpoints(app) {
} }
); );
app.get("/admin/api-keys", [validatedRequest], async (request, response) => { app.get(
try { "/admin/api-keys",
const user = await userFromSession(request, response); [validatedRequest, strictMultiUserRoleValid],
if (!user || user?.role !== "admin") { async (_request, response) => {
response.sendStatus(401).end(); try {
return; 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( app.post(
"/admin/generate-api-key", "/admin/generate-api-key",
[validatedRequest], [validatedRequest, strictMultiUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
if (!user || user?.role !== "admin") {
response.sendStatus(401).end();
return;
}
const { apiKey, error } = await ApiKey.create(user.id); const { apiKey, error } = await ApiKey.create(user.id);
return response.status(200).json({ return response.status(200).json({
apiKey, apiKey,
@ -350,15 +286,10 @@ function adminEndpoints(app) {
app.delete( app.delete(
"/admin/delete-api-key/:id", "/admin/delete-api-key/:id",
[validatedRequest], [validatedRequest, strictMultiUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
const { id } = request.params; 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) }); await ApiKey.delete({ id: Number(id) });
return response.status(200).end(); return response.status(200).end();
} catch (e) { } catch (e) {

View File

@ -40,6 +40,7 @@ const { ApiKey } = require("../models/apiKeys");
const { getCustomModels } = require("../utils/helpers/customModels"); const { getCustomModels } = require("../utils/helpers/customModels");
const { WorkspaceChats } = require("../models/workspaceChats"); const { WorkspaceChats } = require("../models/workspaceChats");
const { Workspace } = require("../models/workspace"); const { Workspace } = require("../models/workspace");
const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected");
function systemEndpoints(app) { function systemEndpoints(app) {
if (!app) return; if (!app) return;
@ -244,20 +245,10 @@ function systemEndpoints(app) {
app.post( app.post(
"/system/update-env", "/system/update-env",
[validatedRequest], [validatedRequest, flexUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
const body = reqBody(request); 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); const { newValues, error } = updateENV(body);
if (process.env.NODE_ENV === "production") await dumpENV(); if (process.env.NODE_ENV === "production") await dumpENV();
response.status(200).json({ newValues, error }); response.status(200).json({ newValues, error });
@ -426,7 +417,7 @@ function systemEndpoints(app) {
app.post( app.post(
"/system/upload-logo", "/system/upload-logo",
[validatedRequest], [validatedRequest, flexUserRoleValid],
handleLogoUploads.single("logo"), handleLogoUploads.single("logo"),
async (request, response) => { async (request, response) => {
if (!request.file || !request.file.originalname) { if (!request.file || !request.file.originalname) {
@ -440,13 +431,6 @@ function systemEndpoints(app) {
} }
try { try {
if (
response.locals.multiUserMode &&
response.locals.user?.role !== "admin"
) {
return response.sendStatus(401).end();
}
const newFilename = await renameLogoFile(request.file.originalname); const newFilename = await renameLogoFile(request.file.originalname);
const existingLogoFilename = await SystemSettings.currentLogoFilename(); const existingLogoFilename = await SystemSettings.currentLogoFilename();
await removeCustomLogo(existingLogoFilename); await removeCustomLogo(existingLogoFilename);
@ -480,16 +464,9 @@ function systemEndpoints(app) {
app.get( app.get(
"/system/remove-logo", "/system/remove-logo",
[validatedRequest], [validatedRequest, flexUserRoleValid],
async (request, response) => { async (_request, response) => {
try { try {
if (
response.locals.multiUserMode &&
response.locals.user?.role !== "admin"
) {
return response.sendStatus(401).end();
}
const currentLogoFilename = await SystemSettings.currentLogoFilename(); const currentLogoFilename = await SystemSettings.currentLogoFilename();
await removeCustomLogo(currentLogoFilename); await removeCustomLogo(currentLogoFilename);
const { success, error } = await SystemSettings.updateSettings({ const { success, error } = await SystemSettings.updateSettings({
@ -517,7 +494,8 @@ function systemEndpoints(app) {
return response.status(200).json({ canDelete: true }); 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 }); return response.status(200).json({ canDelete: true });
} }
@ -548,16 +526,9 @@ function systemEndpoints(app) {
app.post( app.post(
"/system/set-welcome-messages", "/system/set-welcome-messages",
[validatedRequest], [validatedRequest, flexUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
if (
response.locals.multiUserMode &&
response.locals.user?.role !== "admin"
) {
return response.sendStatus(401).end();
}
const { messages = [] } = reqBody(request); const { messages = [] } = reqBody(request);
if (!Array.isArray(messages)) { if (!Array.isArray(messages)) {
return response.status(400).json({ return response.status(400).json({
@ -659,16 +630,9 @@ function systemEndpoints(app) {
app.post( app.post(
"/system/workspace-chats", "/system/workspace-chats",
[validatedRequest], [validatedRequest, flexUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
if (
response.locals.multiUserMode &&
response.locals.user?.role !== "admin"
) {
return response.sendStatus(401).end();
}
const { offset = 0, limit = 20 } = reqBody(request); const { offset = 0, limit = 20 } = reqBody(request);
const chats = await WorkspaceChats.whereWithData( const chats = await WorkspaceChats.whereWithData(
{}, {},
@ -689,16 +653,9 @@ function systemEndpoints(app) {
app.delete( app.delete(
"/system/workspace-chats/:id", "/system/workspace-chats/:id",
[validatedRequest], [validatedRequest, flexUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
if (
response.locals.multiUserMode &&
response.locals.user?.role !== "admin"
) {
return response.sendStatus(401).end();
}
const { id } = request.params; const { id } = request.params;
await WorkspaceChats.delete({ id: Number(id) }); await WorkspaceChats.delete({ id: Number(id) });
response.status(200).json({ success, error }); response.status(200).json({ success, error });
@ -711,16 +668,9 @@ function systemEndpoints(app) {
app.get( app.get(
"/system/export-chats", "/system/export-chats",
[validatedRequest], [validatedRequest, flexUserRoleValid],
async (request, response) => { async (_request, response) => {
try { try {
if (
response.locals.multiUserMode &&
response.locals.user?.role !== "admin"
) {
return response.sendStatus(401).end();
}
const chats = await WorkspaceChats.whereWithData({}, null, null, { const chats = await WorkspaceChats.whereWithData({}, null, null, {
id: "asc", id: "asc",
}); });

View File

@ -11,36 +11,40 @@ const {
processDocument, processDocument,
} = require("../utils/files/documentProcessor"); } = require("../utils/files/documentProcessor");
const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { validatedRequest } = require("../utils/middleware/validatedRequest");
const { SystemSettings } = require("../models/systemSettings");
const { Telemetry } = require("../models/telemetry"); const { Telemetry } = require("../models/telemetry");
const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected");
const { handleUploads } = setupMulter(); const { handleUploads } = setupMulter();
function workspaceEndpoints(app) { function workspaceEndpoints(app) {
if (!app) return; if (!app) return;
app.post("/workspace/new", [validatedRequest], async (request, response) => { app.post(
try { "/workspace/new",
const user = await userFromSession(request, response); [validatedRequest, flexUserRoleValid],
const { name = null, onboardingComplete = false } = reqBody(request); async (request, response) => {
const { workspace, message } = await Workspace.new(name, user?.id); try {
await Telemetry.sendTelemetry( const user = await userFromSession(request, response);
"workspace_created", const { name = null, onboardingComplete = false } = reqBody(request);
{ const { workspace, message } = await Workspace.new(name, user?.id);
multiUserMode: multiUserMode(response), await Telemetry.sendTelemetry(
LLMSelection: process.env.LLM_PROVIDER || "openai", "workspace_created",
VectorDbSelection: process.env.VECTOR_DB || "pinecone", {
}, multiUserMode: multiUserMode(response),
user?.id LLMSelection: process.env.LLM_PROVIDER || "openai",
); VectorDbSelection: process.env.VECTOR_DB || "pinecone",
if (onboardingComplete === true) },
await Telemetry.sendTelemetry("onboarding_complete"); user?.id
);
if (onboardingComplete === true)
await Telemetry.sendTelemetry("onboarding_complete");
response.status(200).json({ workspace, message }); response.status(200).json({ workspace, message });
} catch (e) { } catch (e) {
console.log(e.message, e); console.log(e.message, e);
response.sendStatus(500).end(); response.sendStatus(500).end();
}
} }
}); );
app.post( app.post(
"/workspace/:slug/update", "/workspace/:slug/update",
@ -142,7 +146,7 @@ function workspaceEndpoints(app) {
app.delete( app.delete(
"/workspace/:slug", "/workspace/:slug",
[validatedRequest], [validatedRequest, flexUserRoleValid],
async (request, response) => { async (request, response) => {
try { try {
const { slug = "" } = request.params; const { slug = "" } = request.params;
@ -157,16 +161,6 @@ function workspaceEndpoints(app) {
return; 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 WorkspaceChats.delete({ workspaceId: Number(workspace.id) });
await DocumentVectors.deleteForWorkspace(workspace.id); await DocumentVectors.deleteForWorkspace(workspace.id);
await Document.delete({ workspaceId: Number(workspace.id) }); await Document.delete({ workspaceId: Number(workspace.id) });

View File

@ -64,7 +64,7 @@ const Workspace = {
}, },
getWithUser: async function (user = null, clause = {}) { 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 { try {
const workspace = await prisma.workspaces.findFirst({ const workspace = await prisma.workspaces.findFirst({
@ -142,7 +142,8 @@ const Workspace = {
limit = null, limit = null,
orderBy = 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 { try {
const workspaces = await prisma.workspaces.findMany({ const workspaces = await prisma.workspaces.findMany({

View File

@ -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,
};

View File

@ -20,7 +20,7 @@ async function validatedRequest(request, response, next) {
} }
if (!process.env.AUTH_TOKEN) { if (!process.env.AUTH_TOKEN) {
response.status(403).json({ response.status(401).json({
error: "You need to set an AUTH_TOKEN environment variable.", error: "You need to set an AUTH_TOKEN environment variable.",
}); });
return; return;
@ -30,7 +30,7 @@ async function validatedRequest(request, response, next) {
const token = auth ? auth.split(" ")[1] : null; const token = auth ? auth.split(" ")[1] : null;
if (!token) { if (!token) {
response.status(403).json({ response.status(401).json({
error: "No auth token found.", error: "No auth token found.",
}); });
return; return;
@ -38,7 +38,7 @@ async function validatedRequest(request, response, next) {
const { p } = decodeJWT(token); const { p } = decodeJWT(token);
if (p !== process.env.AUTH_TOKEN) { if (p !== process.env.AUTH_TOKEN) {
response.status(403).json({ response.status(401).json({
error: "Invalid auth token found.", error: "Invalid auth token found.",
}); });
return; return;
@ -52,7 +52,7 @@ async function validateMultiUserRequest(request, response, next) {
const token = auth ? auth.split(" ")[1] : null; const token = auth ? auth.split(" ")[1] : null;
if (!token) { if (!token) {
response.status(403).json({ response.status(401).json({
error: "No auth token found.", error: "No auth token found.",
}); });
return; return;
@ -60,7 +60,7 @@ async function validateMultiUserRequest(request, response, next) {
const valid = decodeJWT(token); const valid = decodeJWT(token);
if (!valid || !valid.id) { if (!valid || !valid.id) {
response.status(403).json({ response.status(401).json({
error: "Invalid auth token.", error: "Invalid auth token.",
}); });
return; return;
@ -68,12 +68,19 @@ async function validateMultiUserRequest(request, response, next) {
const user = await User.get({ id: valid.id }); const user = await User.get({ id: valid.id });
if (!user) { if (!user) {
response.status(403).json({ response.status(401).json({
error: "Invalid auth for user.", error: "Invalid auth for user.",
}); });
return; return;
} }
if (user.suspended) {
response.status(401).json({
error: "User is suspended from system",
});
return;
}
response.locals.user = user; response.locals.user = user;
next(); next();
} }