Implement total permission overhaul ()

* Implement total permission overhaul
Add explicit permissions on each flex and strict route
Patch issues with role escalation and CRUD of users
Patch permissions on all routes for coverage
Improve middleware to accept role array for clarity

* update comments

* remove permissions to API-keys for manager. Manager could generate API-key and using high-privelege api-key give themselves admin

* update sidebar permissions for multi-user and single user

* update options for mobile sidebar
This commit is contained in:
Timothy Carambat 2024-01-22 14:14:01 -08:00 committed by GitHub
parent 62cea07599
commit 9a237db3d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 612 additions and 375 deletions
frontend/src
App.jsx
components/SettingsSidebar
models
pages/Admin/Users
server

View file

@ -81,7 +81,7 @@ export default function App() {
/> />
<Route <Route
path="/settings/api-keys" path="/settings/api-keys"
element={<ManagerRoute Component={GeneralApiKeys} />} element={<AdminRoute Component={GeneralApiKeys} />}
/> />
<Route <Route
path="/settings/workspace-chats" path="/settings/workspace-chats"

View file

@ -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-[100%] flex flex-col w-full justify-between pt-4 overflow-y-hidden">
<div className="h-auto sidebar-items"> <div className="h-auto sidebar-items">
<div className="flex flex-col gap-y-2 h-[65vh] pb-8 overflow-y-scroll no-scroll"> <div className="flex flex-col gap-y-2 h-[65vh] pb-8 overflow-y-scroll no-scroll">
{/* Admin/manager Multi-user Settings */} <Option
{!!user && user?.role !== "default" && ( href={paths.settings.system()}
<> btnText="System Preferences"
<Option icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.system()} user={user}
btnText="System Preferences" allowedRole={["admin", "manager"]}
icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />} />
/> <Option
<Option href={paths.settings.invites()}
href={paths.settings.invites()} btnText="Invitation"
btnText="Invitation" icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />}
icon={ user={user}
<EnvelopeSimple className="h-5 w-5 flex-shrink-0" /> allowedRole={["admin", "manager"]}
} />
/> <Option
<Option href={paths.settings.users()}
href={paths.settings.users()} btnText="Users"
btnText="Users" icon={<Users className="h-5 w-5 flex-shrink-0" />}
icon={<Users className="h-5 w-5 flex-shrink-0" />} user={user}
/> allowedRole={["admin", "manager"]}
<Option />
href={paths.settings.workspaces()} <Option
btnText="Workspaces" href={paths.settings.workspaces()}
icon={<BookOpen className="h-5 w-5 flex-shrink-0" />} btnText="Workspaces"
/> icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
</> user={user}
)} allowedRole={["admin", "manager"]}
/>
<Option <Option
href={paths.settings.chats()} href={paths.settings.chats()}
btnText="Workspace Chat" btnText="Workspace Chat"
icon={<ChatCenteredText className="h-5 w-5 flex-shrink-0" />} icon={<ChatCenteredText className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/> />
<Option <Option
href={paths.settings.appearance()} href={paths.settings.appearance()}
btnText="Appearance" btnText="Appearance"
icon={<Eye className="h-5 w-5 flex-shrink-0" />} icon={<Eye className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/> />
<Option <Option
href={paths.settings.apiKeys()} href={paths.settings.apiKeys()}
btnText="API Keys" btnText="API Keys"
icon={<Key className="h-5 w-5 flex-shrink-0" />} icon={<Key className="h-5 w-5 flex-shrink-0" />}
user={user}
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 <Option
href={paths.settings.security()} href={paths.settings.security()}
btnText="Security" btnText="Security"
icon={<Lock className="h-5 w-5 flex-shrink-0" />} icon={<Lock className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/> />
</div> </div>
</div> </div>
@ -265,63 +283,95 @@ export function SidebarMobileHeader() {
href={paths.settings.system()} href={paths.settings.system()}
btnText="System Preferences" btnText="System Preferences"
icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />} icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/> />
<Option <Option
href={paths.settings.invites()} href={paths.settings.invites()}
btnText="Invitation" btnText="Invitation"
icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />} icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/> />
<Option <Option
href={paths.settings.users()} href={paths.settings.users()}
btnText="Users" btnText="Users"
icon={<Users className="h-5 w-5 flex-shrink-0" />} icon={<Users className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/> />
<Option <Option
href={paths.settings.workspaces()} href={paths.settings.workspaces()}
btnText="Workspaces" btnText="Workspaces"
icon={<BookOpen className="h-5 w-5 flex-shrink-0" />} icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/> />
<Option <Option
href={paths.settings.chats()} href={paths.settings.chats()}
btnText="Workspace Chat" btnText="Workspace Chat"
icon={ icon={
<ChatCenteredText className="h-5 w-5 flex-shrink-0" /> <ChatCenteredText className="h-5 w-5 flex-shrink-0" />
} }
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/> />
<Option <Option
href={paths.settings.appearance()} href={paths.settings.appearance()}
btnText="Appearance" btnText="Appearance"
icon={<Eye className="h-5 w-5 flex-shrink-0" />} icon={<Eye className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/> />
<Option <Option
href={paths.settings.apiKeys()} href={paths.settings.apiKeys()}
btnText="API Keys" btnText="API Keys"
icon={<Key className="h-5 w-5 flex-shrink-0" />} icon={<Key className="h-5 w-5 flex-shrink-0" />}
user={user}
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 <Option
href={paths.settings.security()} href={paths.settings.security()}
btnText="Security" btnText="Security"
icon={<Lock className="h-5 w-5 flex-shrink-0" />} icon={<Lock className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/> />
</div> </div>
</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; 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 ( return (
<div className="flex gap-x-2 items-center justify-between text-white"> <div className="flex gap-x-2 items-center justify-between text-white">
<a <a

View file

@ -209,6 +209,7 @@ const System = {
return await fetch(`${API_BASE}/system/pfp/${id}`, { return await fetch(`${API_BASE}/system/pfp/${id}`, {
method: "GET", method: "GET",
cache: "no-cache", cache: "no-cache",
headers: baseHeaders(),
}) })
.then((res) => { .then((res) => {
if (res.ok && res.status !== 204) return res.blob(); if (res.ok && res.status !== 204) return res.blob();
@ -283,6 +284,7 @@ const System = {
return await fetch(`${API_BASE}/system/welcome-messages`, { return await fetch(`${API_BASE}/system/welcome-messages`, {
method: "GET", method: "GET",
cache: "no-cache", cache: "no-cache",
headers: baseHeaders(),
}) })
.then((res) => { .then((res) => {
if (!res.ok) throw new Error("Could not fetch welcome messages."); if (!res.ok) throw new Error("Could not fetch welcome messages.");

View file

@ -3,9 +3,17 @@ import { titleCase } from "text-case";
import Admin from "@/models/admin"; import Admin from "@/models/admin";
import EditUserModal, { EditUserModalId } from "./EditUserModal"; import EditUserModal, { EditUserModalId } from "./EditUserModal";
import { DotsThreeOutline } from "@phosphor-icons/react"; 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 }) { export default function UserRow({ currUser, user }) {
const rowRef = useRef(null); const rowRef = useRef(null);
const canModify = ModMap[currUser?.role || "default"].includes(user.role);
const [suspended, setSuspended] = useState(user.suspended === 1); const [suspended, setSuspended] = useState(user.suspended === 1);
const handleSuspend = async () => { const handleSuspend = async () => {
if ( if (
@ -14,8 +22,19 @@ export default function UserRow({ currUser, user }) {
) )
) )
return false; 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 () => { const handleDelete = async () => {
if ( if (
@ -24,8 +43,12 @@ export default function UserRow({ currUser, user }) {
) )
) )
return false; return false;
rowRef?.current?.remove(); const { success, error } = await Admin.deleteUser(user.id);
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 ( 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">{titleCase(user.role)}</td>
<td className="px-6 py-4">{user.createdAt}</td> <td className="px-6 py-4">{user.createdAt}</td>
<td className="px-6 py-4 flex items-center gap-x-6"> <td className="px-6 py-4 flex items-center gap-x-6">
{currUser?.role !== "default" && ( {canModify && (
<button <button
onClick={() => onClick={() =>
document?.getElementById(EditUserModalId(user))?.showModal() document?.getElementById(EditUserModalId(user))?.showModal()
@ -50,7 +73,7 @@ export default function UserRow({ currUser, user }) {
<DotsThreeOutline weight="fill" className="h-5 w-5" /> <DotsThreeOutline weight="fill" className="h-5 w-5" />
</button> </button>
)} )}
{currUser?.id !== user.id && currUser?.role !== "default" && ( {currUser?.id !== user.id && canModify && (
<> <>
<button <button
onClick={handleSuspend} onClick={handleSuspend}

View file

@ -105,7 +105,8 @@ const ROLE_HINT = {
"Cannot modify any settings at all.", "Cannot modify any settings at all.",
], ],
manager: [ 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.", "Cannot modify LLM, vectorDB, embedding, or other connections.",
], ],
admin: [ admin: [

View file

@ -7,9 +7,15 @@ const { DocumentVectors } = require("../models/vectors");
const { Workspace } = require("../models/workspace"); const { Workspace } = require("../models/workspace");
const { WorkspaceChats } = require("../models/workspaceChats"); const { WorkspaceChats } = require("../models/workspaceChats");
const { getVectorDbClass } = require("../utils/helpers"); const { getVectorDbClass } = require("../utils/helpers");
const {
validRoleSelection,
canModifyAdmin,
validCanModify,
} = require("../utils/helpers/admin");
const { reqBody, userFromSession } = require("../utils/http"); const { reqBody, userFromSession } = require("../utils/http");
const { const {
strictMultiUserRoleValid, strictMultiUserRoleValid,
ROLES,
} = require("../utils/middleware/multiUserProtected"); } = require("../utils/middleware/multiUserProtected");
const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { validatedRequest } = require("../utils/middleware/validatedRequest");
@ -18,7 +24,7 @@ function adminEndpoints(app) {
app.get( app.get(
"/admin/users", "/admin/users",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (_request, response) => { async (_request, response) => {
try { try {
const users = (await User.where()).map((user) => { const users = (await User.where()).map((user) => {
@ -35,10 +41,20 @@ function adminEndpoints(app) {
app.post( app.post(
"/admin/users/new", "/admin/users/new",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const currUser = await userFromSession(request, response);
const newUserParams = reqBody(request); 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); const { user: newUser, error } = await User.create(newUserParams);
response.status(200).json({ user: newUser, error }); response.status(200).json({ user: newUser, error });
} catch (e) { } catch (e) {
@ -50,29 +66,34 @@ function adminEndpoints(app) {
app.post( app.post(
"/admin/user/:id", "/admin/user/:id",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const currUser = await userFromSession(request, response);
const { id } = request.params; const { id } = request.params;
const updates = reqBody(request); const updates = reqBody(request);
const user = await User.get({ id: Number(id) }); const user = await User.get({ id: Number(id) });
// Check to make sure with this update that includes a role change to const canModify = validCanModify(currUser, user);
// something other than admin that we still have at least one admin left. if (!canModify.valid) {
if ( response.status(200).json({ success: false, error: canModify.error });
updates.hasOwnProperty("role") && // has admin prop to change return;
updates.role !== "admin" && // and we are changing to non-admin }
user.role === "admin" // and they currently are an admin
) { const roleValidation = validRoleSelection(currUser, updates);
const adminCount = await User.count({ role: "admin" }); if (!roleValidation.valid) {
if (adminCount - 1 <= 0) { response
response.status(200).json({ .status(200)
success: false, .json({ success: false, error: roleValidation.error });
error: return;
"No system admins will remain if you do this. Update failed.", }
});
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); const { success, error } = await User.update(id, updates);
@ -86,10 +107,19 @@ function adminEndpoints(app) {
app.delete( app.delete(
"/admin/user/:id", "/admin/user/:id",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const currUser = await userFromSession(request, response);
const { id } = request.params; 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) }); await User.delete({ id: Number(id) });
response.status(200).json({ success: true, error: null }); response.status(200).json({ success: true, error: null });
} catch (e) { } catch (e) {
@ -101,7 +131,7 @@ function adminEndpoints(app) {
app.get( app.get(
"/admin/invites", "/admin/invites",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (_request, response) => { async (_request, response) => {
try { try {
const invites = await Invite.whereWithUsers(); const invites = await Invite.whereWithUsers();
@ -115,7 +145,7 @@ function adminEndpoints(app) {
app.get( app.get(
"/admin/invite/new", "/admin/invite/new",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
@ -130,7 +160,7 @@ function adminEndpoints(app) {
app.delete( app.delete(
"/admin/invite/:id", "/admin/invite/:id",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const { id } = request.params; const { id } = request.params;
@ -145,7 +175,7 @@ function adminEndpoints(app) {
app.get( app.get(
"/admin/workspaces", "/admin/workspaces",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (_request, response) => { async (_request, response) => {
try { try {
const workspaces = await Workspace.whereWithUsers(); const workspaces = await Workspace.whereWithUsers();
@ -159,7 +189,7 @@ function adminEndpoints(app) {
app.post( app.post(
"/admin/workspaces/new", "/admin/workspaces/new",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
@ -178,7 +208,7 @@ function adminEndpoints(app) {
app.post( app.post(
"/admin/workspaces/:workspaceId/update-users", "/admin/workspaces/:workspaceId/update-users",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const { workspaceId } = request.params; const { workspaceId } = request.params;
@ -197,7 +227,7 @@ function adminEndpoints(app) {
app.delete( app.delete(
"/admin/workspaces/:id", "/admin/workspaces/:id",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const { id } = request.params; const { id } = request.params;
@ -228,7 +258,7 @@ function adminEndpoints(app) {
app.get( app.get(
"/admin/system-preferences", "/admin/system-preferences",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (_request, response) => { async (_request, response) => {
try { try {
const settings = { const settings = {
@ -253,7 +283,7 @@ function adminEndpoints(app) {
app.post( app.post(
"/admin/system-preferences", "/admin/system-preferences",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const updates = reqBody(request); const updates = reqBody(request);
@ -268,7 +298,7 @@ function adminEndpoints(app) {
app.get( app.get(
"/admin/api-keys", "/admin/api-keys",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
async (_request, response) => { async (_request, response) => {
try { try {
const apiKeys = await ApiKey.whereWithUser({}); const apiKeys = await ApiKey.whereWithUser({});
@ -288,7 +318,7 @@ function adminEndpoints(app) {
app.post( app.post(
"/admin/generate-api-key", "/admin/generate-api-key",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
@ -306,7 +336,7 @@ function adminEndpoints(app) {
app.delete( app.delete(
"/admin/delete-api-key/:id", "/admin/delete-api-key/:id",
[validatedRequest, strictMultiUserRoleValid], [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
async (request, response) => { async (request, response) => {
try { try {
const { id } = request.params; const { id } = request.params;

View file

@ -3,6 +3,7 @@ const { SystemSettings } = require("../../../models/systemSettings");
const { User } = require("../../../models/user"); const { User } = require("../../../models/user");
const { Workspace } = require("../../../models/workspace"); const { Workspace } = require("../../../models/workspace");
const { WorkspaceChats } = require("../../../models/workspaceChats"); const { WorkspaceChats } = require("../../../models/workspaceChats");
const { canModifyAdmin } = require("../../../utils/helpers/admin");
const { multiUserMode, reqBody } = require("../../../utils/http"); const { multiUserMode, reqBody } = require("../../../utils/http");
const { validApiKey } = require("../../../utils/middleware/validApiKey"); const { validApiKey } = require("../../../utils/middleware/validApiKey");
@ -198,23 +199,13 @@ function apiAdminEndpoints(app) {
const { id } = request.params; const { id } = request.params;
const updates = reqBody(request); const updates = reqBody(request);
const user = await User.get({ id: Number(id) }); 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 if (!validAdminRoleModification.valid) {
// something other than admin that we still have at least one admin left. response
if ( .status(200)
updates.hasOwnProperty("role") && // has admin prop to change .json({ success: false, error: validAdminRoleModification.error });
updates.role !== "admin" && // and we are changing to non-admin return;
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 { success, error } = await User.update(id, updates); const { success, error } = await User.update(id, updates);

View file

@ -10,13 +10,17 @@ const {
writeResponseChunk, writeResponseChunk,
VALID_CHAT_MODE, VALID_CHAT_MODE,
} = require("../utils/chats/stream"); } = require("../utils/chats/stream");
const {
ROLES,
flexUserRoleValid,
} = require("../utils/middleware/multiUserProtected");
function chatEndpoints(app) { function chatEndpoints(app) {
if (!app) return; if (!app) return;
app.post( app.post(
"/workspace/:slug/stream-chat", "/workspace/:slug/stream-chat",
[validatedRequest], [validatedRequest, flexUserRoleValid([ROLES.all])],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
@ -52,7 +56,7 @@ function chatEndpoints(app) {
response.setHeader("Connection", "keep-alive"); response.setHeader("Connection", "keep-alive");
response.flushHeaders(); response.flushHeaders();
if (multiUserMode(response) && user.role !== "admin") { if (multiUserMode(response) && user.role !== ROLES.admin) {
const limitMessagesSetting = await SystemSettings.get({ const limitMessagesSetting = await SystemSettings.get({
label: "limit_user_messages", label: "limit_user_messages",
}); });

View file

@ -4,6 +4,7 @@ const {
} = require("../../utils/files/documentProcessor"); } = require("../../utils/files/documentProcessor");
const { const {
flexUserRoleValid, flexUserRoleValid,
ROLES,
} = require("../../utils/middleware/multiUserProtected"); } = require("../../utils/middleware/multiUserProtected");
const { validatedRequest } = require("../../utils/middleware/validatedRequest"); const { validatedRequest } = require("../../utils/middleware/validatedRequest");
@ -12,7 +13,7 @@ function extensionEndpoints(app) {
app.post( app.post(
"/ext/github/branches", "/ext/github/branches",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const responseFromProcessor = await forwardExtensionRequest({ const responseFromProcessor = await forwardExtensionRequest({
@ -30,7 +31,7 @@ function extensionEndpoints(app) {
app.post( app.post(
"/ext/github/repo", "/ext/github/repo",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const responseFromProcessor = await forwardExtensionRequest({ const responseFromProcessor = await forwardExtensionRequest({
@ -51,7 +52,7 @@ function extensionEndpoints(app) {
app.post( app.post(
"/ext/youtube/transcript", "/ext/youtube/transcript",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const responseFromProcessor = await forwardExtensionRequest({ const responseFromProcessor = await forwardExtensionRequest({

View file

@ -39,10 +39,15 @@ const { WelcomeMessages } = require("../models/welcomeMessages");
const { ApiKey } = require("../models/apiKeys"); const { ApiKey } = require("../models/apiKeys");
const { getCustomModels } = require("../utils/helpers/customModels"); const { getCustomModels } = require("../utils/helpers/customModels");
const { WorkspaceChats } = require("../models/workspaceChats"); const { WorkspaceChats } = require("../models/workspaceChats");
const { Workspace } = require("../models/workspace"); const {
const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected"); flexUserRoleValid,
ROLES,
} = require("../utils/middleware/multiUserProtected");
const { fetchPfp, determinePfpFilepath } = require("../utils/files/pfp"); const { fetchPfp, determinePfpFilepath } = require("../utils/files/pfp");
const { convertToCSV, convertToJSON, convertToJSONL } = require("./utils"); const {
prepareWorkspaceChatsForExport,
exportChatsAsType,
} = require("../utils/helpers/chat/convertTo");
function systemEndpoints(app) { function systemEndpoints(app) {
if (!app) return; if (!app) return;
@ -275,15 +280,9 @@ function systemEndpoints(app) {
app.post( app.post(
"/system/update-env", "/system/update-env",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.admin])],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response);
if (!!user && user.role !== "admin") {
response.sendStatus(401).end();
return;
}
const body = reqBody(request); const body = reqBody(request);
const { newValues, error } = await updateENV(body); const { newValues, error } = await updateENV(body);
if (process.env.NODE_ENV === "production") await dumpENV(); if (process.env.NODE_ENV === "production") await dumpENV();
@ -341,7 +340,7 @@ function systemEndpoints(app) {
const { user, error } = await User.create({ const { user, error } = await User.create({
username, username,
password, password,
role: "admin", role: ROLES.admin,
}); });
await SystemSettings.updateSettings({ await SystemSettings.updateSettings({
multi_user_mode: true, 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 { try {
const multiUserMode = await SystemSettings.isMultiUserMode(); const multiUserMode = await SystemSettings.isMultiUserMode();
response.status(200).json({ multiUserMode }); 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 { try {
const defaultFilename = getDefaultFilename(); const defaultFilename = getDefaultFilename();
const logoPath = await determineLogoFilepath(defaultFilename); const logoPath = await determineLogoFilepath(defaultFilename);
@ -409,56 +408,61 @@ function systemEndpoints(app) {
} }
}); });
app.get("/system/pfp/:id", async function (request, response) { app.get(
try { "/system/pfp/:id",
const { id } = request.params; [validatedRequest, flexUserRoleValid([ROLES.all])],
const pfpPath = await determinePfpFilepath(id); async function (request, response) {
try {
const { id } = request.params;
const pfpPath = await determinePfpFilepath(id);
if (!pfpPath) { if (!pfpPath) {
response.sendStatus(204).end(); response.sendStatus(204).end();
return;
}
const { found, buffer, size, mime } = fetchPfp(pfpPath);
if (!found) {
response.sendStatus(204).end();
return;
}
response.writeHead(200, {
"Content-Type": mime || "image/png",
"Content-Disposition": `attachment; filename=${path.basename(
pfpPath
)}`,
"Content-Length": size,
});
response.end(Buffer.from(buffer, "base64"));
return; return;
} catch (error) {
console.error("Error processing the logo request:", error);
response.status(500).json({ message: "Internal server error" });
} }
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" });
} }
}); );
app.post( app.post(
"/system/upload-pfp", "/system/upload-pfp",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.all])],
handlePfpUploads.single("file"), handlePfpUploads.single("file"),
async function (request, response) { async function (request, response) {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
const uploadedFileName = request.randomFileName; const uploadedFileName = request.randomFileName;
if (!uploadedFileName) { if (!uploadedFileName) {
return response.status(400).json({ message: "File upload failed." }); return response.status(400).json({ message: "File upload failed." });
} }
const userRecord = await User.get({ id: user.id }); const userRecord = await User.get({ id: user.id });
const oldPfpFilename = normalizePath(userRecord.pfpFilename); const oldPfpFilename = userRecord.pfpFilename;
console.log("oldPfpFilename", oldPfpFilename); console.log("oldPfpFilename", oldPfpFilename);
if (oldPfpFilename) { if (oldPfpFilename) {
const oldPfpPath = path.join( const oldPfpPath = path.join(
__dirname, __dirname,
`../storage/assets/pfp/${oldPfpFilename}` `../storage/assets/pfp/${normalizePath(userRecord.pfpFilename)}`
); );
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath); if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
@ -482,17 +486,18 @@ function systemEndpoints(app) {
app.delete( app.delete(
"/system/remove-pfp", "/system/remove-pfp",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.all])],
async function (request, response) { async function (request, response) {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
const userRecord = await User.get({ id: user.id }); const userRecord = await User.get({ id: user.id });
const oldPfpFilename = normalizePath(userRecord.pfpFilename); const oldPfpFilename = userRecord.pfpFilename;
console.log("oldPfpFilename", oldPfpFilename); console.log("oldPfpFilename", oldPfpFilename);
if (oldPfpFilename) { if (oldPfpFilename) {
const oldPfpPath = path.join( const oldPfpPath = path.join(
__dirname, __dirname,
`../storage/assets/pfp/${oldPfpFilename}` `../storage/assets/pfp/${normalizePath(oldPfpFilename)}`
); );
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath); if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
@ -516,7 +521,7 @@ function systemEndpoints(app) {
app.post( app.post(
"/system/upload-logo", "/system/upload-logo",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
handleLogoUploads.single("logo"), handleLogoUploads.single("logo"),
async (request, response) => { async (request, response) => {
if (!request.file || !request.file.originalname) { 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 { try {
const currentLogoFilename = await SystemSettings.currentLogoFilename(); const currentLogoFilename = await SystemSettings.currentLogoFilename();
const isDefaultLogo = currentLogoFilename === LOGO_FILENAME; const isDefaultLogo = currentLogoFilename === LOGO_FILENAME;
@ -563,7 +568,7 @@ function systemEndpoints(app) {
app.get( app.get(
"/system/remove-logo", "/system/remove-logo",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (_request, response) => { async (_request, response) => {
try { try {
const currentLogoFilename = await SystemSettings.currentLogoFilename(); const currentLogoFilename = await SystemSettings.currentLogoFilename();
@ -594,7 +599,7 @@ function systemEndpoints(app) {
} }
const user = await userFromSession(request, response); 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 }); return response.status(200).json({ canDelete: true });
} }
@ -611,21 +616,25 @@ function systemEndpoints(app) {
} }
); );
app.get("/system/welcome-messages", async function (request, response) { app.get(
try { "/system/welcome-messages",
const welcomeMessages = await WelcomeMessages.getMessages(); [validatedRequest, flexUserRoleValid([ROLES.all])],
response.status(200).json({ success: true, welcomeMessages }); async function (_, response) {
} catch (error) { try {
console.error("Error fetching welcome messages:", error); const welcomeMessages = await WelcomeMessages.getMessages();
response response.status(200).json({ success: true, welcomeMessages });
.status(500) } catch (error) {
.json({ success: false, message: "Internal server error" }); console.error("Error fetching welcome messages:", error);
response
.status(500)
.json({ success: false, message: "Internal server error" });
}
} }
}); );
app.post( app.post(
"/system/set-welcome-messages", "/system/set-welcome-messages",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const { messages = [] } = reqBody(request); const { messages = [] } = reqBody(request);
@ -733,7 +742,7 @@ function systemEndpoints(app) {
app.post( app.post(
"/system/workspace-chats", "/system/workspace-chats",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const { offset = 0, limit = 20 } = reqBody(request); const { offset = 0, limit = 20 } = reqBody(request);
@ -756,7 +765,7 @@ function systemEndpoints(app) {
app.delete( app.delete(
"/system/workspace-chats/:id", "/system/workspace-chats/:id",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const { id } = request.params; const { id } = request.params;
@ -771,81 +780,14 @@ function systemEndpoints(app) {
app.get( app.get(
"/system/export-chats", "/system/export-chats",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.manager, ROLES.admin])],
async (request, response) => { async (request, response) => {
try { try {
const { type = "jsonl" } = request.query; const { type = "jsonl" } = request.query;
const chats = await WorkspaceChats.whereWithData({}, null, null, { const chats = await prepareWorkspaceChatsForExport();
id: "asc", const { contentType, data } = await exportChatsAsType(chats, type);
}); response.setHeader("Content-Type", contentType);
const workspaceIds = [ response.status(200).send(data);
...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);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
response.sendStatus(500).end(); 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) => { app.post("/system/user", [validatedRequest], async (request, response) => {
try { try {
const sessionUser = await userFromSession(request, response); const sessionUser = await userFromSession(request, response);

View file

@ -1,5 +1,27 @@
const { SystemSettings } = require("../models/systemSettings"); 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() { function getGitVersion() {
try { try {
return require("child_process") 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 = { module.exports = {
utilEndpoints, utilEndpoints,
getGitVersion, getGitVersion,
convertToCSV,
convertToJSON,
convertToJSONL,
}; };

View file

@ -13,7 +13,10 @@ const {
} = require("../utils/files/documentProcessor"); } = require("../utils/files/documentProcessor");
const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { validatedRequest } = require("../utils/middleware/validatedRequest");
const { Telemetry } = require("../models/telemetry"); const { Telemetry } = require("../models/telemetry");
const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected"); const {
flexUserRoleValid,
ROLES,
} = require("../utils/middleware/multiUserProtected");
const { handleUploads } = setupMulter(); const { handleUploads } = setupMulter();
function workspaceEndpoints(app) { function workspaceEndpoints(app) {
@ -21,7 +24,7 @@ function workspaceEndpoints(app) {
app.post( app.post(
"/workspace/new", "/workspace/new",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
@ -50,7 +53,7 @@ function workspaceEndpoints(app) {
app.post( app.post(
"/workspace/:slug/update", "/workspace/:slug/update",
[validatedRequest], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
@ -79,6 +82,7 @@ function workspaceEndpoints(app) {
app.post( app.post(
"/workspace/:slug/upload", "/workspace/:slug/upload",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
handleUploads.single("file"), handleUploads.single("file"),
async function (request, response) { async function (request, response) {
const { originalname } = request.file; const { originalname } = request.file;
@ -111,7 +115,7 @@ function workspaceEndpoints(app) {
app.post( app.post(
"/workspace/:slug/upload-link", "/workspace/:slug/upload-link",
[validatedRequest], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
const { link = "" } = reqBody(request); const { link = "" } = reqBody(request);
const processingOnline = await checkProcessorAlive(); const processingOnline = await checkProcessorAlive();
@ -143,7 +147,7 @@ function workspaceEndpoints(app) {
app.post( app.post(
"/workspace/:slug/update-embeddings", "/workspace/:slug/update-embeddings",
[validatedRequest], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
@ -182,7 +186,7 @@ function workspaceEndpoints(app) {
app.delete( app.delete(
"/workspace/:slug", "/workspace/:slug",
[validatedRequest, flexUserRoleValid], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => { async (request, response) => {
try { try {
const { slug = "" } = request.params; const { slug = "" } = request.params;
@ -215,38 +219,46 @@ function workspaceEndpoints(app) {
} }
); );
app.get("/workspaces", [validatedRequest], async (request, response) => { app.get(
try { "/workspaces",
const user = await userFromSession(request, response); [validatedRequest, flexUserRoleValid([ROLES.all])],
const workspaces = multiUserMode(response) async (request, response) => {
? await Workspace.whereWithUser(user) try {
: await Workspace.where(); const user = await userFromSession(request, response);
const workspaces = multiUserMode(response)
? await Workspace.whereWithUser(user)
: await Workspace.where();
response.status(200).json({ workspaces }); response.status(200).json({ workspaces });
} catch (e) { } catch (e) {
console.log(e.message, e); console.log(e.message, e);
response.sendStatus(500).end(); response.sendStatus(500).end();
}
} }
}); );
app.get("/workspace/:slug", [validatedRequest], async (request, response) => { app.get(
try { "/workspace/:slug",
const { slug } = request.params; [validatedRequest, flexUserRoleValid([ROLES.all])],
const user = await userFromSession(request, response); async (request, response) => {
const workspace = multiUserMode(response) try {
? await Workspace.getWithUser(user, { slug }) const { slug } = request.params;
: await Workspace.get({ slug }); const user = await userFromSession(request, response);
const workspace = multiUserMode(response)
? await Workspace.getWithUser(user, { slug })
: await Workspace.get({ slug });
response.status(200).json({ workspace }); response.status(200).json({ workspace });
} catch (e) { } catch (e) {
console.log(e.message, e); console.log(e.message, e);
response.sendStatus(500).end(); response.sendStatus(500).end();
}
} }
}); );
app.get( app.get(
"/workspace/:slug/chats", "/workspace/:slug/chats",
[validatedRequest], [validatedRequest, flexUserRoleValid([ROLES.all])],
async (request, response) => { async (request, response) => {
try { try {
const { slug } = request.params; const { slug } = request.params;

View file

@ -2,6 +2,7 @@ const prisma = require("../utils/prisma");
const slugify = require("slugify"); const slugify = require("slugify");
const { Document } = require("./documents"); const { Document } = require("./documents");
const { WorkspaceUser } = require("./workspaceUsers"); const { WorkspaceUser } = require("./workspaceUsers");
const { ROLES } = require("../utils/middleware/multiUserProtected");
const Workspace = { const Workspace = {
writable: [ writable: [
@ -66,7 +67,8 @@ const Workspace = {
}, },
getWithUser: async function (user = null, clause = {}) { 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 { try {
const workspace = await prisma.workspaces.findFirst({ const workspace = await prisma.workspaces.findFirst({
@ -144,7 +146,7 @@ const Workspace = {
limit = null, limit = null,
orderBy = null orderBy = null
) { ) {
if (["admin", "manager"].includes(user.role)) if ([ROLES.admin, ROLES.manager].includes(user.role))
return await this.where(clause, limit, orderBy); return await this.where(clause, limit, orderBy);
try { try {

View file

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

View file

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

View file

@ -1,41 +1,71 @@
const { SystemSettings } = require("../../models/systemSettings"); const { SystemSettings } = require("../../models/systemSettings");
const { userFromSession } = require("../http"); const { userFromSession } = require("../http");
const ROLES = {
const ROLES = ["admin", "manager"]; 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 // 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. // requesting user has the appropriate role to modify or call the URL.
async function strictMultiUserRoleValid(request, response, next) { function strictMultiUserRoleValid(allowedRoles = DEFAULT_ROLES) {
const multiUserMode = return async (request, response, next) => {
response.locals?.multiUserMode ?? (await SystemSettings.isMultiUserMode()); // If the access-control is allowable for all - skip validations and continue;
if (!multiUserMode) return response.sendStatus(401).end(); if (allowedRoles.includes(ROLES.all)) {
next();
return;
}
const user = const multiUserMode =
response.locals?.user ?? (await userFromSession(request, response)); response.locals?.multiUserMode ??
if (!ROLES.includes(user?.role)) return response.sendStatus(401).end(); (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. // 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. // 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. // Checks if the requesting user has the appropriate role to modify or call the URL.
async function flexUserRoleValid(request, response, next) { function flexUserRoleValid(allowedRoles = DEFAULT_ROLES) {
const multiUserMode = return async (request, response, next) => {
response.locals?.multiUserMode ?? (await SystemSettings.isMultiUserMode()); // If the access-control is allowable for all - skip validations and continue;
if (!multiUserMode) { // It does not matter if multi-user or not.
next(); if (allowedRoles.includes(ROLES.all)) {
return; next();
} return;
}
const user = // Bypass if not in multi-user mode
response.locals?.user ?? (await userFromSession(request, response)); const multiUserMode =
if (!ROLES.includes(user?.role)) return response.sendStatus(401).end(); 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 = { module.exports = {
ROLES,
strictMultiUserRoleValid, strictMultiUserRoleValid,
flexUserRoleValid, flexUserRoleValid,
}; };