Merge branch 'master' of github.com:Mintplex-Labs/anything-llm

This commit is contained in:
Timothy Carambat
2026-05-20 17:02:40 -07:00
13 changed files with 125 additions and 18 deletions

View File

@@ -408,6 +408,9 @@ GID='1000'
# See https://docs.anythingllm.com/configuration#disable-view-chat-history for more information.
# DISABLE_VIEW_CHAT_HISTORY=1
# Disable workspace deletion from the UI and APIs when this ENV is present with any value.
# WORKSPACE_DELETION_PROTECTION=1
# Enable simple SSO passthrough to pre-authenticate users from a third party service.
# See https://docs.anythingllm.com/configuration#simple-sso-passthrough for more information.
# SIMPLE_SSO_ENABLED=1

View File

@@ -3,7 +3,11 @@ import Admin from "@/models/admin";
import paths from "@/utils/paths";
import { LinkSimple, Trash } from "@phosphor-icons/react";
export default function WorkspaceRow({ workspace, users: _users }) {
export default function WorkspaceRow({
workspace,
users: _users,
deletionProtected = false,
}) {
const rowRef = useRef(null);
const handleDelete = async () => {
if (
@@ -25,7 +29,7 @@ export default function WorkspaceRow({ workspace, users: _users }) {
<th scope="row" className="px-6 whitespace-nowrap">
{workspace.name}
</th>
<td className="px-6 flex items-center">
<td className="px-6">
<a
href={paths.workspace.chat(workspace.slug)}
target="_blank"
@@ -44,13 +48,15 @@ export default function WorkspaceRow({ workspace, users: _users }) {
</a>
</td>
<td className="px-6">{workspace.createdAt}</td>
<td className="px-6 flex items-center gap-x-6 h-full mt-1">
<button
onClick={handleDelete}
className="text-xs font-medium text-white/80 light:text-black/80 hover:light:text-red-500 hover:text-red-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10"
>
<Trash className="h-5 w-5" />
</button>
<td className="px-6">
{!deletionProtected && (
<button
onClick={handleDelete}
className="text-xs font-medium text-white/80 light:text-black/80 hover:light:text-red-500 hover:text-red-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10"
>
<Trash className="h-5 w-5" />
</button>
)}
</td>
</tr>
</>

View File

@@ -5,6 +5,7 @@ import * as Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import { BookOpen } from "@phosphor-icons/react";
import Admin from "@/models/admin";
import System from "@/models/system";
import WorkspaceRow from "./WorkspaceRow";
import NewWorkspaceModal from "./NewWorkspaceModal";
import { useModal } from "@/hooks/useModal";
@@ -57,13 +58,18 @@ function WorkspacesContainer() {
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState([]);
const [workspaces, setWorkspaces] = useState([]);
const [deletionProtected, setDeletionProtected] = useState(false);
useEffect(() => {
async function fetchData() {
const _users = await Admin.users();
const _workspaces = await Admin.workspaces();
const [_users, _workspaces, _settings] = await Promise.all([
Admin.users(),
Admin.workspaces(),
System.keys(),
]);
setUsers(_users);
setWorkspaces(_workspaces);
setDeletionProtected(_settings?.WorkspaceDeletionProtection === true);
setLoading(false);
}
fetchData();
@@ -110,6 +116,7 @@ function WorkspacesContainer() {
key={workspace.id}
workspace={workspace}
users={users}
deletionProtected={deletionProtected}
/>
))}
</tbody>

View File

@@ -5,7 +5,7 @@ import paths from "@/utils/paths";
import { useTranslation } from "react-i18next";
import showToast from "@/utils/toast";
export default function DeleteWorkspace({ workspace }) {
export default function DeleteWorkspace({ workspace, visible = true }) {
const { slug } = useParams();
const [deleting, setDeleting] = useState(false);
const { t } = useTranslation();
@@ -32,6 +32,8 @@ export default function DeleteWorkspace({ workspace }) {
? (window.location = paths.home())
: window.location.reload();
};
if (!visible) return null;
return (
<div className="flex flex-col mt-10">
<label className="block input-label">{t("general.delete.title")}</label>

View File

@@ -7,7 +7,7 @@ import SuggestedChatMessages from "./SuggestedChatMessages";
import DeleteWorkspace from "./DeleteWorkspace";
import CTAButton from "@/components/lib/CTAButton";
export default function GeneralInfo({ slug }) {
export default function GeneralInfo({ slug, deletionProtected = false }) {
const [workspace, setWorkspace] = useState(null);
const [hasChanges, setHasChanges] = useState(false);
const [saving, setSaving] = useState(false);
@@ -64,7 +64,7 @@ export default function GeneralInfo({ slug }) {
/>
</form>
<SuggestedChatMessages slug={workspace.slug} />
<DeleteWorkspace workspace={workspace} />
<DeleteWorkspace workspace={workspace} visible={!deletionProtected} />
</div>
);
}

View File

@@ -49,6 +49,7 @@ function ShowWorkspaceChat() {
const { slug, tab } = useParams();
const { user } = useUser();
const [workspace, setWorkspace] = useState(null);
const [deletionProtected, setDeletionProtected] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
@@ -67,6 +68,7 @@ function ShowWorkspaceChat() {
vectorDB: _settings?.VectorDB,
suggestedMessages,
});
setDeletionProtected(_settings?.WorkspaceDeletionProtection === true);
setLoading(false);
}
getWorkspace();
@@ -117,7 +119,11 @@ function ShowWorkspaceChat() {
/>
</div>
<div className="px-16 py-6">
<TabContent slug={slug} workspace={workspace} />
<TabContent
slug={slug}
workspace={workspace}
deletionProtected={deletionProtected}
/>
</div>
</div>
</div>

View File

@@ -412,6 +412,9 @@ TTS_PROVIDER="native"
# See https://docs.anythingllm.com/configuration#disable-view-chat-history for more information.
# DISABLE_VIEW_CHAT_HISTORY=1
# Disable workspace deletion from the UI and APIs when this ENV is present with any value.
# WORKSPACE_DELETION_PROTECTION=1
# Enable simple SSO passthrough to pre-authenticate users from a third party service.
# See https://docs.anythingllm.com/configuration#simple-sso-passthrough for more information.
# SIMPLE_SSO_ENABLED=1

View File

@@ -0,0 +1,43 @@
const {
workspaceDeletionProtection,
} = require("../../../utils/middleware/workspaceDeletionProtection");
describe("workspaceDeletionProtection middleware", () => {
const originalValue = process.env.WORKSPACE_DELETION_PROTECTION;
const hadValue = "WORKSPACE_DELETION_PROTECTION" in process.env;
afterEach(() => {
if (hadValue) {
process.env.WORKSPACE_DELETION_PROTECTION = originalValue;
} else {
delete process.env.WORKSPACE_DELETION_PROTECTION;
}
});
it("calls next when WORKSPACE_DELETION_PROTECTION is not present", () => {
delete process.env.WORKSPACE_DELETION_PROTECTION;
const next = jest.fn();
workspaceDeletionProtection({}, {}, next);
expect(next).toHaveBeenCalledTimes(1);
});
it("returns 403 when WORKSPACE_DELETION_PROTECTION is present with any value", () => {
process.env.WORKSPACE_DELETION_PROTECTION = "1";
const next = jest.fn();
const response = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
workspaceDeletionProtection({}, response, next);
expect(next).not.toHaveBeenCalled();
expect(response.status).toHaveBeenCalledWith(403);
expect(response.json).toHaveBeenCalledWith({
success: false,
error: "Workspace deletion is blocked by the system administrator.",
});
});
});

View File

@@ -28,6 +28,9 @@ const ImportedPlugin = require("../utils/agents/imported");
const {
simpleSSOLoginDisabledMiddleware,
} = require("../utils/middleware/simpleSSOEnabled");
const {
workspaceDeletionProtection,
} = require("../utils/middleware/workspaceDeletionProtection");
function adminEndpoints(app) {
if (!app) return;
@@ -291,7 +294,11 @@ function adminEndpoints(app) {
app.delete(
"/admin/workspaces/:id",
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
[
validatedRequest,
strictMultiUserRoleValid([ROLES.admin, ROLES.manager]),
workspaceDeletionProtection,
],
async (request, response) => {
try {
const { id } = request.params;

View File

@@ -15,6 +15,9 @@ const {
} = require("../../../utils/helpers/chat/responses");
const { ApiChatHandler } = require("../../../utils/chats/apiChatHandler");
const { getModelTag } = require("../../utils");
const {
workspaceDeletionProtection,
} = require("../../../utils/middleware/workspaceDeletionProtection");
function apiWorkspaceEndpoints(app) {
if (!app) return;
@@ -221,7 +224,7 @@ function apiWorkspaceEndpoints(app) {
app.delete(
"/v1/workspace/:slug",
[validApiKey],
[validApiKey, workspaceDeletionProtection],
async (request, response) => {
/*
#swagger.tags = ['Workspaces']

View File

@@ -38,6 +38,9 @@ const { purgeDocument } = require("../utils/files/purgeDocument");
const { getModelTag } = require("./utils");
const { searchWorkspaceAndThreads } = require("../utils/helpers/search");
const { workspaceParsedFilesEndpoints } = require("./workspacesParsedFiles");
const {
workspaceDeletionProtection,
} = require("../utils/middleware/workspaceDeletionProtection");
function workspaceEndpoints(app) {
if (!app) return;
@@ -270,7 +273,11 @@ function workspaceEndpoints(app) {
app.delete(
"/workspace/:slug",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
[
validatedRequest,
flexUserRoleValid([ROLES.admin, ROLES.manager]),
workspaceDeletionProtection,
],
async (request, response) => {
try {
const { slug = "" } = request.params;

View File

@@ -544,6 +544,8 @@ const SystemSettings = {
// Disable View Chat History for the whole instance.
DisableViewChatHistory:
"DISABLE_VIEW_CHAT_HISTORY" in process.env || false,
WorkspaceDeletionProtection:
"WORKSPACE_DELETION_PROTECTION" in process.env || false,
// --------------------------------------------------------
// Simple SSO Settings

View File

@@ -0,0 +1,18 @@
/**
* A simple middleware that blocks workspace deletion routes when the
* `WORKSPACE_DELETION_PROTECTION` environment variable is set AT ALL.
* @param {Request} request - The request object.
* @param {Response} response - The response object.
* @param {NextFunction} next - The next function.
*/
function workspaceDeletionProtection(_request, response, next) {
if (!("WORKSPACE_DELETION_PROTECTION" in process.env)) return next();
return response.status(403).json({
success: false,
error: "Workspace deletion is blocked by the system administrator.",
});
}
module.exports = {
workspaceDeletionProtection,
};