diff --git a/.gitignore b/.gitignore index f4e4419320..c5fca46779 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ aws_cf_deploy_anything_llm.json yarn.lock *.bak .idea +docker-compose.override.yml diff --git a/README.md b/README.md index e109b6a0eb..0219a0e1ce 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docker/docker-compose.override.yml b/docker/docker-compose.override.yml new file mode 100644 index 0000000000..20f70f457d --- /dev/null +++ b/docker/docker-compose.override.yml @@ -0,0 +1,4 @@ +services: + anything-llm: + environment: + - MULTI_USER_MODE=true diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx index 32999e11c6..2fdf399d09 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx @@ -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, @@ -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, @@ -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 (
diff --git a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx index aaf1b658be..b7a7d10da4 100644 --- a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx @@ -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 }) { @@ -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); @@ -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 @@ -147,6 +163,12 @@ export default function EditUserModal({ currentUser, user, closeModal }) { limit={messageLimit.limit} updateState={setMessageLimit} /> + {error &&

Error: {error}

}
diff --git a/frontend/src/pages/Admin/Users/index.jsx b/frontend/src/pages/Admin/Users/index.jsx index 3759894764..7777e39b1b 100644 --- a/frontend/src/pages/Admin/Users/index.jsx +++ b/frontend/src/pages/Admin/Users/index.jsx @@ -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.", ], }; @@ -197,3 +200,62 @@ export function MessageLimitInput({ enabled, limit, updateState, role }) {
); } + +export function DocumentUploadPermission({ + enabled, + limit, + updateState, + role, +}) { + if (role === "admin" || role === "manager") return null; + return ( +
+
+
+

+ Can upload documents +

+ +
+

+ Allow this user to upload documents to workspaces they have access to. +

+
+ {enabled && ( +
+ +
+ 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" + /> +
+
+ )} +
+ ); +} diff --git a/server/endpoints/api/document/index.js b/server/endpoints/api/document/index.js index efac6a22d3..936aca4249 100644 --- a/server/endpoints/api/document/index.js +++ b/server/endpoints/api/document/index.js @@ -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, @@ -25,7 +28,7 @@ function apiDocumentEndpoints(app) { app.post( "/v1/document/upload", - [validApiKey, handleAPIFileUpload], + [validApiKey, canUploadDocuments, handleAPIFileUpload], async (request, response) => { /* #swagger.tags = ['Documents'] @@ -127,7 +130,7 @@ function apiDocumentEndpoints(app) { app.post( "/v1/document/upload/:folderName", - [validApiKey, handleAPIFileUpload], + [validApiKey, canUploadDocuments, handleAPIFileUpload], async (request, response) => { /* #swagger.tags = ['Documents'] @@ -286,7 +289,7 @@ function apiDocumentEndpoints(app) { app.post( "/v1/document/upload-link", - [validApiKey], + [validApiKey, canUploadDocuments], async (request, response) => { /* #swagger.tags = ['Documents'] @@ -383,7 +386,7 @@ function apiDocumentEndpoints(app) { app.post( "/v1/document/raw-text", - [validApiKey], + [validApiKey, canUploadDocuments], async (request, response) => { /* #swagger.tags = ['Documents'] @@ -784,7 +787,7 @@ function apiDocumentEndpoints(app) { app.post( "/v1/document/create-folder", - [validApiKey], + [validApiKey, canUploadDocuments], async (request, response) => { /* #swagger.tags = ['Documents'] @@ -850,7 +853,7 @@ function apiDocumentEndpoints(app) { app.delete( "/v1/document/remove-folder", - [validApiKey], + [validApiKey, canUploadDocuments], async (request, response) => { /* #swagger.tags = ['Documents'] @@ -909,7 +912,7 @@ function apiDocumentEndpoints(app) { app.post( "/v1/document/move-files", - [validApiKey], + [validApiKey, canUploadDocuments], async (request, response) => { /* #swagger.tags = ['Documents'] diff --git a/server/endpoints/document.js b/server/endpoints/document.js index e4c311aee5..9ff0afe1e3 100644 --- a/server/endpoints/document.js +++ b/server/endpoints/document.js @@ -5,7 +5,10 @@ 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"); @@ -13,7 +16,7 @@ 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); @@ -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); diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index 95fe61b49a..907ede56e5 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -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, @@ -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(); @@ -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(); @@ -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); @@ -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; @@ -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; diff --git a/server/models/documents.js b/server/models/documents.js index 81c2dd9a79..26516548ee 100644 --- a/server/models/documents.js +++ b/server/models/documents.js @@ -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( @@ -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"); diff --git a/server/models/user.js b/server/models/user.js index 35e8271bcf..326a9211b5 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -22,6 +22,8 @@ const User = { "role", "suspended", "dailyMessageLimit", + "documentUploadLimit", + "canUploadDocuments", "bio", ], validations: { @@ -55,6 +57,19 @@ const User = { } return limit; }, + documentUploadLimit: (documentUploadLimit = 10) => { + if (documentUploadLimit === null) return null; + const limit = Number(documentUploadLimit); + if (isNaN(limit) || limit < 1) { + throw new Error( + "Document upload limit must be null or a number greater than or equal to 1" + ); + } + return limit; + }, + canUploadDocuments: (canUpload = false) => { + return Boolean(canUpload); + }, bio: (bio = "") => { if (!bio || typeof bio !== "string") return ""; if (bio.length > 1000) @@ -68,7 +83,10 @@ const User = { case "suspended": return Number(Boolean(value)); case "dailyMessageLimit": + case "documentUploadLimit": return value === null ? null : Number(value); + case "canUploadDocuments": + return Boolean(value); default: return String(value); } @@ -323,6 +341,32 @@ const User = { return currentChatCount < user.dailyMessageLimit; }, + + /** + * Check if a user can upload documents based on their permissions and document upload limit. + * Admin and Manager roles can always upload documents regardless of the canUploadDocuments flag. + * @param {User} user The user object record. + * @returns {Promise} True if the user can upload documents, false otherwise. + */ + canUploadDocument: async function (user) { + const { ROLES } = require("../utils/middleware/multiUserProtected"); + if (!user) return false; + + // Admin and Manager can always upload + if (user.role === ROLES.admin || user.role === ROLES.manager) return true; + + // For regular users, check the permission flag + if (!user.canUploadDocuments) return false; + + // If no upload limit is set, check only the permission flag + if (user.documentUploadLimit === null) return true; + + // Check against the document upload limit + const { Document } = require("./documents"); + const currentDocumentCount = await Document.countByUser(user.id); + + return currentDocumentCount < user.documentUploadLimit; + }, }; module.exports = { User }; diff --git a/server/prisma/migrations/20250413101107_add_document_upload_permissions/migration.sql b/server/prisma/migrations/20250413101107_add_document_upload_permissions/migration.sql new file mode 100644 index 0000000000..42bcf6c9e7 --- /dev/null +++ b/server/prisma/migrations/20250413101107_add_document_upload_permissions/migration.sql @@ -0,0 +1,44 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_users" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "username" TEXT, + "password" TEXT NOT NULL, + "pfpFilename" TEXT, + "role" TEXT NOT NULL DEFAULT 'default', + "suspended" INTEGER NOT NULL DEFAULT 0, + "seen_recovery_codes" BOOLEAN DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "dailyMessageLimit" INTEGER, + "documentUploadLimit" INTEGER DEFAULT 10, + "canUploadDocuments" BOOLEAN NOT NULL DEFAULT false, + "bio" TEXT DEFAULT '' +); +INSERT INTO "new_users" ("bio", "createdAt", "dailyMessageLimit", "id", "lastUpdatedAt", "password", "pfpFilename", "role", "seen_recovery_codes", "suspended", "username") SELECT "bio", "createdAt", "dailyMessageLimit", "id", "lastUpdatedAt", "password", "pfpFilename", "role", "seen_recovery_codes", "suspended", "username" FROM "users"; +DROP TABLE "users"; +ALTER TABLE "new_users" RENAME TO "users"; +CREATE UNIQUE INDEX "users_username_key" ON "users"("username"); +CREATE TABLE "new_workspace_documents" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "docId" TEXT NOT NULL, + "filename" TEXT NOT NULL, + "docpath" TEXT NOT NULL, + "workspaceId" INTEGER NOT NULL, + "metadata" TEXT, + "pinned" BOOLEAN DEFAULT false, + "watched" BOOLEAN DEFAULT false, + "uploadedBy" INTEGER, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "workspace_documents_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "workspace_documents_uploadedBy_fkey" FOREIGN KEY ("uploadedBy") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_workspace_documents" ("createdAt", "docId", "docpath", "filename", "id", "lastUpdatedAt", "metadata", "pinned", "watched", "workspaceId") SELECT "createdAt", "docId", "docpath", "filename", "id", "lastUpdatedAt", "metadata", "pinned", "watched", "workspaceId" FROM "workspace_documents"; +DROP TABLE "workspace_documents"; +ALTER TABLE "new_workspace_documents" RENAME TO "workspace_documents"; +CREATE UNIQUE INDEX "workspace_documents_docId_key" ON "workspace_documents"("docId"); +CREATE INDEX "workspace_documents_uploadedBy_idx" ON "workspace_documents"("uploadedBy"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/server/prisma/migrations/migration_lock.toml b/server/prisma/migrations/migration_lock.toml index e5e5c4705a..2a5a44419d 100644 --- a/server/prisma/migrations/migration_lock.toml +++ b/server/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "sqlite" \ No newline at end of file +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index fb1dae19bc..698bcfe421 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -32,10 +32,14 @@ model workspace_documents { metadata String? pinned Boolean? @default(false) watched Boolean? @default(false) + uploadedBy Int? createdAt DateTime @default(now()) lastUpdatedAt DateTime @default(now()) workspace workspaces @relation(fields: [workspaceId], references: [id]) + user users? @relation("UploadedDocuments", fields: [uploadedBy], references: [id], onDelete: SetNull) document_sync_queues document_sync_queues? + + @@index([uploadedBy]) } model invites { @@ -68,6 +72,8 @@ model users { createdAt DateTime @default(now()) lastUpdatedAt DateTime @default(now()) dailyMessageLimit Int? + documentUploadLimit Int? @default(10) + canUploadDocuments Boolean @default(false) bio String? @default("") workspace_chats workspace_chats[] workspace_users workspace_users[] @@ -81,6 +87,7 @@ model users { browser_extension_api_keys browser_extension_api_keys[] temporary_auth_tokens temporary_auth_tokens[] system_prompt_variables system_prompt_variables[] + uploaded_documents workspace_documents[] @relation("UploadedDocuments") } model recovery_codes { diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json index 94a1d71e39..858bdfbfc2 100644 --- a/server/swagger/openapi.json +++ b/server/swagger/openapi.json @@ -875,6 +875,9 @@ } } }, + "401": { + "description": "Unauthorized" + }, "403": { "description": "Forbidden", "content": { @@ -965,6 +968,9 @@ } } }, + "401": { + "description": "Unauthorized" + }, "403": { "description": "Forbidden", "content": { @@ -1056,6 +1062,9 @@ } } }, + "401": { + "description": "Unauthorized" + }, "403": { "description": "Forbidden", "content": { @@ -1129,6 +1138,9 @@ } } }, + "401": { + "description": "Unauthorized" + }, "403": { "description": "Forbidden", "content": { @@ -1497,6 +1509,9 @@ } } }, + "401": { + "description": "Unauthorized" + }, "403": { "description": "Forbidden", "content": { @@ -1554,6 +1569,9 @@ } } }, + "401": { + "description": "Unauthorized" + }, "403": { "description": "Forbidden", "content": { @@ -1614,6 +1632,9 @@ } } }, + "401": { + "description": "Unauthorized" + }, "403": { "description": "Forbidden", "content": { diff --git a/server/utils/middleware/USER_PERMISSIONS.md b/server/utils/middleware/USER_PERMISSIONS.md new file mode 100644 index 0000000000..f3614f4c3c --- /dev/null +++ b/server/utils/middleware/USER_PERMISSIONS.md @@ -0,0 +1,71 @@ +# User Permissions in AnythingLLM + +AnythingLLM supports a powerful role-based permission system when running in multi-user mode. This document explains the different permission types available and how to manage them. + +## Role Types + +Users in AnythingLLM can have one of the following roles: + +- **Admin**: Full system access, can manage all users, workspaces, and settings +- **Manager**: Can manage workspaces and upload documents +- **Default**: Regular user with limited permissions + +## Document Upload Permissions + +By default, only admin and manager roles can upload documents to workspaces. However, AnythingLLM supports granting document upload permissions to regular users. + +### Enabling Document Upload for Regular Users + +As an admin, you can enable document upload permissions for regular users: + +1. Navigate to the Admin panel +2. Select "Users" from the sidebar +3. Click the edit icon next to a user with the "default" role +4. In the user edit modal, you'll see a "Document Upload Permissions" section +5. Toggle "Can upload documents" to enable this permission +6. Set a document upload limit (maximum number of documents the user can upload) +7. Save the changes + +### Document Upload Quota + +When enabling document upload for a regular user, you can set a quota to limit the maximum number of documents they can upload: + +- Set a specific number (e.g., 10, 50, 100) +- Leave empty for unlimited uploads (same as admin/manager) + +### Technical Implementation + +The document upload permission system consists of: + +1. Database fields in the User model: + + - `canUploadDocuments` (boolean): Permission flag + - `documentUploadLimit` (integer): Maximum number of documents this user can upload + +2. Middleware to enforce permissions: + + - `canUploadDocuments` middleware checks both the permission flag and quota + +3. Tracking of document ownership: + - Each document is linked to the user who uploaded it + - A count is maintained to enforce the quota + +### API Endpoints + +The document upload permission is enforced on all document-related endpoints: + +- `/workspace/:slug/upload-and-embed` +- `/workspace/:slug/upload` +- `/workspace/:slug/upload-link` +- `/workspace/:slug/update-embeddings` +- `/workspace/:slug/remove-and-unembed` + +## Managing Permissions + +Only administrators can modify user permissions. The permission settings are found in the user management section of the admin panel. + +## Security Considerations + +- Even with document upload permissions, regular users can only upload to workspaces they have access to +- Document upload permissions do not grant workspace creation/management permissions +- All uploaded documents go through the same content validation process regardless of user role diff --git a/server/utils/middleware/validatedRequest.js b/server/utils/middleware/validatedRequest.js index f78709de21..9a91559f6c 100644 --- a/server/utils/middleware/validatedRequest.js +++ b/server/utils/middleware/validatedRequest.js @@ -106,6 +106,39 @@ async function validateMultiUserRequest(request, response, next) { next(); } +/** + * Check if the authenticated user has permission to upload documents. + * This middleware should be used after validatedRequest. + * @returns {function} + */ +async function canUploadDocuments(request, response, next) { + const multiUserMode = response.locals?.multiUserMode; + if (!multiUserMode) { + next(); + return; + } + + const user = response.locals?.user; + if (!user) { + return response.status(401).json({ + success: false, + error: "Authentication required", + }); + } + + const canUpload = await User.canUploadDocument(user); + if (!canUpload) { + return response.status(403).json({ + success: false, + error: + "You don't have permission to upload documents or have reached your upload limit", + }); + } + + next(); +} + module.exports = { validatedRequest, + canUploadDocuments, };