Skip to content

PR #3347 - Default User File Upload #3638

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ aws_cf_deploy_anything_llm.json
yarn.lock
*.bak
.idea
docker-compose.override.yml
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ AnythingLLM divides your documents into objects called `workspaces`. A Workspace
- πŸ†• [**No-code AI Agent builder**](https://docs.anythingllm.com/agent-flows/overview)
- πŸ–ΌοΈ **Multi-modal support (both closed and open-source LLMs!)**
- [**Custom AI Agents**](https://docs.anythingllm.com/agent/custom/introduction)
- πŸ‘€ Multi-user instance support and permissioning _Docker version only_
- πŸ‘€ Multi-user instance support with granular permissions (including document upload control) _Docker version only_
- 🦾 Agents inside your workspace (browse the web, etc)
- πŸ’¬ [Custom Embeddable Chat widget for your website](https://github.com/Mintplex-Labs/anythingllm-embed/blob/main/README.md) _Docker version only_
- πŸ“– Multiple document type support (PDF, TXT, DOCX, etc)
Expand Down
4 changes: 4 additions & 0 deletions docker/docker-compose.override.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services:
anything-llm:
environment:
- MULTI_USER_MODE=true
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ export function DnDFileUploaderProvider({ workspace, children }) {
type: "attachment",
});
} else {
// If the user is a default user, we do not want to allow them to upload files.
if (!!user && user.role === "default") continue;
// If the user is a default user without upload permission, we do not want to allow them to upload files.
if (!!user && user.role === "default" && !user.canUploadDocuments)
continue;
newAccepted.push({
uid: v4(),
file,
Expand Down Expand Up @@ -147,8 +148,9 @@ export function DnDFileUploaderProvider({ workspace, children }) {
type: "attachment",
});
} else {
// If the user is a default user, we do not want to allow them to upload files.
if (!!user && user.role === "default") continue;
// If the user is a default user without upload permission, we do not want to allow them to upload files.
if (!!user && user.role === "default" && !user.canUploadDocuments)
continue;
newAccepted.push({
uid: v4(),
file,
Expand Down Expand Up @@ -220,7 +222,8 @@ export default function DnDFileUploaderWrapper({ children }) {
onDragLeave: () => setDragging(false),
});
const { user } = useUser();
const canUploadAll = !user || user?.role !== "default";
const canUploadAll =
!user || user?.role !== "default" || user?.canUploadDocuments;

return (
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { useTranslation } from "react-i18next";
export default function AttachItem() {
const { t } = useTranslation();
const { user } = useUser();
if (!!user && user.role === "default") return null;
if (!!user && user.role === "default" && !user.canUploadDocuments)
return null;

return (
<>
Expand Down
24 changes: 23 additions & 1 deletion frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React, { useState } from "react";
import { X } from "@phosphor-icons/react";
import Admin from "@/models/admin";
import { MessageLimitInput, RoleHintDisplay } from "../..";
import {
DocumentUploadPermission,
MessageLimitInput,
RoleHintDisplay,
} from "../..";
import { AUTH_USER } from "@/utils/constants";

export default function EditUserModal({ currentUser, user, closeModal }) {
Expand All @@ -11,6 +15,10 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
enabled: user.dailyMessageLimit !== null,
limit: user.dailyMessageLimit || 10,
});
const [documentUpload, setDocumentUpload] = useState({
enabled: user.canUploadDocuments || false,
limit: user.documentUploadLimit || 10,
});

const handleUpdate = async (e) => {
setError(null);
Expand All @@ -27,6 +35,14 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
data.dailyMessageLimit = null;
}

// Document upload permissions
data.canUploadDocuments = documentUpload.enabled;
if (documentUpload.enabled) {
data.documentUploadLimit = documentUpload.limit;
} else {
data.documentUploadLimit = null;
}

const { success, error } = await Admin.updateUser(user.id, data);
if (success) {
// Update local storage if we're editing our own user
Expand Down Expand Up @@ -147,6 +163,12 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
limit={messageLimit.limit}
updateState={setMessageLimit}
/>
<DocumentUploadPermission
role={role}
enabled={documentUpload.enabled}
limit={documentUpload.limit}
updateState={setDocumentUpload}
/>
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
</div>
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
Expand Down
62 changes: 62 additions & 0 deletions frontend/src/pages/Admin/Users/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,19 @@ function UsersContainer() {
const ROLE_HINT = {
default: [
"Can only send chats with workspaces they are added to by admin or managers.",
"Can upload documents if granted permission by an admin or manager.",
"Cannot modify any settings at all.",
],
manager: [
"Can view, create, and delete any workspaces and modify workspace-specific settings.",
"Can create, update and invite new users to the instance.",
"Can upload documents to any workspace.",
"Cannot modify LLM, vectorDB, embedding, or other connections.",
],
admin: [
"Highest user level privilege.",
"Can see and do everything across the system.",
"Can upload documents to any workspace.",
],
};

Expand Down Expand Up @@ -197,3 +200,62 @@ export function MessageLimitInput({ enabled, limit, updateState, role }) {
</div>
);
}

export function DocumentUploadPermission({
enabled,
limit,
updateState,
role,
}) {
if (role === "admin" || role === "manager") return null;
return (
<div className="mt-4 mb-8">
<div className="flex flex-col gap-y-1">
<div className="flex items-center gap-x-2">
<h2 className="text-base leading-6 font-bold text-white">
Can upload documents
</h2>
<label className="relative inline-flex cursor-pointer items-center">
<input
type="checkbox"
checked={enabled}
onChange={(e) => {
updateState((prev) => ({
...prev,
enabled: e.target.checked,
}));
}}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div>
<p className="text-xs leading-[18px] font-base text-white/60">
Allow this user to upload documents to workspaces they have access to.
</p>
</div>
{enabled && (
<div className="mt-4">
<label className="text-white text-sm font-semibold block mb-4">
Document upload limit
</label>
<div className="relative mt-2">
<input
type="number"
onScroll={(e) => e.target.blur()}
onChange={(e) => {
updateState({
enabled: true,
limit: Number(e?.target?.value || 0),
});
}}
value={limit}
min={1}
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
/>
</div>
</div>
)}
</div>
);
}
17 changes: 10 additions & 7 deletions server/endpoints/api/document/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const { Telemetry } = require("../../../models/telemetry");
const { validApiKey } = require("../../../utils/middleware/validApiKey");
const {
canUploadDocuments,
} = require("../../../utils/middleware/validatedRequest");
const { handleAPIFileUpload } = require("../../../utils/files/multer");
const {
viewLocalFiles,
Expand All @@ -25,7 +28,7 @@ function apiDocumentEndpoints(app) {

app.post(
"/v1/document/upload",
[validApiKey, handleAPIFileUpload],
[validApiKey, canUploadDocuments, handleAPIFileUpload],
async (request, response) => {
/*
#swagger.tags = ['Documents']
Expand Down Expand Up @@ -127,7 +130,7 @@ function apiDocumentEndpoints(app) {

app.post(
"/v1/document/upload/:folderName",
[validApiKey, handleAPIFileUpload],
[validApiKey, canUploadDocuments, handleAPIFileUpload],
async (request, response) => {
/*
#swagger.tags = ['Documents']
Expand Down Expand Up @@ -286,7 +289,7 @@ function apiDocumentEndpoints(app) {

app.post(
"/v1/document/upload-link",
[validApiKey],
[validApiKey, canUploadDocuments],
async (request, response) => {
/*
#swagger.tags = ['Documents']
Expand Down Expand Up @@ -383,7 +386,7 @@ function apiDocumentEndpoints(app) {

app.post(
"/v1/document/raw-text",
[validApiKey],
[validApiKey, canUploadDocuments],
async (request, response) => {
/*
#swagger.tags = ['Documents']
Expand Down Expand Up @@ -784,7 +787,7 @@ function apiDocumentEndpoints(app) {

app.post(
"/v1/document/create-folder",
[validApiKey],
[validApiKey, canUploadDocuments],
async (request, response) => {
/*
#swagger.tags = ['Documents']
Expand Down Expand Up @@ -850,7 +853,7 @@ function apiDocumentEndpoints(app) {

app.delete(
"/v1/document/remove-folder",
[validApiKey],
[validApiKey, canUploadDocuments],
async (request, response) => {
/*
#swagger.tags = ['Documents']
Expand Down Expand Up @@ -909,7 +912,7 @@ function apiDocumentEndpoints(app) {

app.post(
"/v1/document/move-files",
[validApiKey],
[validApiKey, canUploadDocuments],
async (request, response) => {
/*
#swagger.tags = ['Documents']
Expand Down
9 changes: 6 additions & 3 deletions server/endpoints/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ const {
flexUserRoleValid,
ROLES,
} = require("../utils/middleware/multiUserProtected");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const {
validatedRequest,
canUploadDocuments,
} = require("../utils/middleware/validatedRequest");
const fs = require("fs");
const path = require("path");

function documentEndpoints(app) {
if (!app) return;
app.post(
"/document/create-folder",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
[validatedRequest, canUploadDocuments],
async (request, response) => {
try {
const { name } = reqBody(request);
Expand Down Expand Up @@ -43,7 +46,7 @@ function documentEndpoints(app) {

app.post(
"/document/move-files",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
[validatedRequest, canUploadDocuments],
async (request, response) => {
try {
const { files } = reqBody(request);
Expand Down
27 changes: 9 additions & 18 deletions server/endpoints/workspaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ const { DocumentVectors } = require("../models/vectors");
const { WorkspaceChats } = require("../models/workspaceChats");
const { getVectorDbClass } = require("../utils/helpers");
const { handleFileUpload, handlePfpUpload } = require("../utils/files/multer");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const {
validatedRequest,
canUploadDocuments,
} = require("../utils/middleware/validatedRequest");
const { Telemetry } = require("../models/telemetry");
const {
flexUserRoleValid,
Expand Down Expand Up @@ -109,11 +112,7 @@ function workspaceEndpoints(app) {

app.post(
"/workspace/:slug/upload",
[
validatedRequest,
flexUserRoleValid([ROLES.admin, ROLES.manager]),
handleFileUpload,
],
[validatedRequest, canUploadDocuments, handleFileUpload],
async function (request, response) {
try {
const Collector = new CollectorApi();
Expand Down Expand Up @@ -159,7 +158,7 @@ function workspaceEndpoints(app) {

app.post(
"/workspace/:slug/upload-link",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
[validatedRequest, canUploadDocuments],
async (request, response) => {
try {
const Collector = new CollectorApi();
Expand Down Expand Up @@ -202,7 +201,7 @@ function workspaceEndpoints(app) {

app.post(
"/workspace/:slug/update-embeddings",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
[validatedRequest, canUploadDocuments],
async (request, response) => {
try {
const user = await userFromSession(request, response);
Expand Down Expand Up @@ -869,11 +868,7 @@ function workspaceEndpoints(app) {
/** Handles the uploading and embedding in one-call by uploading via drag-and-drop in chat container. */
app.post(
"/workspace/:slug/upload-and-embed",
[
validatedRequest,
flexUserRoleValid([ROLES.admin, ROLES.manager]),
handleFileUpload,
],
[validatedRequest, canUploadDocuments, handleFileUpload],
async function (request, response) {
try {
const { slug = null } = request.params;
Expand Down Expand Up @@ -947,11 +942,7 @@ function workspaceEndpoints(app) {

app.delete(
"/workspace/:slug/remove-and-unembed",
[
validatedRequest,
flexUserRoleValid([ROLES.admin, ROLES.manager]),
handleFileUpload,
],
[validatedRequest, canUploadDocuments, handleFileUpload],
async function (request, response) {
try {
const { slug = null } = request.params;
Expand Down
15 changes: 15 additions & 0 deletions server/models/documents.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const Document = {
docpath: path,
workspaceId: workspace.id,
metadata: JSON.stringify(metadata),
uploadedBy: userId ? Number(userId) : null,
};

const { vectorized, error } = await VectorDb.addDocumentToNamespace(
Expand Down Expand Up @@ -198,6 +199,20 @@ const Document = {
return 0;
}
},

countByUser: async function (userId) {
if (!userId) return 0;

try {
const count = await prisma.workspace_documents.count({
where: { uploadedBy: Number(userId) },
});
return count;
} catch (error) {
console.error("FAILED TO COUNT USER DOCUMENTS.", error.message);
return 0;
}
},
update: async function (id = null, data = {}) {
if (!id) throw new Error("No workspace document id provided for update");

Expand Down
Loading