mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2026-06-15 23:20:32 +03:00
Merge branch 'master' of github.com:Mintplex-Labs/anything-llm
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
18
server/utils/middleware/workspaceDeletionProtection.js
Normal file
18
server/utils/middleware/workspaceDeletionProtection.js
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user