Enable the ability to disable the chat history UI ()

* Enable the ability to disable the chat history UI

* forgot files
This commit is contained in:
Timothy Carambat 2024-10-21 13:19:19 -07:00 committed by GitHub
parent e71392d83f
commit 0524aadf58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 438 additions and 272 deletions
docker
frontend/src
components
CanViewChatHistory
SettingsSidebar
models
pages/GeneralSettings
Chats
EmbedChats
server

View file

@ -279,4 +279,12 @@ GID='1000'
# AGENT_SERPLY_API_KEY=
#------ SearXNG ----------- https://github.com/searxng/searxng
# AGENT_SEARXNG_API_URL=
# AGENT_SEARXNG_API_URL=
###########################################
######## Other Configurations ############
###########################################
# Disable viewing chat history from the UI and frontend APIs.
# See https://docs.anythingllm.com/configuration#disable-view-chat-history for more information.
# DISABLE_VIEW_CHAT_HISTORY=1

View file

@ -0,0 +1,50 @@
import { useEffect, useState } from "react";
import { FullScreenLoader } from "@/components/Preloader";
import System from "@/models/system";
import paths from "@/utils/paths";
/**
* Protects the view from system set ups who cannot view chat history.
* If the user cannot view chat history, they are redirected to the home page.
* @param {React.ReactNode} children
*/
export function CanViewChatHistory({ children }) {
const { loading, viewable } = useCanViewChatHistory();
if (loading) return <FullScreenLoader />;
if (!viewable) {
window.location.href = paths.home();
return <FullScreenLoader />;
}
return <>{children}</>;
}
/**
* Provides the `viewable` state to the children.
* @returns {React.ReactNode}
*/
export function CanViewChatHistoryProvider({ children }) {
const { loading, viewable } = useCanViewChatHistory();
if (loading) return null;
return <>{children({ viewable })}</>;
}
/**
* Hook that fetches the can view chat history state from local storage or the system settings.
* @returns {Promise<{viewable: boolean, error: string | null}>}
*/
export function useCanViewChatHistory() {
const [loading, setLoading] = useState(true);
const [viewable, setViewable] = useState(false);
useEffect(() => {
async function fetchViewable() {
const { viewable } = await System.fetchCanViewChatHistory();
setViewable(viewable);
setLoading(false);
}
fetchViewable();
}, []);
return { loading, viewable };
}

View file

