CVE-2024-0795 (JavaScript, published 2024-03-02T22:15Z)
Description: value: If an attacked was given access to an instance with the admin or manager role there is no backend authentication that would prevent the attacked from creating a new user with an `admin` role and then be able to use this new account to have elevated privileges on the instance
Severity: nan (CVSS v3 base score: )
Weakness: CWE-284 \u2014 Improper Access Control
The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.
Repository: https://github.com/mintplex-labs/anything-llm
Files changed:
frontend/src/App.jsx
frontend/src/components/SettingsSidebar/index.jsx
frontend/src/models/system.js
frontend/src/pages/Admin/Users/UserRow/index.jsx
frontend/src/pages/Admin/Users/index.jsx
server/endpoints/admin.js
server/endpoints/api/admin/index.js
server/endpoints/chat.js
server/endpoints/extensions/index.js
server/endpoints/system.js
server/endpoints/utils.js
server/endpoints/workspaces.js
server/models/workspace.js
server/utils/helpers/admin/index.js
server/utils/helpers/chat/convertTo.js
server/utils/middleware/multiUserProtected.js
Commit message: Implement total permission overhaul (#629)

* 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

Vulnerable code:
                element={<ManagerRoute Component={GeneralApiKeys} />}
                {/* 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" />}
                    />
                  </>
                )}



                {(!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" />}
                    />
                  </>
                )}

                  {(!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" />}
                      />
                    </>
                  )}
const Option = ({ btnText, icon, href }) => {
    setSuspended(!suspended);
    await Admin.updateUser(user.id, { suspended: suspended ? 0 : 1 });
    rowRef?.current?.remove();
    await Admin.deleteUser(user.id);
          {currUser?.role !== "default" && (
          {currUser?.id !== user.id && currUser?.role !== "default" && (
    "Can view all workspaces and modify all settings.",
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
        // 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;
          }
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
    [validatedRequest, strictMultiUserRoleValid],
      // 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;
        }
    [validatedRequest],
        if (multiUserMode(response) && user.role !== "admin") {
    [validatedRequest, flexUserRoleValid],
    [validatedRequest, flexUserRoleValid],
    [validatedRequest, flexUserRoleValid],
const { Workspace } = require("../models/workspace");
const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected");
const { convertToCSV, convertToJSON, convertToJSONL } = require("./utils");
    [validatedRequest, flexUserRoleValid],
        const user = await userFromSession(request, response);
        if (!!user && user.role !== "admin") {
          response.sendStatus(401).end();
          return;
        }

          role: "admin",
  app.get("/system/multi-user-mode", async (request, response) => {
  app.get("/system/logo", async function (request, response) {
  app.get("/system/pfp/:id", async function (request, response) {
    try {
      const { id } = request.params;
      const pfpPath = await determinePfpFilepath(id);
      if (!pfpPath) {
        response.sendStatus(204).end();
        return;
      }
      const { found, buffer, size, mime } = fetchPfp(pfpPath);
      if (!found) {
        response.sendStatus(204).end();

      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" });
  });
    [validatedRequest, flexUserRoleValid],

        const oldPfpFilename = normalizePath(userRecord.pfpFilename);
            `../storage/assets/pfp/${oldPfpFilename}`
    [validatedRequest, flexUserRoleValid],
        const oldPfpFilename = normalizePath(userRecord.pfpFilename);
            `../storage/assets/pfp/${oldPfpFilename}`
    [validatedRequest, flexUserRoleValid],
  app.get("/system/is-default-logo", async (request, response) => {
    [validatedRequest, flexUserRoleValid],
        if (["admin", "manager"].includes(user?.role)) {
  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" });
  });
    [validatedRequest, flexUserRoleValid],
    [validatedRequest, flexUserRoleValid],
    [validatedRequest, flexUserRoleValid],
    [validatedRequest, flexUserRoleValid],
        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);
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();
    }
  });
}

  convertToCSV,
  convertToJSON,
  convertToJSONL,
const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected");
    [validatedRequest, flexUserRoleValid],
    [validatedRequest],
    [validatedRequest],
    [validatedRequest],
    [validatedRequest, flexUserRoleValid],
  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();
      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 });
      response.status(200).json({ workspace });
    } catch (e) {
      console.log(e.message, e);
      response.sendStatus(500).end();
  });
    [validatedRequest],
    if (["admin", "manager"].includes(user.role)) return this.get(clause);
    if (["admin", "manager"].includes(user.role))

const ROLES = ["admin", "manager"];
async function strictMultiUserRoleValid(request, response, next) {
  const multiUserMode =
    response.locals?.multiUserMode ?? (await SystemSettings.isMultiUserMode());
  if (!multiUserMode) return response.sendStatus(401).end();
  const user =
    response.locals?.user ?? (await userFromSession(request, response));
  if (!ROLES.includes(user?.role)) return response.sendStatus(401).end();
  next();
async function flexUserRoleValid(request, response, next) {
  const multiUserMode =
    response.locals?.multiUserMode ?? (await SystemSettings.isMultiUserMode());
  if (!multiUserMode) {
    next();
    return;
  }
  const user =
    response.locals?.user ?? (await userFromSession(request, response));
  if (!ROLES.includes(user?.role)) return response.sendStatus(401).end();
  next();

Fixed code:
                element={<AdminRoute Component={GeneralApiKeys} />}
                <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"]}
                />
                  user={user}
                  flex={true}
                  allowedRole={["admin", "manager"]}
                  user={user}
                  flex={true}
                  allowedRole={["admin", "manager"]}
                  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}
                  flex={true}
                  allowedRole={["admin", "manager"]}
                    user={user}
                    allowedRole={["admin", "manager"]}
                    user={user}
                    allowedRole={["admin", "manager"]}
                    user={user}
                    allowedRole={["admin", "manager"]}
                    user={user}
                    allowedRole={["admin", "manager"]}
                    user={user}
                    flex={true}
                    allowedRole={["admin", "manager"]}
                    user={user}
                    flex={true}
                    allowedRole={["admin", "manager"]}
                    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}
                    flex={true}
                    allowedRole={["admin", "manager"]}
const Option = ({
  btnText,
  icon,
  href,
  flex = false,
  user = null,
  allowedRole = [],
}) => {

  // 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;
      headers: baseHeaders(),
      headers: baseHeaders(),
import showToast from "@/utils/toast";

const ModMap = {
  admin: ["admin", "manager", "default"],
  manager: ["manager", "default"],
  default: [],
};
  const canModify = ModMap[currUser?.role || "default"].includes(user.role);

    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 { 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 });
    }
          {canModify && (
          {currUser?.id !== user.id && canModify && (
    "Can view, create, and delete any workspaces and modify workspace-specific settings.",
    "Can create, update and invite new users to the instance.",
const {
  validRoleSelection,
  canModifyAdmin,
  validCanModify,
} = require("../utils/helpers/admin");
  ROLES,
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
        const currUser = await userFromSession(request, response);
        const roleValidation = validRoleSelection(currUser, newUserParams);

        if (!roleValidation.valid) {
          response
            .status(200)
            .json({ user: null, error: roleValidation.error });
          return;
        }

    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
        const currUser = await userFromSession(request, response);
        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;
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
        const currUser = await userFromSession(request, response);
        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;
        }

    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
    [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],
const { canModifyAdmin } = require("../../../utils/helpers/admin");
      const validAdminRoleModification = await canModifyAdmin(user, updates);
      if (!validAdminRoleModification.valid) {
        response
          .status(200)
          .json({ success: false, error: validAdminRoleModification.error });
        return;
const {
  ROLES,
  flexUserRoleValid,
} = require("../utils/middleware/multiUserProtected");
    [validatedRequest, flexUserRoleValid([ROLES.all])],
        if (multiUserMode(response) && user.role !== ROLES.admin) {
  ROLES,
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
const {
  flexUserRoleValid,
  ROLES,
} = require("../utils/middleware/multiUserProtected");
const {
  prepareWorkspaceChatsForExport,
  exportChatsAsType,
} = require("../utils/helpers/chat/convertTo");
    [validatedRequest, flexUserRoleValid([ROLES.admin])],
          role: ROLES.admin,
  app.get("/system/multi-user-mode", async (_, response) => {
  app.get("/system/logo", async function (_, response) {
  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;
        }
        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"));
      } catch (error) {
        console.error("Error processing the logo request:", error);
        response.status(500).json({ message: "Internal server error" });
  );
    [validatedRequest, flexUserRoleValid([ROLES.all])],
        const oldPfpFilename = userRecord.pfpFilename;
            `../storage/assets/pfp/${normalizePath(userRecord.pfpFilename)}`
    [validatedRequest, flexUserRoleValid([ROLES.all])],
        const oldPfpFilename = userRecord.pfpFilename;

            `../storage/assets/pfp/${normalizePath(oldPfpFilename)}`
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  app.get("/system/is-default-logo", async (_, response) => {
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
        if ([ROLES.admin, ROLES.manager].includes(user?.role)) {
  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" });
      }
  );
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.manager, ROLES.admin])],
        const chats = await prepareWorkspaceChatsForExport();
        const { contentType, data } = await exportChatsAsType(chats, type);
        response.setHeader("Content-Type", contentType);
        response.status(200).send(data);
  // Used for when a user in multi-user updates their own profile
  // from the UI.
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();
    }
  });
}

const {
  flexUserRoleValid,
  ROLES,
} = require("../utils/middleware/multiUserProtected");
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  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();
      }
  );
  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();
      }
  );
    [validatedRequest, flexUserRoleValid([ROLES.all])],
const { ROLES } = require("../utils/middleware/multiUserProtected");
    if ([ROLES.admin, ROLES.manager].includes(user.role))
      return this.get(clause);
    if ([ROLES.admin, ROLES.manager].includes(user.role))
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,
};
// 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,
};
const ROLES = {
  all: "<all>",
  admin: "admin",
  manager: "manager",
  default: "default",
};
const DEFAULT_ROLES = [ROLES.admin, ROLES.admin];
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 multiUserMode =
      response.locals?.multiUserMode ??
      (await SystemSettings.isMultiUserMode());
    if (!multiUserMode) return response.sendStatus(401).end();
    const user =
      response.locals?.user ?? (await userFromSession(request, response));
    if (allowedRoles.includes(user?.role)) {
      next();
      return;
    }
    return response.sendStatus(401).end();
  };
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;
    }
    // Bypass if not in multi-user mode
    const multiUserMode =
      response.locals?.multiUserMode ??
      (await SystemSettings.isMultiUserMode());
    if (!multiUserMode) {
      next();
      return;
    }
    const user =
      response.locals?.user ?? (await userFromSession(request, response));
    if (allowedRoles.includes(user?.role)) {
      next();
      return;
    }
    return response.sendStatus(401).end();
  };
  ROLES,