mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-04 22:10:12 +01:00
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:
parent
7fcf29d769
commit
fa29003a46
@ -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="/settings/export-import"
|
||||
element={<ManagerRoute Component={GeneralExportImport} />}
|
||||
/>
|
||||
<Route
|
||||
path="/general/export-import"
|
||||
element={<PrivateRoute Component={GeneralExportImport} />}
|
||||
path="/settings/security"
|
||||
element={<ManagerRoute Component={GeneralSecurity} />}
|
||||
/>
|
||||
<Route
|
||||
path="/general/security"
|
||||
element={<PrivateRoute Component={GeneralSecurity} />}
|
||||
path="/settings/appearance"
|
||||
element={<ManagerRoute Component={GeneralAppearance} />}
|
||||
/>
|
||||
<Route
|
||||
path="/general/appearance"
|
||||
element={<PrivateRoute Component={GeneralAppearance} />}
|
||||
path="/settings/api-keys"
|
||||
element={<ManagerRoute Component={GeneralApiKeys} />}
|
||||
/>
|
||||
<Route
|
||||
path="/general/api-keys"
|
||||
element={<PrivateRoute Component={GeneralApiKeys} />}
|
||||
path="/settings/workspace-chats"
|
||||
element={<ManagerRoute Component={GeneralChats} />}
|
||||
/>
|
||||
<Route
|
||||
path="/general/workspace-chats"
|
||||
element={<PrivateRoute 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 />} />
|
||||
|
@ -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 →
|
||||
|
@ -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 →
|
||||
|
@ -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 = () => {
|
||||
|
@ -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,14 @@ 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) {
|
||||
@ -89,7 +95,28 @@ export function AdminRoute({ Component }) {
|
||||
}
|
||||
|
||||
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>
|
||||
<Component />
|
||||
</UserMenu>
|
||||
|
@ -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" />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* General Settings */}
|
||||
<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"
|
||||
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" />}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -39,47 +39,42 @@ export default {
|
||||
apiDocs: () => {
|
||||
return `${API_BASE}/docs`;
|
||||
},
|
||||
general: {
|
||||
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: {
|
||||
settings: {
|
||||
system: () => {
|
||||
return `/admin/system-preferences`;
|
||||
return `/settings/system-preferences`;
|
||||
},
|
||||
users: () => {
|
||||
return `/admin/users`;
|
||||
return `/settings/users`;
|
||||
},
|
||||
invites: () => {
|
||||
return `/admin/invites`;
|
||||
return `/settings/invites`;
|
||||
},
|
||||
workspaces: () => {
|
||||
return `/admin/workspaces`;
|
||||
return `/settings/workspaces`;
|
||||
},
|
||||
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";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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",
|
||||
});
|
||||
|
@ -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) });
|
||||
|
@ -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({
|
||||
|
41
server/utils/middleware/multiUserProtected.js
Normal file
41
server/utils/middleware/multiUserProtected.js
Normal 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,
|
||||
};
|
@ -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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user