diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7acd2c5347994c124effd2a9ee0a0f01023925c1..058505d204631d3d26225c455c44957a75de820e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -81,7 +81,7 @@ export default function App() { /> <Route path="/settings/api-keys" - element={<ManagerRoute Component={GeneralApiKeys} />} + element={<AdminRoute Component={GeneralApiKeys} />} /> <Route path="/settings/workspace-chats" diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx index 99cf1eb35df99b92a041c02e99c3e7124fe6d789..851ea5d57fab4c62c8b005ccf68196ec837613a9 100644 --- a/frontend/src/components/SettingsSidebar/index.jsx +++ b/frontend/src/components/SettingsSidebar/index.jsx @@ -62,79 +62,97 @@ 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/manager Multi-user Settings */} - {!!user && user?.role !== "default" && ( - <> - <Option - href={paths.settings.system()} - btnText="System Preferences" - icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />} - /> - <Option - href={paths.settings.invites()} - btnText="Invitation" - icon={ - <EnvelopeSimple className="h-5 w-5 flex-shrink-0" /> - } - /> - <Option - href={paths.settings.users()} - btnText="Users" - icon={<Users className="h-5 w-5 flex-shrink-0" />} - /> - <Option - href={paths.settings.workspaces()} - btnText="Workspaces" - icon={<BookOpen className="h-5 w-5 flex-shrink-0" />} - /> - </> - )} - + <Option + href={paths.settings.system()} + btnText="System Preferences" + icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />} + user={user} + allowedRole={["admin", "manager"]} + /> + <Option + href={paths.settings.invites()} + btnText="Invitation" + icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />} + user={user} + allowedRole={["admin", "manager"]} + /> + <Option + href={paths.settings.users()} + btnText="Users" + icon={<Users className="h-5 w-5 flex-shrink-0" />} + user={user} + allowedRole={["admin", "manager"]} + /> + <Option + href={paths.settings.workspaces()} + btnText="Workspaces" + icon={<BookOpen className="h-5 w-5 flex-shrink-0" />} + user={user} + allowedRole={["admin", "manager"]} + /> <Option href={paths.settings.chats()} btnText="Workspace Chat" icon={<ChatCenteredText className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin", "manager"]} /> - <Option href={paths.settings.appearance()} btnText="Appearance" icon={<Eye className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin", "manager"]} /> <Option href={paths.settings.apiKeys()} btnText="API Keys" icon={<Key className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin"]} + /> + <Option + href={paths.settings.llmPreference()} + btnText="LLM Preference" + icon={<ChatText className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin"]} + /> + <Option + href={paths.settings.embeddingPreference()} + btnText="Embedding Preference" + icon={<FileCode className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin"]} + /> + <Option + href={paths.settings.vectorDatabase()} + btnText="Vector Database" + icon={<Database className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin"]} + /> + <Option + href={paths.settings.dataConnectors.list()} + btnText="Data Connectors" + icon={<Plugs className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin", "manager"]} /> - - {(!user || user?.role === "admin") && ( - <> - <Option - href={paths.settings.llmPreference()} - btnText="LLM Preference" - icon={<ChatText className="h-5 w-5 flex-shrink-0" />} - /> - <Option - href={paths.settings.embeddingPreference()} - btnText="Embedding Preference" - icon={<FileCode className="h-5 w-5 flex-shrink-0" />} - /> - <Option - href={paths.settings.vectorDatabase()} - btnText="Vector Database" - icon={<Database className="h-5 w-5 flex-shrink-0" />} - /> - <Option - href={paths.settings.dataConnectors.list()} - btnText="Data Connectors" - icon={<Plugs className="h-5 w-5 flex-shrink-0" />} - /> - </> - )} <Option href={paths.settings.security()} btnText="Security" icon={<Lock className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin", "manager"]} /> </div> </div> @@ -265,63 +283,95 @@ export function SidebarMobileHeader() { href={paths.settings.system()} btnText="System Preferences" icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />} + user={user} + allowedRole={["admin", "manager"]} /> <Option href={paths.settings.invites()} btnText="Invitation" icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />} + user={user} + allowedRole={["admin", "manager"]} /> <Option href={paths.settings.users()} btnText="Users" icon={<Users className="h-5 w-5 flex-shrink-0" />} + user={user} + allowedRole={["admin", "manager"]} /> <Option href={paths.settings.workspaces()} btnText="Workspaces" icon={<BookOpen className="h-5 w-5 flex-shrink-0" />} + user={user} + allowedRole={["admin", "manager"]} /> - <Option href={paths.settings.chats()} btnText="Workspace Chat" icon={ <ChatCenteredText className="h-5 w-5 flex-shrink-0" /> } + user={user} + flex={true} + allowedRole={["admin", "manager"]} /> <Option href={paths.settings.appearance()} btnText="Appearance" icon={<Eye className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin", "manager"]} /> <Option href={paths.settings.apiKeys()} btnText="API Keys" icon={<Key className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin"]} + /> + <Option + href={paths.settings.llmPreference()} + btnText="LLM Preference" + icon={<ChatText className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin"]} + /> + <Option + href={paths.settings.embeddingPreference()} + btnText="Embedding Preference" + icon={<FileCode className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin"]} + /> + <Option + href={paths.settings.vectorDatabase()} + btnText="Vector Database" + icon={<Database className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin"]} + /> + <Option + href={paths.settings.dataConnectors.list()} + btnText="Data Connectors" + icon={<Plugs className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin", "manager"]} /> - {(!user || user?.role === "admin") && ( - <> - <Option - href={paths.settings.llmPreference()} - btnText="LLM Preference" - icon={<ChatText className="h-5 w-5 flex-shrink-0" />} - /> - <Option - href={paths.settings.embeddingPreference()} - btnText="Embedding Preference" - icon={<FileCode className="h-5 w-5 flex-shrink-0" />} - /> - <Option - href={paths.settings.vectorDatabase()} - btnText="Vector Database" - icon={<Database className="h-5 w-5 flex-shrink-0" />} - /> - </> - )} <Option href={paths.settings.security()} btnText="Security" icon={<Lock className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin", "manager"]} /> </div> </div> @@ -364,8 +414,21 @@ export function SidebarMobileHeader() { ); } -const Option = ({ btnText, icon, href }) => { +const Option = ({ + btnText, + icon, + href, + flex = false, + user = null, + allowedRole = [], +}) => { const isActive = window.location.pathname === href; + + // Option only for multi-user + if (!flex && !allowedRole.includes(user?.role)) return null; + + // Option is dual-mode, but user exists, we need to check permissions + if (flex && !!user && !allowedRole.includes(user?.role)) return null; return ( <div className="flex gap-x-2 items-center justify-between text-white"> <a diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index e504fcb26d3477199a198075e5383e8af7bd51bb..596348ede8df14ff6c2a32d44f494ea84183b68f 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -209,6 +209,7 @@ const System = { return await fetch(`${API_BASE}/system/pfp/${id}`, { method: "GET", cache: "no-cache", + headers: baseHeaders(), }) .then((res) => { if (res.ok && res.status !== 204) return res.blob(); @@ -283,6 +284,7 @@ const System = { return await fetch(`${API_BASE}/system/welcome-messages`, { method: "GET", cache: "no-cache", + headers: baseHeaders(), }) .then((res) => { if (!res.ok) throw new Error("Could not fetch welcome messages."); diff --git a/frontend/src/pages/Admin/Users/UserRow/index.jsx b/frontend/src/pages/Admin/Users/UserRow/index.jsx index d734759fc673b8db9e42c1ffff2b65b1c94043a9..cd1c4773274c089821604f4698cba0a4eb8192e9 100644 --- a/frontend/src/pages/Admin/Users/UserRow/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/index.jsx @@ -3,9 +3,17 @@ import { titleCase } from "text-case"; import Admin from "@/models/admin"; import EditUserModal, { EditUserModalId } from "./EditUserModal"; import { DotsThreeOutline } from "@phosphor-icons/react"; +import showToast from "@/utils/toast"; + +const ModMap = { + admin: ["admin", "manager", "default"], + manager: ["manager", "default"], + default: [], +}; export default function UserRow({ currUser, user }) { const rowRef = useRef(null); + const canModify = ModMap[currUser?.role || "default"].includes(user.role); const [suspended, setSuspended] = useState(user.suspended === 1); const handleSuspend = async () => { if ( @@ -14,8 +22,19 @@ export default function UserRow({ currUser, user }) { ) ) return false; - setSuspended(!suspended); - await Admin.updateUser(user.id, { suspended: suspended ? 0 : 1 }); + + const { success, error } = await Admin.updateUser(user.id, { + suspended: suspended ? 0 : 1, + }); + if (!success) showToast(error, "error", { clear: true }); + if (success) { + showToast( + `User ${!suspended ? "has been suspended" : "is no longer suspended"}.`, + "success", + { clear: true } + ); + setSuspended(!suspended); + } }; const handleDelete = async () => { if ( @@ -24,8 +43,12 @@ export default function UserRow({ currUser, user }) { ) ) return false; - rowRef?.current?.remove(); - await Admin.deleteUser(user.id); + const { success, error } = await Admin.deleteUser(user.id); + if (!success) showToast(error, "error", { clear: true }); + if (success) { + rowRef?.current?.remove(); + showToast("User deleted from system.", "success", { clear: true }); + } }; return ( @@ -40,7 +63,7 @@ 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"> - {currUser?.role !== "default" && ( + {canModify && ( <button onClick={() => document?.getElementById(EditUserModalId(user))?.showModal() @@ -50,7 +73,7 @@ export default function UserRow({ currUser, user }) { <DotsThreeOutline weight="fill" className="h-5 w-5" /> </button> )} - {currUser?.id !== user.id && currUser?.role !== "default" && ( + {currUser?.id !== user.id && canModify && ( <> <button onClick={handleSuspend} diff --git a/frontend/src/pages/Admin/Users/index.jsx b/frontend/src/pages/Admin/Users/index.jsx index b59bbe5761b76f94b35fbc9308e47fe38cec4aa4..fb8f50080276d11ce613c367609282159a20b62a 100644 --- a/frontend/src/pages/Admin/Users/index.jsx +++ b/frontend/src/pages/Admin/Users/index.jsx @@ -105,7 +105,8 @@ const ROLE_HINT = { "Cannot modify any settings at all.", ], manager: [ - "Can view all workspaces and modify all settings.", + "Can view, create, and delete any workspaces and modify workspace-specific settings.", + "Can create, update and invite new users to the instance.", "Cannot modify LLM, vectorDB, embedding, or other connections.", ], admin: [ diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index b32de3b3e17984938171954082638a1ac2bffded..b107a11b424e7f26da394fd491727621fc1956d5 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -7,9 +7,15 @@ const { DocumentVectors } = require("../models/vectors"); const { Workspace } = require("../models/workspace"); const { WorkspaceChats } = require("../models/workspaceChats"); const { getVectorDbClass } = require("../utils/helpers"); +const { + validRoleSelection, + canModifyAdmin, + validCanModify, +} = require("../utils/helpers/admin"); const { reqBody, userFromSession } = require("../utils/http"); const { strictMultiUserRoleValid, + ROLES, } = require("../utils/middleware/multiUserProtected"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); @@ -18,7 +24,7 @@ function adminEndpoints(app) { app.get( "/admin/users", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (_request, response) => { try { const users = (await User.where()).map((user) => { @@ -35,10 +41,20 @@ function adminEndpoints(app) { app.post( "/admin/users/new", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { + const currUser = await userFromSession(request, response); const newUserParams = reqBody(request); + const roleValidation = validRoleSelection(currUser, newUserParams); + + if (!roleValidation.valid) { + response + .status(200) + .json({ user: null, error: roleValidation.error }); + return; + } + const { user: newUser, error } = await User.create(newUserParams); response.status(200).json({ user: newUser, error }); } catch (e) { @@ -50,29 +66,34 @@ function adminEndpoints(app) { app.post( "/admin/user/:id", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { + const currUser = await userFromSession(request, response); const { id } = request.params; const updates = reqBody(request); const user = await User.get({ id: Number(id) }); - // Check to make sure with this update that includes a role change to - // something other than admin that we still have at least one admin left. - if ( - updates.hasOwnProperty("role") && // has admin prop to change - updates.role !== "admin" && // and we are changing to non-admin - user.role === "admin" // and they currently are an admin - ) { - const adminCount = await User.count({ role: "admin" }); - if (adminCount - 1 <= 0) { - response.status(200).json({ - success: false, - error: - "No system admins will remain if you do this. Update failed.", - }); - return; - } + const canModify = validCanModify(currUser, user); + if (!canModify.valid) { + response.status(200).json({ success: false, error: canModify.error }); + return; + } + + const roleValidation = validRoleSelection(currUser, updates); + if (!roleValidation.valid) { + response + .status(200) + .json({ success: false, error: roleValidation.error }); + return; + } + + const validAdminRoleModification = await canModifyAdmin(user, updates); + if (!validAdminRoleModification.valid) { + response + .status(200) + .json({ success: false, error: validAdminRoleModification.error }); + return; } const { success, error } = await User.update(id, updates); @@ -86,10 +107,19 @@ function adminEndpoints(app) { app.delete( "/admin/user/:id", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { + const currUser = await userFromSession(request, response); const { id } = request.params; + const user = await User.get({ id: Number(id) }); + + const canModify = validCanModify(currUser, user); + if (!canModify.valid) { + response.status(200).json({ success: false, error: canModify.error }); + return; + } + await User.delete({ id: Number(id) }); response.status(200).json({ success: true, error: null }); } catch (e) { @@ -101,7 +131,7 @@ function adminEndpoints(app) { app.get( "/admin/invites", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (_request, response) => { try { const invites = await Invite.whereWithUsers(); @@ -115,7 +145,7 @@ function adminEndpoints(app) { app.get( "/admin/invite/new", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -130,7 +160,7 @@ function adminEndpoints(app) { app.delete( "/admin/invite/:id", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const { id } = request.params; @@ -145,7 +175,7 @@ function adminEndpoints(app) { app.get( "/admin/workspaces", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (_request, response) => { try { const workspaces = await Workspace.whereWithUsers(); @@ -159,7 +189,7 @@ function adminEndpoints(app) { app.post( "/admin/workspaces/new", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -178,7 +208,7 @@ function adminEndpoints(app) { app.post( "/admin/workspaces/:workspaceId/update-users", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const { workspaceId } = request.params; @@ -197,7 +227,7 @@ function adminEndpoints(app) { app.delete( "/admin/workspaces/:id", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const { id } = request.params; @@ -228,7 +258,7 @@ function adminEndpoints(app) { app.get( "/admin/system-preferences", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (_request, response) => { try { const settings = { @@ -253,7 +283,7 @@ function adminEndpoints(app) { app.post( "/admin/system-preferences", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const updates = reqBody(request); @@ -268,7 +298,7 @@ function adminEndpoints(app) { app.get( "/admin/api-keys", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin])], async (_request, response) => { try { const apiKeys = await ApiKey.whereWithUser({}); @@ -288,7 +318,7 @@ function adminEndpoints(app) { app.post( "/admin/generate-api-key", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -306,7 +336,7 @@ function adminEndpoints(app) { app.delete( "/admin/delete-api-key/:id", - [validatedRequest, strictMultiUserRoleValid], + [validatedRequest, strictMultiUserRoleValid([ROLES.admin])], async (request, response) => { try { const { id } = request.params; diff --git a/server/endpoints/api/admin/index.js b/server/endpoints/api/admin/index.js index ebe662c696ba088a1bf3c79d28f4308cbff5b730..1f2a5bae74cbb4bf60591e5d5529508244acaa65 100644 --- a/server/endpoints/api/admin/index.js +++ b/server/endpoints/api/admin/index.js @@ -3,6 +3,7 @@ const { SystemSettings } = require("../../../models/systemSettings"); const { User } = require("../../../models/user"); const { Workspace } = require("../../../models/workspace"); const { WorkspaceChats } = require("../../../models/workspaceChats"); +const { canModifyAdmin } = require("../../../utils/helpers/admin"); const { multiUserMode, reqBody } = require("../../../utils/http"); const { validApiKey } = require("../../../utils/middleware/validApiKey"); @@ -198,23 +199,13 @@ function apiAdminEndpoints(app) { const { id } = request.params; const updates = reqBody(request); const user = await User.get({ id: Number(id) }); + const validAdminRoleModification = await canModifyAdmin(user, updates); - // Check to make sure with this update that includes a role change to - // something other than admin that we still have at least one admin left. - if ( - updates.hasOwnProperty("role") && // has admin prop to change - updates.role !== "admin" && // and we are changing to non-admin - user.role === "admin" // and they currently are an admin - ) { - const adminCount = await User.count({ role: "admin" }); - if (adminCount - 1 <= 0) { - response.status(200).json({ - success: false, - error: - "No system admins will remain if you do this. Update failed.", - }); - return; - } + if (!validAdminRoleModification.valid) { + response + .status(200) + .json({ success: false, error: validAdminRoleModification.error }); + return; } const { success, error } = await User.update(id, updates); diff --git a/server/endpoints/chat.js b/server/endpoints/chat.js index adfec0ec3f5cd1fabc603d45d72f2c2677d58ff7..23739084a7a2d3c1dbb82cd1b5b8bd845648b8d6 100644 --- a/server/endpoints/chat.js +++ b/server/endpoints/chat.js @@ -10,13 +10,17 @@ const { writeResponseChunk, VALID_CHAT_MODE, } = require("../utils/chats/stream"); +const { + ROLES, + flexUserRoleValid, +} = require("../utils/middleware/multiUserProtected"); function chatEndpoints(app) { if (!app) return; app.post( "/workspace/:slug/stream-chat", - [validatedRequest], + [validatedRequest, flexUserRoleValid([ROLES.all])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -52,7 +56,7 @@ function chatEndpoints(app) { response.setHeader("Connection", "keep-alive"); response.flushHeaders(); - if (multiUserMode(response) && user.role !== "admin") { + if (multiUserMode(response) && user.role !== ROLES.admin) { const limitMessagesSetting = await SystemSettings.get({ label: "limit_user_messages", }); diff --git a/server/endpoints/extensions/index.js b/server/endpoints/extensions/index.js index 1b3770374b02edf1acb49d6dea54572d8ed98025..a2c884a01bc33cdc6755ded57450fdfa8c6840bf 100644 --- a/server/endpoints/extensions/index.js +++ b/server/endpoints/extensions/index.js @@ -4,6 +4,7 @@ const { } = require("../../utils/files/documentProcessor"); const { flexUserRoleValid, + ROLES, } = require("../../utils/middleware/multiUserProtected"); const { validatedRequest } = require("../../utils/middleware/validatedRequest"); @@ -12,7 +13,7 @@ function extensionEndpoints(app) { app.post( "/ext/github/branches", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const responseFromProcessor = await forwardExtensionRequest({ @@ -30,7 +31,7 @@ function extensionEndpoints(app) { app.post( "/ext/github/repo", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const responseFromProcessor = await forwardExtensionRequest({ @@ -51,7 +52,7 @@ function extensionEndpoints(app) { app.post( "/ext/youtube/transcript", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const responseFromProcessor = await forwardExtensionRequest({ diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 3f11bf5e011a8e52757b3c408b8590b8d440fed6..14aa22e043c60bd11593e720921a3e0aac1aea0c 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -39,10 +39,15 @@ const { WelcomeMessages } = require("../models/welcomeMessages"); const { ApiKey } = require("../models/apiKeys"); const { getCustomModels } = require("../utils/helpers/customModels"); const { WorkspaceChats } = require("../models/workspaceChats"); -const { Workspace } = require("../models/workspace"); -const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected"); +const { + flexUserRoleValid, + ROLES, +} = require("../utils/middleware/multiUserProtected"); const { fetchPfp, determinePfpFilepath } = require("../utils/files/pfp"); -const { convertToCSV, convertToJSON, convertToJSONL } = require("./utils"); +const { + prepareWorkspaceChatsForExport, + exportChatsAsType, +} = require("../utils/helpers/chat/convertTo"); function systemEndpoints(app) { if (!app) return; @@ -275,15 +280,9 @@ function systemEndpoints(app) { app.post( "/system/update-env", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.admin])], async (request, response) => { try { - const user = await userFromSession(request, response); - if (!!user && user.role !== "admin") { - response.sendStatus(401).end(); - return; - } - const body = reqBody(request); const { newValues, error } = await updateENV(body); if (process.env.NODE_ENV === "production") await dumpENV(); @@ -341,7 +340,7 @@ function systemEndpoints(app) { const { user, error } = await User.create({ username, password, - role: "admin", + role: ROLES.admin, }); await SystemSettings.updateSettings({ multi_user_mode: true, @@ -374,7 +373,7 @@ function systemEndpoints(app) { } ); - app.get("/system/multi-user-mode", async (request, response) => { + app.get("/system/multi-user-mode", async (_, response) => { try { const multiUserMode = await SystemSettings.isMultiUserMode(); response.status(200).json({ multiUserMode }); @@ -384,7 +383,7 @@ function systemEndpoints(app) { } }); - app.get("/system/logo", async function (request, response) { + app.get("/system/logo", async function (_, response) { try { const defaultFilename = getDefaultFilename(); const logoPath = await determineLogoFilepath(defaultFilename); @@ -409,56 +408,61 @@ function systemEndpoints(app) { } }); - app.get("/system/pfp/:id", async function (request, response) { - try { - const { id } = request.params; - const pfpPath = await determinePfpFilepath(id); + app.get( + "/system/pfp/:id", + [validatedRequest, flexUserRoleValid([ROLES.all])], + async function (request, response) { + try { + const { id } = request.params; + const pfpPath = await determinePfpFilepath(id); - if (!pfpPath) { - response.sendStatus(204).end(); - return; - } + if (!pfpPath) { + response.sendStatus(204).end(); + return; + } - const { found, buffer, size, mime } = fetchPfp(pfpPath); - if (!found) { - response.sendStatus(204).end(); + const { found, buffer, size, mime } = fetchPfp(pfpPath); + if (!found) { + response.sendStatus(204).end(); + return; + } + + response.writeHead(200, { + "Content-Type": mime || "image/png", + "Content-Disposition": `attachment; filename=${path.basename( + pfpPath + )}`, + "Content-Length": size, + }); + response.end(Buffer.from(buffer, "base64")); return; + } catch (error) { + console.error("Error processing the logo request:", error); + response.status(500).json({ message: "Internal server error" }); } - - response.writeHead(200, { - "Content-Type": mime || "image/png", - "Content-Disposition": `attachment; filename=${path.basename(pfpPath)}`, - "Content-Length": size, - }); - response.end(Buffer.from(buffer, "base64")); - return; - } catch (error) { - console.error("Error processing the logo request:", error); - response.status(500).json({ message: "Internal server error" }); } - }); + ); app.post( "/system/upload-pfp", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.all])], handlePfpUploads.single("file"), async function (request, response) { try { const user = await userFromSession(request, response); const uploadedFileName = request.randomFileName; - if (!uploadedFileName) { return response.status(400).json({ message: "File upload failed." }); } const userRecord = await User.get({ id: user.id }); - const oldPfpFilename = normalizePath(userRecord.pfpFilename); + const oldPfpFilename = userRecord.pfpFilename; console.log("oldPfpFilename", oldPfpFilename); if (oldPfpFilename) { const oldPfpPath = path.join( __dirname, - `../storage/assets/pfp/${oldPfpFilename}` + `../storage/assets/pfp/${normalizePath(userRecord.pfpFilename)}` ); if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath); @@ -482,17 +486,18 @@ function systemEndpoints(app) { app.delete( "/system/remove-pfp", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.all])], async function (request, response) { try { const user = await userFromSession(request, response); const userRecord = await User.get({ id: user.id }); - const oldPfpFilename = normalizePath(userRecord.pfpFilename); + const oldPfpFilename = userRecord.pfpFilename; + console.log("oldPfpFilename", oldPfpFilename); if (oldPfpFilename) { const oldPfpPath = path.join( __dirname, - `../storage/assets/pfp/${oldPfpFilename}` + `../storage/assets/pfp/${normalizePath(oldPfpFilename)}` ); if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath); @@ -516,7 +521,7 @@ function systemEndpoints(app) { app.post( "/system/upload-logo", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], handleLogoUploads.single("logo"), async (request, response) => { if (!request.file || !request.file.originalname) { @@ -550,7 +555,7 @@ function systemEndpoints(app) { } ); - app.get("/system/is-default-logo", async (request, response) => { + app.get("/system/is-default-logo", async (_, response) => { try { const currentLogoFilename = await SystemSettings.currentLogoFilename(); const isDefaultLogo = currentLogoFilename === LOGO_FILENAME; @@ -563,7 +568,7 @@ function systemEndpoints(app) { app.get( "/system/remove-logo", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (_request, response) => { try { const currentLogoFilename = await SystemSettings.currentLogoFilename(); @@ -594,7 +599,7 @@ function systemEndpoints(app) { } const user = await userFromSession(request, response); - if (["admin", "manager"].includes(user?.role)) { + if ([ROLES.admin, ROLES.manager].includes(user?.role)) { return response.status(200).json({ canDelete: true }); } @@ -611,21 +616,25 @@ function systemEndpoints(app) { } ); - app.get("/system/welcome-messages", async function (request, response) { - try { - const welcomeMessages = await WelcomeMessages.getMessages(); - response.status(200).json({ success: true, welcomeMessages }); - } catch (error) { - console.error("Error fetching welcome messages:", error); - response - .status(500) - .json({ success: false, message: "Internal server error" }); + app.get( + "/system/welcome-messages", + [validatedRequest, flexUserRoleValid([ROLES.all])], + async function (_, response) { + try { + const welcomeMessages = await WelcomeMessages.getMessages(); + response.status(200).json({ success: true, welcomeMessages }); + } catch (error) { + console.error("Error fetching welcome messages:", error); + response + .status(500) + .json({ success: false, message: "Internal server error" }); + } } - }); + ); app.post( "/system/set-welcome-messages", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const { messages = [] } = reqBody(request); @@ -733,7 +742,7 @@ function systemEndpoints(app) { app.post( "/system/workspace-chats", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const { offset = 0, limit = 20 } = reqBody(request); @@ -756,7 +765,7 @@ function systemEndpoints(app) { app.delete( "/system/workspace-chats/:id", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const { id } = request.params; @@ -771,81 +780,14 @@ function systemEndpoints(app) { app.get( "/system/export-chats", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.manager, ROLES.admin])], async (request, response) => { try { const { type = "jsonl" } = request.query; - const chats = await WorkspaceChats.whereWithData({}, null, null, { - id: "asc", - }); - const workspaceIds = [ - ...new Set(chats.map((chat) => chat.workspaceId)), - ]; - - const workspacesWithPrompts = await Promise.all( - workspaceIds.map((id) => Workspace.get({ id: Number(id) })) - ); - - const workspacePromptsMap = workspacesWithPrompts.reduce( - (acc, workspace) => { - acc[workspace.id] = workspace.openAiPrompt; - return acc; - }, - {} - ); - - const workspaceChatsMap = chats.reduce((acc, chat) => { - const { prompt, response, workspaceId } = chat; - const responseJson = JSON.parse(response); - - if (!acc[workspaceId]) { - acc[workspaceId] = { - messages: [ - { - role: "system", - content: - workspacePromptsMap[workspaceId] || - "Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.", - }, - ], - }; - } - - acc[workspaceId].messages.push( - { - role: "user", - content: prompt, - }, - { - role: "assistant", - content: responseJson.text, - } - ); - - return acc; - }, {}); - - let output; - switch (type.toLowerCase()) { - case "json": { - response.setHeader("Content-Type", "application/json"); - output = await convertToJSON(workspaceChatsMap); - break; - } - case "csv": { - response.setHeader("Content-Type", "text/csv"); - output = await convertToCSV(workspaceChatsMap); - break; - } - // JSONL default - default: { - response.setHeader("Content-Type", "application/jsonl"); - output = await convertToJSONL(workspaceChatsMap); - break; - } - } - - response.status(200).send(output); + const chats = await prepareWorkspaceChatsForExport(); + const { contentType, data } = await exportChatsAsType(chats, type); + response.setHeader("Content-Type", contentType); + response.status(200).send(data); } catch (e) { console.error(e); response.sendStatus(500).end(); @@ -853,6 +795,8 @@ function systemEndpoints(app) { } ); + // Used for when a user in multi-user updates their own profile + // from the UI. app.post("/system/user", [validatedRequest], async (request, response) => { try { const sessionUser = await userFromSession(request, response); diff --git a/server/endpoints/utils.js b/server/endpoints/utils.js index 3c639b3e562a6f337286c9158dd67e1cb5860df1..e9a7d0caa2c4265546e18fd23d22c5c7da1f742f 100644 --- a/server/endpoints/utils.js +++ b/server/endpoints/utils.js @@ -1,5 +1,27 @@ const { SystemSettings } = require("../models/systemSettings"); +function utilEndpoints(app) { + if (!app) return; + + app.get("/utils/metrics", async (_, response) => { + try { + const metrics = { + online: true, + version: getGitVersion(), + mode: (await SystemSettings.isMultiUserMode()) + ? "multi-user" + : "single-user", + vectorDB: process.env.VECTOR_DB || "lancedb", + storage: await getDiskStorage(), + }; + response.status(200).json(metrics); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + }); +} + function getGitVersion() { try { return require("child_process") @@ -32,60 +54,7 @@ async function getDiskStorage() { } } -async function convertToCSV(workspaceChatsMap) { - const rows = ["role,content"]; - for (const workspaceChats of Object.values(workspaceChatsMap)) { - for (const message of workspaceChats.messages) { - // Escape double quotes and wrap content in double quotes - const escapedContent = `"${message.content - .replace(/"/g, '""') - .replace(/\n/g, " ")}"`; - rows.push(`${message.role},${escapedContent}`); - } - } - return rows.join("\n"); -} - -async function convertToJSON(workspaceChatsMap) { - const allMessages = [].concat.apply( - [], - Object.values(workspaceChatsMap).map((workspace) => workspace.messages) - ); - return JSON.stringify(allMessages); -} - -async function convertToJSONL(workspaceChatsMap) { - return Object.values(workspaceChatsMap) - .map((workspaceChats) => JSON.stringify(workspaceChats)) - .join("\n"); -} - -function utilEndpoints(app) { - if (!app) return; - - app.get("/utils/metrics", async (_, response) => { - try { - const metrics = { - online: true, - version: getGitVersion(), - mode: (await SystemSettings.isMultiUserMode()) - ? "multi-user" - : "single-user", - vectorDB: process.env.VECTOR_DB || "lancedb", - storage: await getDiskStorage(), - }; - response.status(200).json(metrics); - } catch (e) { - console.error(e); - response.sendStatus(500).end(); - } - }); -} - module.exports = { utilEndpoints, getGitVersion, - convertToCSV, - convertToJSON, - convertToJSONL, }; diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index 7119297f6a5aa39e1a35a56ff619db2646a821f0..25e391036fea9b9b5ac16beca81fe840a29bc638 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -13,7 +13,10 @@ const { } = require("../utils/files/documentProcessor"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { Telemetry } = require("../models/telemetry"); -const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected"); +const { + flexUserRoleValid, + ROLES, +} = require("../utils/middleware/multiUserProtected"); const { handleUploads } = setupMulter(); function workspaceEndpoints(app) { @@ -21,7 +24,7 @@ function workspaceEndpoints(app) { app.post( "/workspace/new", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -50,7 +53,7 @@ function workspaceEndpoints(app) { app.post( "/workspace/:slug/update", - [validatedRequest], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -79,6 +82,7 @@ function workspaceEndpoints(app) { app.post( "/workspace/:slug/upload", + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], handleUploads.single("file"), async function (request, response) { const { originalname } = request.file; @@ -111,7 +115,7 @@ function workspaceEndpoints(app) { app.post( "/workspace/:slug/upload-link", - [validatedRequest], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { const { link = "" } = reqBody(request); const processingOnline = await checkProcessorAlive(); @@ -143,7 +147,7 @@ function workspaceEndpoints(app) { app.post( "/workspace/:slug/update-embeddings", - [validatedRequest], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -182,7 +186,7 @@ function workspaceEndpoints(app) { app.delete( "/workspace/:slug", - [validatedRequest, flexUserRoleValid], + [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (request, response) => { try { const { slug = "" } = request.params; @@ -215,38 +219,46 @@ function workspaceEndpoints(app) { } ); - app.get("/workspaces", [validatedRequest], async (request, response) => { - try { - const user = await userFromSession(request, response); - const workspaces = multiUserMode(response) - ? await Workspace.whereWithUser(user) - : await Workspace.where(); + app.get( + "/workspaces", + [validatedRequest, flexUserRoleValid([ROLES.all])], + async (request, response) => { + try { + const user = await userFromSession(request, response); + const workspaces = multiUserMode(response) + ? await Workspace.whereWithUser(user) + : await Workspace.where(); - response.status(200).json({ workspaces }); - } catch (e) { - console.log(e.message, e); - response.sendStatus(500).end(); + response.status(200).json({ workspaces }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } } - }); + ); - app.get("/workspace/:slug", [validatedRequest], async (request, response) => { - try { - const { slug } = request.params; - const user = await userFromSession(request, response); - const workspace = multiUserMode(response) - ? await Workspace.getWithUser(user, { slug }) - : await Workspace.get({ slug }); + app.get( + "/workspace/:slug", + [validatedRequest, flexUserRoleValid([ROLES.all])], + async (request, response) => { + try { + const { slug } = request.params; + const user = await userFromSession(request, response); + const workspace = multiUserMode(response) + ? await Workspace.getWithUser(user, { slug }) + : await Workspace.get({ slug }); - response.status(200).json({ workspace }); - } catch (e) { - console.log(e.message, e); - response.sendStatus(500).end(); + response.status(200).json({ workspace }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } } - }); + ); app.get( "/workspace/:slug/chats", - [validatedRequest], + [validatedRequest, flexUserRoleValid([ROLES.all])], async (request, response) => { try { const { slug } = request.params; diff --git a/server/models/workspace.js b/server/models/workspace.js index 9169d193dd8faa8478b4e1f92a31aa7b0311f369..c8e1247ee4e902b00a952e56054a1199a3bf6808 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -2,6 +2,7 @@ const prisma = require("../utils/prisma"); const slugify = require("slugify"); const { Document } = require("./documents"); const { WorkspaceUser } = require("./workspaceUsers"); +const { ROLES } = require("../utils/middleware/multiUserProtected"); const Workspace = { writable: [ @@ -66,7 +67,8 @@ const Workspace = { }, getWithUser: async function (user = null, clause = {}) { - if (["admin", "manager"].includes(user.role)) return this.get(clause); + if ([ROLES.admin, ROLES.manager].includes(user.role)) + return this.get(clause); try { const workspace = await prisma.workspaces.findFirst({ @@ -144,7 +146,7 @@ const Workspace = { limit = null, orderBy = null ) { - if (["admin", "manager"].includes(user.role)) + if ([ROLES.admin, ROLES.manager].includes(user.role)) return await this.where(clause, limit, orderBy); try { diff --git a/server/utils/helpers/admin/index.js b/server/utils/helpers/admin/index.js new file mode 100644 index 0000000000000000000000000000000000000000..87a0127841a567ad6044e833ed6177892ffeea8e --- /dev/null +++ b/server/utils/helpers/admin/index.js @@ -0,0 +1,52 @@ +const { User } = require("../../../models/user"); +const { ROLES } = require("../../middleware/multiUserProtected"); + +// When a user is updating or creating a user in multi-user, we need to check if they +// are allowed to do this and that the new or existing user will be at or below their permission level. +// the user executing this function should be an admin or manager. +function validRoleSelection(currentUser = {}, newUserParams = {}) { + if (!newUserParams.hasOwnProperty("role")) + return { valid: true, error: null }; // not updating role, so skip. + if (currentUser.role === ROLES.admin) return { valid: true, error: null }; + if (currentUser.role === ROLES.manager) { + const validRoles = [ROLES.manager, ROLES.default]; + if (!validRoles.includes(newUserParams.role)) + return { valid: false, error: "Invalid role selection for user." }; + return { valid: true, error: null }; + } + return { valid: false, error: "Invalid condition for caller." }; +} + +// Check to make sure with this update that includes a role change to an existing admin to a non-admin +// that we still have at least one admin left or else they will lock themselves out. +async function canModifyAdmin(userToModify, updates) { + // if updates don't include role property or the user being modified isn't an admin currently - skip. + if (!updates.hasOwnProperty("role")) return { valid: true, error: null }; + if (userToModify.role !== ROLES.admin) return { valid: true, error: null }; + + const adminCount = await User.count({ role: ROLES.admin }); + if (adminCount - 1 <= 0) + return { + valid: false, + error: "No system admins will remain if you do this. Update failed.", + }; + return { valid: true, error: null }; +} + +function validCanModify(currentUser, existingUser) { + if (currentUser.role === ROLES.admin) return { valid: true, error: null }; + if (currentUser.role === ROLES.manager) { + const validRoles = [ROLES.manager, ROLES.default]; + if (!validRoles.includes(existingUser.role)) + return { valid: false, error: "Cannot perform that action on user." }; + return { valid: true, error: null }; + } + + return { valid: false, error: "Invalid condition for caller." }; +} + +module.exports = { + validCanModify, + validRoleSelection, + canModifyAdmin, +}; diff --git a/server/utils/helpers/chat/convertTo.js b/server/utils/helpers/chat/convertTo.js new file mode 100644 index 0000000000000000000000000000000000000000..4dc3955e91d98ac169d8954b3913a63b5c740e05 --- /dev/null +++ b/server/utils/helpers/chat/convertTo.js @@ -0,0 +1,113 @@ +// Helpers that convert workspace chats to some supported format +// for external use by the user. + +const { Workspace } = require("../../../models/workspace"); +const { WorkspaceChats } = require("../../../models/workspaceChats"); + +// Todo: make this more useful for export by adding other columns about workspace, user, time, etc for post-filtering. +async function convertToCSV(workspaceChatsMap) { + const rows = ["role,content"]; + for (const workspaceChats of Object.values(workspaceChatsMap)) { + for (const message of workspaceChats.messages) { + // Escape double quotes and wrap content in double quotes + const escapedContent = `"${message.content + .replace(/"/g, '""') + .replace(/\n/g, " ")}"`; + rows.push(`${message.role},${escapedContent}`); + } + } + return rows.join("\n"); +} + +async function convertToJSON(workspaceChatsMap) { + const allMessages = [].concat.apply( + [], + Object.values(workspaceChatsMap).map((workspace) => workspace.messages) + ); + return JSON.stringify(allMessages); +} + +async function convertToJSONL(workspaceChatsMap) { + return Object.values(workspaceChatsMap) + .map((workspaceChats) => JSON.stringify(workspaceChats)) + .join("\n"); +} + +async function prepareWorkspaceChatsForExport() { + const chats = await WorkspaceChats.whereWithData({}, null, null, { + id: "asc", + }); + const workspaceIds = [...new Set(chats.map((chat) => chat.workspaceId))]; + + const workspacesWithPrompts = await Promise.all( + workspaceIds.map((id) => Workspace.get({ id: Number(id) })) + ); + + const workspacePromptsMap = workspacesWithPrompts.reduce((acc, workspace) => { + acc[workspace.id] = workspace.openAiPrompt; + return acc; + }, {}); + + const workspaceChatsMap = chats.reduce((acc, chat) => { + const { prompt, response, workspaceId } = chat; + const responseJson = JSON.parse(response); + + if (!acc[workspaceId]) { + acc[workspaceId] = { + messages: [ + { + role: "system", + content: + workspacePromptsMap[workspaceId] || + "Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.", + }, + ], + }; + } + + acc[workspaceId].messages.push( + { + role: "user", + content: prompt, + }, + { + role: "assistant", + content: responseJson.text, + } + ); + + return acc; + }, {}); + + return workspaceChatsMap; +} + +const exportMap = { + json: { + contentType: "application/json", + func: convertToJSON, + }, + csv: { + contentType: "text/csv", + func: convertToCSV, + }, + jsonl: { + contentType: "application/jsonl", + func: convertToJSONL, + }, +}; + +async function exportChatsAsType(workspaceChatsMap, format = "jsonl") { + const { contentType, func } = exportMap.hasOwnProperty(format) + ? exportMap[format] + : exportMap.jsonl; + return { + contentType, + data: await func(workspaceChatsMap), + }; +} + +module.exports = { + prepareWorkspaceChatsForExport, + exportChatsAsType, +}; diff --git a/server/utils/middleware/multiUserProtected.js b/server/utils/middleware/multiUserProtected.js index 7de09ac575871846ebc8b77f69f5f130f364c88a..f8a28c962b7708742f7a1b993d82e4df0372e07f 100644 --- a/server/utils/middleware/multiUserProtected.js +++ b/server/utils/middleware/multiUserProtected.js @@ -1,41 +1,71 @@ const { SystemSettings } = require("../../models/systemSettings"); const { userFromSession } = require("../http"); - -const ROLES = ["admin", "manager"]; +const ROLES = { + all: "<all>", + admin: "admin", + manager: "manager", + default: "default", +}; +const DEFAULT_ROLES = [ROLES.admin, ROLES.admin]; // Explicitly check that multi user mode is enabled as well as that the // requesting user has the appropriate role to modify or call the URL. -async function strictMultiUserRoleValid(request, response, next) { - const multiUserMode = - response.locals?.multiUserMode ?? (await SystemSettings.isMultiUserMode()); - if (!multiUserMode) return response.sendStatus(401).end(); +function strictMultiUserRoleValid(allowedRoles = DEFAULT_ROLES) { + return async (request, response, next) => { + // If the access-control is allowable for all - skip validations and continue; + if (allowedRoles.includes(ROLES.all)) { + next(); + return; + } - const user = - response.locals?.user ?? (await userFromSession(request, response)); - if (!ROLES.includes(user?.role)) return response.sendStatus(401).end(); + const multiUserMode = + response.locals?.multiUserMode ?? + (await SystemSettings.isMultiUserMode()); + if (!multiUserMode) return response.sendStatus(401).end(); - next(); + const user = + response.locals?.user ?? (await userFromSession(request, response)); + if (allowedRoles.includes(user?.role)) { + next(); + return; + } + return response.sendStatus(401).end(); + }; } // Apply role permission checks IF the current system is in multi-user mode. // This is relevant for routes that are shared between MUM and single-user mode. // Checks if the requesting user has the appropriate role to modify or call the URL. -async function flexUserRoleValid(request, response, next) { - const multiUserMode = - response.locals?.multiUserMode ?? (await SystemSettings.isMultiUserMode()); - if (!multiUserMode) { - next(); - return; - } +function flexUserRoleValid(allowedRoles = DEFAULT_ROLES) { + return async (request, response, next) => { + // If the access-control is allowable for all - skip validations and continue; + // It does not matter if multi-user or not. + if (allowedRoles.includes(ROLES.all)) { + next(); + return; + } - const user = - response.locals?.user ?? (await userFromSession(request, response)); - if (!ROLES.includes(user?.role)) return response.sendStatus(401).end(); + // Bypass if not in multi-user mode + const multiUserMode = + response.locals?.multiUserMode ?? + (await SystemSettings.isMultiUserMode()); + if (!multiUserMode) { + next(); + return; + } - next(); + const user = + response.locals?.user ?? (await userFromSession(request, response)); + if (allowedRoles.includes(user?.role)) { + next(); + return; + } + return response.sendStatus(401).end(); + }; } module.exports = { + ROLES, strictMultiUserRoleValid, flexUserRoleValid, };