@ -149,17 +149,32 @@ function useIsExpanded({
return { isExpanded, setIsExpanded };
}
/**
* Checks if the child options are visible to the user.
* This hides the top level options if the child options are not visible
* for either the users permissions or the child options hidden prop is set to true by other means.
* If all child options return false for `isVisible` then the parent option will not be visible as well.
* @param {object} user - The user object.
* @param {array} childOptions - The child options.
* @returns {boolean} - True if the child options are visible, false otherwise.
*/
function hasVisibleOptions(user = null, childOptions = []) {
if (!Array.isArray(childOptions) || childOptions?.length === 0) return false;
function isVisible({ roles = [], user = null, flex = false }) {
function isVisible({
roles = [],
user = null,
flex = false,
hidden = false,
}) {
if (hidden) return false;
if (!flex && !roles.includes(user?.role)) return false;
if (flex && !!user && !roles.includes(user?.role)) return false;
return true;
}
return childOptions.some((opt) =>
isVisible({ roles: opt.roles, user, flex: opt.flex })
isVisible({ roles: opt.roles, user, flex: opt.flex, hidden: opt.hidden })
);
}

View file

@ -21,6 +21,7 @@ import { useTranslation } from "react-i18next";
import showToast from "@/utils/toast";
import System from "@/models/system";
import Option from "./MenuOption";
import { CanViewChatHistoryProvider } from "../CanViewChatHistory";
export default function SettingsSidebar() {
const { t } = useTranslation();
@ -208,151 +209,157 @@ function SupportEmail() {
}
const SidebarOptions = ({ user = null, t }) => (
<>
<Option
btnText={t("settings.ai-providers")}
icon={<Gear className="h-5 w-5 flex-shrink-0" />}
user={user}
childOptions={[
{
btnText: t("settings.llm"),
href: paths.settings.llmPreference(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.vector-database"),
href: paths.settings.vectorDatabase(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.embedder"),
href: paths.settings.embedder.modelPreference(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.text-splitting"),
href: paths.settings.embedder.chunkingPreference(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.voice-speech"),
href: paths.settings.audioPreference(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.transcription"),
href: paths.settings.transcriptionPreference(),
flex: true,
roles: ["admin"],
},
]}
/>
<Option
btnText={t("settings.admin")}
icon={<UserCircleGear className="h-5 w-5 flex-shrink-0" />}
user={user}
childOptions={[
{
btnText: t("settings.users"),
href: paths.settings.users(),
roles: ["admin", "manager"],
},
{
btnText: t("settings.workspaces"),
href: paths.settings.workspaces(),
roles: ["admin", "manager"],
},
{
btnText: t("settings.workspace-chats"),
href: paths.settings.chats(),
flex: true,
roles: ["admin", "manager"],
},
{
btnText: t("settings.invites"),
href: paths.settings.invites(),
roles: ["admin", "manager"],
},
]}
/>
<Option
btnText={t("settings.agent-skills")}
icon={<Robot className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.agentSkills()}
user={user}
flex={true}
roles={["admin"]}
/>
<Option
btnText={t("settings.customization")}
icon={<PencilSimpleLine className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.appearance()}
user={user}
flex={true}
roles={["admin", "manager"]}
/>
<Option
btnText={t("settings.tools")}
icon={<Toolbox className="h-5 w-5 flex-shrink-0" />}
user={user}
childOptions={[
{
btnText: t("settings.embed-chats"),
href: paths.settings.embedChats(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.embeds"),
href: paths.settings.embedSetup(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.event-logs"),
href: paths.settings.logs(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.api-keys"),
href: paths.settings.apiKeys(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.browser-extension"),
href: paths.settings.browserExtension(),
flex: true,
roles: ["admin", "manager"],
},
]}
/>
<Option
btnText={t("settings.security")}
icon={<Nut className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.security()}
user={user}
flex={true}
roles={["admin", "manager"]}
hidden={user?.role}
/>
<HoldToReveal key="exp_features">
<Option
btnText={t("settings.experimental-features")}
icon={<Flask className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.experimental()}
user={user}
flex={true}
roles={["admin"]}
/>
</HoldToReveal>
</>
<CanViewChatHistoryProvider>
{({ viewable: canViewChatHistory }) => (
<>
<Option
btnText={t("settings.ai-providers")}
icon={<Gear className="h-5 w-5 flex-shrink-0" />}
user={user}
childOptions={[
{
btnText: t("settings.llm"),
href: paths.settings.llmPreference(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.vector-database"),
href: paths.settings.vectorDatabase(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.embedder"),
href: paths.settings.embedder.modelPreference(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.text-splitting"),
href: paths.settings.embedder.chunkingPreference(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.voice-speech"),
href: paths.settings.audioPreference(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.transcription"),
href: paths.settings.transcriptionPreference(),
flex: true,
roles: ["admin"],
},
]}
/>
<Option
btnText={t("settings.admin")}
icon={<UserCircleGear className="h-5 w-5 flex-shrink-0" />}
user={user}
childOptions={[
{
btnText: t("settings.users"),
href: paths.settings.users(),
roles: ["admin", "manager"],
},
{
btnText: t("settings.workspaces"),
href: paths.settings.workspaces(),
roles: ["admin", "manager"],
},
{
hidden: !canViewChatHistory,
btnText: t("settings.workspace-chats"),
href: paths.settings.chats(),
flex: true,
roles: ["admin", "manager"],
},
{
btnText: t("settings.invites"),
href: paths.settings.invites(),
roles: ["admin", "manager"],
},
]}
/>
<Option
btnText={t("settings.agent-skills")}
icon={<Robot className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.agentSkills()}
user={user}
flex={true}
roles={["admin"]}
/>
<Option
btnText={t("settings.customization")}
icon={<PencilSimpleLine className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.appearance()}
user={user}
flex={true}
roles={["admin", "manager"]}
/>
<Option
btnText={t("settings.tools")}
icon={<Toolbox className="h-5 w-5 flex-shrink-0" />}
user={user}
childOptions={[
{
hidden: !canViewChatHistory,
btnText: t("settings.embed-chats"),
href: paths.settings.embedChats(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.embeds"),
href: paths.settings.embedSetup(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.event-logs"),
href: paths.settings.logs(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.api-keys"),
href: paths.settings.apiKeys(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.browser-extension"),
href: paths.settings.browserExtension(),
flex: true,
roles: ["admin", "manager"],
},
]}
/>
<Option
btnText={t("settings.security")}
icon={<Nut className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.security()}
user={user}
flex={true}
roles={["admin", "manager"]}
hidden={user?.role}
/>
<HoldToReveal key="exp_features">
<Option
btnText={t("settings.experimental-features")}
icon={<Flask className="h-5 w-5 flex-shrink-0" />}
href={paths.settings.experimental()}
user={user}
flex={true}
roles={["admin"]}
/>
</HoldToReveal>
</>
)}
</CanViewChatHistoryProvider>
);
function HoldToReveal({ children, holdForMs = 3_000 }) {

View file

@ -9,6 +9,7 @@ const System = {
footerIcons: "anythingllm_footer_links",
supportEmail: "anythingllm_support_email",
customAppName: "anythingllm_custom_app_name",
canViewChatHistory: "anythingllm_can_view_chat_history",
},
ping: async function () {
return await fetch(`${API_BASE}/ping`)
@ -675,6 +676,36 @@ const System = {
return false;
});
},
/**
* Fetches the can view chat history state from local storage or the system settings.
* Notice: This is an instance setting that cannot be changed via the UI and it is cached
* in local storage for 24 hours.
* @returns {Promise<{viewable: boolean, error: string | null}>}
*/
fetchCanViewChatHistory: async function () {
const cache = window.localStorage.getItem(
this.cacheKeys.canViewChatHistory
);
const { viewable, lastFetched } = cache
? safeJsonParse(cache, { viewable: false, lastFetched: 0 })
: { viewable: false, lastFetched: 0 };
// Since this is an instance setting that cannot be changed via the UI,
// we can cache it in local storage for a day and if the admin changes it,
// they should instruct the users to clear local storage.
if (typeof viewable === "boolean" && Date.now() - lastFetched < 8.64e7)
return { viewable, error: null };
const res = await System.keys();
const isViewable = res?.DisableViewChatHistory === false;
window.localStorage.setItem(
this.cacheKeys.canViewChatHistory,
JSON.stringify({ viewable: isViewable, lastFetched: Date.now() })
);
return { viewable: isViewable, error: null };
},
experimentalFeatures: {
liveSync: LiveDocumentSync,
agentPlugins: AgentPlugins,

View file

@ -11,6 +11,7 @@ import { CaretDown, Download, Sparkle, Trash } from "@phosphor-icons/react";
import { saveAs } from "file-saver";
import { useTranslation } from "react-i18next";
import paths from "@/utils/paths";
import { CanViewChatHistory } from "@/components/CanViewChatHistory";
const exportOptions = {
csv: {
@ -106,7 +107,8 @@ export default function WorkspaceChats() {
useEffect(() => {
async function fetchChats() {
const { chats: _chats, hasPages = false } = await System.chats(offset);
const { chats: _chats = [], hasPages = false } =
await System.chats(offset);
setChats(_chats);
setCanNext(hasPages);
setLoading(false);
@ -115,85 +117,87 @@ export default function WorkspaceChats() {
}, [offset]);
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
{t("recorded.title")}
</p>
<div className="relative">
<button
ref={openMenuButton}
onClick={toggleMenu}
className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:text-white text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Download size={18} weight="bold" />
{t("recorded.export")}
<CaretDown size={18} weight="bold" />
</button>
<div
ref={menuRef}
className={`${
showMenu ? "slide-down" : "slide-up hidden"
} z-20 w-fit rounded-lg absolute top-full right-0 bg-secondary mt-2 shadow-md`}
>
<div className="py-2">
{Object.entries(exportOptions).map(([key, data]) => (
<button
key={key}
onClick={() => {
handleDumpChats(key);
setShowMenu(false);
}}
className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147]"
>
{data.name}
</button>
))}
<CanViewChatHistory>
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
{t("recorded.title")}
</p>
<div className="relative">
<button
ref={openMenuButton}
onClick={toggleMenu}
className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:text-white text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Download size={18} weight="bold" />
{t("recorded.export")}
<CaretDown size={18} weight="bold" />
</button>
<div
ref={menuRef}
className={`${
showMenu ? "slide-down" : "slide-up hidden"
} z-20 w-fit rounded-lg absolute top-full right-0 bg-secondary mt-2 shadow-md`}
>
<div className="py-2">
{Object.entries(exportOptions).map(([key, data]) => (
<button
key={key}
onClick={() => {
handleDumpChats(key);
setShowMenu(false);
}}
className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147]"
>
{data.name}
</button>
))}
</div>
</div>
</div>
{chats.length > 0 && (
<>
<button
onClick={handleClearAllChats}
className="flex items-center gap-x-2 px-4 py-1 border hover:border-transparent border-white/40 text-white/40 rounded-lg bg-transparent hover:text-white text-xs font-semibold hover:bg-red-500 shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Trash size={18} weight="bold" />
Clear Chats
</button>
<a
href={paths.orderFineTune()}
className="flex items-center gap-x-2 px-4 py-1 border hover:border-transparent border-yellow-300 text-yellow-300/80 rounded-lg bg-transparent hover:text-white text-xs font-semibold hover:bg-yellow-300/75 shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Sparkle size={18} weight="bold" />
Order Fine-Tune Model
</a>
</>
)}
</div>
{chats.length > 0 && (
<>
<button
onClick={handleClearAllChats}
className="flex items-center gap-x-2 px-4 py-1 border hover:border-transparent border-white/40 text-white/40 rounded-lg bg-transparent hover:text-white text-xs font-semibold hover:bg-red-500 shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Trash size={18} weight="bold" />
Clear Chats
</button>
<a
href={paths.orderFineTune()}
className="flex items-center gap-x-2 px-4 py-1 border hover:border-transparent border-yellow-300 text-yellow-300/80 rounded-lg bg-transparent hover:text-white text-xs font-semibold hover:bg-yellow-300/75 shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Sparkle size={18} weight="bold" />
Order Fine-Tune Model
</a>
</>
)}
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
{t("recorded.description")}
</p>
</div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
{t("recorded.description")}
</p>
<ChatsContainer
loading={loading}
chats={chats}
setChats={setChats}
offset={offset}
setOffset={setOffset}
canNext={canNext}
t={t}
/>
</div>
<ChatsContainer
loading={loading}
chats={chats}
setChats={setChats}
offset={offset}
setOffset={setOffset}
canNext={canNext}
t={t}
/>
</div>
</div>
</div>
</CanViewChatHistory>
);
}

View file

@ -11,6 +11,7 @@ import { CaretDown, Download } from "@phosphor-icons/react";
import showToast from "@/utils/toast";
import { saveAs } from "file-saver";
import System from "@/models/system";
import { CanViewChatHistory } from "@/components/CanViewChatHistory";
const exportOptions = {
csv: {
@ -88,59 +89,61 @@ export default function EmbedChats() {
}, []);
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
{t("embed-chats.title")}
</p>
<div className="relative">
<button
ref={openMenuButton}
onClick={toggleMenu}
className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:text-white text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Download size={18} weight="bold" />
{t("embed-chats.export")}
<CaretDown size={18} weight="bold" />
</button>
<div
ref={menuRef}
className={`${
showMenu ? "slide-down" : "slide-up hidden"
} z-20 w-fit rounded-lg absolute top-full right-0 bg-secondary mt-2 shadow-md`}
>
<div className="py-2">
{Object.entries(exportOptions).map(([key, data]) => (
<button
key={key}
onClick={() => {
handleDumpChats(key);
setShowMenu(false);
}}
className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147]"
>
{data.name}
</button>
))}
<CanViewChatHistory>
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
{t("embed-chats.title")}
</p>
<div className="relative">
<button
ref={openMenuButton}
onClick={toggleMenu}
className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:text-white text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Download size={18} weight="bold" />
{t("embed-chats.export")}
<CaretDown size={18} weight="bold" />
</button>
<div
ref={menuRef}
className={`${
showMenu ? "slide-down" : "slide-up hidden"
} z-20 w-fit rounded-lg absolute top-full right-0 bg-secondary mt-2 shadow-md`}
>
<div className="py-2">
{Object.entries(exportOptions).map(([key, data]) => (
<button
key={key}
onClick={() => {
handleDumpChats(key);
setShowMenu(false);
}}
className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147]"
>
{data.name}
</button>
))}
</div>
</div>
</div>
</div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
{t("embed-chats.description")}
</p>
</div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
{t("embed-chats.description")}
</p>
<ChatsContainer />
</div>
<ChatsContainer />
</div>
</div>
</div>
</CanViewChatHistory>
);
}

View file

@ -268,4 +268,12 @@ TTS_PROVIDER="native"
# AGENT_SERPLY_API_KEY=
#------ SearXNG ----------- https://github.com/searxng/searxng
# AGENT_SEARXNG_API_URL=
# AGENT_SEARXNG_API_URL=
###########################################
######## Other Configurations ############
###########################################
# Disable viewing chat history from the UI and frontend APIs.
# See https://docs.anythingllm.com/configuration#disable-view-chat-history for more information.
# DISABLE_VIEW_CHAT_HISTORY=1

View file

@ -1,7 +1,6 @@
const { EmbedChats } = require("../models/embedChats");
const { EmbedConfig } = require("../models/embedConfig");
const { EventLogs } = require("../models/eventLogs");
const { Workspace } = require("../models/workspace");
const { reqBody, userFromSession } = require("../utils/http");
const { validEmbedConfigId } = require("../utils/middleware/embedMiddleware");
const {
@ -9,6 +8,9 @@ const {
ROLES,
} = require("../utils/middleware/multiUserProtected");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const {
chatHistoryViewable,
} = require("../utils/middleware/chatHistoryViewable");
function embedManagementEndpoints(app) {
if (!app) return;
@ -90,7 +92,7 @@ function embedManagementEndpoints(app) {
app.post(
"/embed/chats",
[validatedRequest, flexUserRoleValid([ROLES.admin])],
[chatHistoryViewable, validatedRequest, flexUserRoleValid([ROLES.admin])],
async (request, response) => {
try {
const { offset = 0, limit = 20 } = reqBody(request);

View file

@ -50,6 +50,9 @@ const {
const { SlashCommandPresets } = require("../models/slashCommandsPresets");
const { EncryptionManager } = require("../utils/EncryptionManager");
const { BrowserExtensionApiKey } = require("../models/browserExtensionApiKey");
const {
chatHistoryViewable,
} = require("../utils/middleware/chatHistoryViewable");
function systemEndpoints(app) {
if (!app) return;
@ -961,7 +964,11 @@ function systemEndpoints(app) {
app.post(
"/system/workspace-chats",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
[
chatHistoryViewable,
validatedRequest,
flexUserRoleValid([ROLES.admin, ROLES.manager]),
],
async (request, response) => {
try {
const { offset = 0, limit = 20 } = reqBody(request);
@ -1001,7 +1008,11 @@ function systemEndpoints(app) {
app.get(
"/system/export-chats",
[validatedRequest, flexUserRoleValid([ROLES.manager, ROLES.admin])],
[
chatHistoryViewable,
validatedRequest,
flexUserRoleValid([ROLES.manager, ROLES.admin]),
],
async (request, response) => {
try {
const { type = "jsonl", chatType = "workspace" } = request.query;

View file

@ -246,6 +246,13 @@ const SystemSettings = {
AgentSerplyApiKey: !!process.env.AGENT_SERPLY_API_KEY || null,
AgentSearXNGApiUrl: process.env.AGENT_SEARXNG_API_URL || null,
AgentTavilyApiKey: !!process.env.AGENT_TAVILY_API_KEY || null,
// --------------------------------------------------------
// Compliance Settings
// --------------------------------------------------------
// Disable View Chat History for the whole instance.
DisableViewChatHistory:
"DISABLE_VIEW_CHAT_HISTORY" in process.env || false,
};
},

View file

@ -886,6 +886,8 @@ function dumpENV() {
"ENABLE_HTTPS",
"HTTPS_CERT_PATH",
"HTTPS_KEY_PATH",
// Other Configuration Keys
"DISABLE_VIEW_CHAT_HISTORY",
];
// Simple sanitization of each value to prevent ENV injection via newline or quote escaping.

View file

@ -0,0 +1,18 @@
/**
* A simple middleware that validates that the chat history is viewable.
* via the `DISABLE_VIEW_CHAT_HISTORY` environment variable being set AT ALL.
* @param {Request} request - The request object.
* @param {Response} response - The response object.
* @param {NextFunction} next - The next function.
*/
function chatHistoryViewable(_request, response, next) {
if ("DISABLE_VIEW_CHAT_HISTORY" in process.env)
return response
.status(422)
.send("This feature has been disabled by the administrator.");
next();
}
module.exports = {
chatHistoryViewable,
};