(
href: paths.settings.invites(),
roles: ["admin", "manager"],
},
- {
- btnText: t("settings.system"),
- href: paths.settings.system(),
- roles: ["admin", "manager"],
- },
]}
/>
- Username must be only contain lowercase letters, numbers,
+ Username must only contain lowercase letters, numbers,
underscores, and hyphens with no spaces
diff --git a/frontend/src/pages/Admin/System/index.jsx b/frontend/src/pages/Admin/System/index.jsx
deleted file mode 100644
index dba1872d3e..0000000000
--- a/frontend/src/pages/Admin/System/index.jsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import { useEffect, useState } from "react";
-import Sidebar from "@/components/SettingsSidebar";
-import { isMobile } from "react-device-detect";
-import Admin from "@/models/admin";
-import showToast from "@/utils/toast";
-import CTAButton from "@/components/lib/CTAButton";
-
-export default function AdminSystem() {
- const [saving, setSaving] = useState(false);
- const [hasChanges, setHasChanges] = useState(false);
- const [messageLimit, setMessageLimit] = useState({
- enabled: false,
- limit: 10,
- });
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- setSaving(true);
- await Admin.updateSystemPreferences({
- limit_user_messages: messageLimit.enabled,
- message_limit: messageLimit.limit,
- });
- setSaving(false);
- setHasChanges(false);
- showToast("System preferences updated successfully.", "success");
- };
-
- useEffect(() => {
- async function fetchSettings() {
- const settings = (await Admin.systemPreferences())?.settings;
- if (!settings) return;
- setMessageLimit({
- enabled: settings.limit_user_messages,
- limit: settings.message_limit,
- });
- }
- fetchSettings();
- }, []);
-
- return (
-
- );
-}
diff --git a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx
index 3af7ebd4a2..ebca2debb1 100644
--- a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx
+++ b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx
@@ -2,11 +2,15 @@ import React, { useState } from "react";
import { X } from "@phosphor-icons/react";
import Admin from "@/models/admin";
import { userFromStorage } from "@/utils/request";
-import { RoleHintDisplay } from "..";
+import { MessageLimitInput, RoleHintDisplay } from "..";
export default function NewUserModal({ closeModal }) {
const [error, setError] = useState(null);
const [role, setRole] = useState("default");
+ const [messageLimit, setMessageLimit] = useState({
+ enabled: false,
+ limit: 10,
+ });
const handleCreate = async (e) => {
setError(null);
@@ -14,6 +18,8 @@ export default function NewUserModal({ closeModal }) {
const data = {};
const form = new FormData(e.target);
for (var [key, value] of form.entries()) data[key] = value;
+ data.dailyMessageLimit = messageLimit.enabled ? messageLimit.limit : null;
+
const { user, error } = await Admin.newUser(data);
if (!!user) window.location.reload();
setError(error);
@@ -58,13 +64,13 @@ export default function NewUserModal({ closeModal }) {
pattern="^[a-z0-9_-]+$"
onInvalid={(e) =>
e.target.setCustomValidity(
- "Username must be only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
+ "Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
)
}
onChange={(e) => e.target.setCustomValidity("")}
/>
- Username must be only contain lowercase letters, numbers,
+ Username must only contain lowercase letters, numbers,
underscores, and hyphens with no spaces
@@ -110,6 +116,12 @@ export default function NewUserModal({ closeModal }) {
+
{error && Error: {error}
}
After creating a user they will need to login with their initial
diff --git a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx
index ec234c2f41..b6c30a852f 100644
--- a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx
+++ b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx
@@ -1,11 +1,15 @@
import React, { useState } from "react";
import { X } from "@phosphor-icons/react";
import Admin from "@/models/admin";
-import { RoleHintDisplay } from "../..";
+import { MessageLimitInput, RoleHintDisplay } from "../..";
export default function EditUserModal({ currentUser, user, closeModal }) {
const [role, setRole] = useState(user.role);
const [error, setError] = useState(null);
+ const [messageLimit, setMessageLimit] = useState({
+ enabled: user.dailyMessageLimit !== null,
+ limit: user.dailyMessageLimit || 10,
+ });
const handleUpdate = async (e) => {
setError(null);
@@ -16,6 +20,12 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
if (!value || value === null) continue;
data[key] = value;
}
+ if (messageLimit.enabled) {
+ data.dailyMessageLimit = messageLimit.limit;
+ } else {
+ data.dailyMessageLimit = null;
+ }
+
const { success, error } = await Admin.updateUser(user.id, data);
if (success) window.location.reload();
setError(error);
@@ -58,7 +68,7 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
autoComplete="off"
/>
- Username must be only contain lowercase letters, numbers,
+ Username must only contain lowercase letters, numbers,
underscores, and hyphens with no spaces
@@ -103,6 +113,12 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
+
{error && Error: {error}
}
diff --git a/frontend/src/pages/Admin/Users/index.jsx b/frontend/src/pages/Admin/Users/index.jsx
index 408e794aae..ed9f8b3f14 100644
--- a/frontend/src/pages/Admin/Users/index.jsx
+++ b/frontend/src/pages/Admin/Users/index.jsx
@@ -135,3 +135,58 @@ export function RoleHintDisplay({ role }) {
);
}
+
+export function MessageLimitInput({ enabled, limit, updateState, role }) {
+ if (role === "admin") return null;
+ return (
+
+
+
+
+ Limit messages per day
+
+
+ {
+ updateState((prev) => ({
+ ...prev,
+ enabled: e.target.checked,
+ }));
+ }}
+ className="peer sr-only"
+ />
+
+
+
+
+ Restrict this user to a number of successful queries or chats within a
+ 24 hour window.
+
+
+ {enabled && (
+
+
+ Message limit per day
+
+
+ e.target.blur()}
+ onChange={(e) => {
+ updateState({
+ enabled: true,
+ limit: Number(e?.target?.value || 0),
+ });
+ }}
+ value={limit}
+ min={1}
+ className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-60 p-2.5"
+ />
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js
index 28d36025cc..554cc4599a 100644
--- a/frontend/src/utils/paths.js
+++ b/frontend/src/utils/paths.js
@@ -80,9 +80,6 @@ export default {
return `/fine-tuning`;
},
settings: {
- system: () => {
- return `/settings/system-preferences`;
- },
users: () => {
return `/settings/users`;
},
diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js
index 994c8e4165..cf3c310d85 100644
--- a/server/endpoints/admin.js
+++ b/server/endpoints/admin.js
@@ -347,14 +347,6 @@ function adminEndpoints(app) {
: await SystemSettings.get({ label });
switch (label) {
- case "limit_user_messages":
- requestedSettings[label] = setting?.value === "true";
- break;
- case "message_limit":
- requestedSettings[label] = setting?.value
- ? Number(setting.value)
- : 10;
- break;
case "footer_data":
requestedSettings[label] = setting?.value ?? JSON.stringify([]);
break;
@@ -422,13 +414,6 @@ function adminEndpoints(app) {
try {
const embedder = getEmbeddingEngineSelection();
const settings = {
- limit_user_messages:
- (await SystemSettings.get({ label: "limit_user_messages" }))
- ?.value === "true",
- message_limit:
- Number(
- (await SystemSettings.get({ label: "message_limit" }))?.value
- ) || 10,
footer_data:
(await SystemSettings.get({ label: "footer_data" }))?.value ||
JSON.stringify([]),
diff --git a/server/endpoints/api/admin/index.js b/server/endpoints/api/admin/index.js
index 11c9d1c116..c165a9cc02 100644
--- a/server/endpoints/api/admin/index.js
+++ b/server/endpoints/api/admin/index.js
@@ -595,56 +595,6 @@ function apiAdminEndpoints(app) {
}
);
- app.get("/v1/admin/preferences", [validApiKey], async (request, response) => {
- /*
- #swagger.tags = ['Admin']
- #swagger.description = 'Show all multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.'
- #swagger.responses[200] = {
- content: {
- "application/json": {
- schema: {
- type: 'object',
- example: {
- settings: {
- limit_user_messages: false,
- message_limit: 10,
- }
- }
- }
- }
- }
- }
- #swagger.responses[403] = {
- schema: {
- "$ref": "#/definitions/InvalidAPIKey"
- }
- }
- #swagger.responses[401] = {
- description: "Instance is not in Multi-User mode. Method denied",
- }
- */
- try {
- if (!multiUserMode(response)) {
- response.sendStatus(401).end();
- return;
- }
-
- const settings = {
- limit_user_messages:
- (await SystemSettings.get({ label: "limit_user_messages" }))
- ?.value === "true",
- message_limit:
- Number(
- (await SystemSettings.get({ label: "message_limit" }))?.value
- ) || 10,
- };
- response.status(200).json({ settings });
- } catch (e) {
- console.error(e);
- response.sendStatus(500).end();
- }
- });
-
app.post(
"/v1/admin/preferences",
[validApiKey],
@@ -658,8 +608,7 @@ function apiAdminEndpoints(app) {
content: {
"application/json": {
example: {
- limit_user_messages: true,
- message_limit: 5,
+ support_email: "support@example.com",
}
}
}
diff --git a/server/endpoints/chat.js b/server/endpoints/chat.js
index 64beefeb6a..7e8a72b618 100644
--- a/server/endpoints/chat.js
+++ b/server/endpoints/chat.js
@@ -1,8 +1,6 @@
const { v4: uuidv4 } = require("uuid");
const { reqBody, userFromSession, multiUserMode } = require("../utils/http");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
-const { WorkspaceChats } = require("../models/workspaceChats");
-const { SystemSettings } = require("../models/systemSettings");
const { Telemetry } = require("../models/telemetry");
const { streamChatWithWorkspace } = require("../utils/chats/stream");
const {
@@ -16,6 +14,7 @@ const {
} = require("../utils/middleware/validWorkspace");
const { writeResponseChunk } = require("../utils/helpers/chat/responses");
const { WorkspaceThread } = require("../models/workspaceThread");
+const { User } = require("../models/user");
const truncate = require("truncate");
function chatEndpoints(app) {
@@ -48,39 +47,16 @@ function chatEndpoints(app) {
response.setHeader("Connection", "keep-alive");
response.flushHeaders();
- if (multiUserMode(response) && user.role !== ROLES.admin) {
- const limitMessagesSetting = await SystemSettings.get({
- label: "limit_user_messages",
+ if (multiUserMode(response) && !(await User.canSendChat(user))) {
+ writeResponseChunk(response, {
+ id: uuidv4(),
+ type: "abort",
+ textResponse: null,
+ sources: [],
+ close: true,
+ error: `You have met your maximum 24 hour chat quota of ${user.dailyMessageLimit} chats. Try again later.`,
});
- const limitMessages = limitMessagesSetting?.value === "true";
-
- if (limitMessages) {
- const messageLimitSetting = await SystemSettings.get({
- label: "message_limit",
- });
- const systemLimit = Number(messageLimitSetting?.value);
-
- if (!!systemLimit) {
- const currentChatCount = await WorkspaceChats.count({
- user_id: user.id,
- createdAt: {
- gte: new Date(new Date() - 24 * 60 * 60 * 1000),
- },
- });
-
- if (currentChatCount >= systemLimit) {
- writeResponseChunk(response, {
- id: uuidv4(),
- type: "abort",
- textResponse: null,
- sources: [],
- close: true,
- error: `You have met your maximum 24 hour chat quota of ${systemLimit} chats set by the instance administrators. Try again later.`,
- });
- return;
- }
- }
- }
+ return;
}
await streamChatWithWorkspace(
@@ -157,41 +133,16 @@ function chatEndpoints(app) {
response.setHeader("Connection", "keep-alive");
response.flushHeaders();
- if (multiUserMode(response) && user.role !== ROLES.admin) {
- const limitMessagesSetting = await SystemSettings.get({
- label: "limit_user_messages",
+ if (multiUserMode(response) && !(await User.canSendChat(user))) {
+ writeResponseChunk(response, {
+ id: uuidv4(),
+ type: "abort",
+ textResponse: null,
+ sources: [],
+ close: true,
+ error: `You have met your maximum 24 hour chat quota of ${user.dailyMessageLimit} chats. Try again later.`,
});
- const limitMessages = limitMessagesSetting?.value === "true";
-
- if (limitMessages) {
- const messageLimitSetting = await SystemSettings.get({
- label: "message_limit",
- });
- const systemLimit = Number(messageLimitSetting?.value);
-
- if (!!systemLimit) {
- // Chat qty includes all threads because any user can freely
- // create threads and would bypass this rule.
- const currentChatCount = await WorkspaceChats.count({
- user_id: user.id,
- createdAt: {
- gte: new Date(new Date() - 24 * 60 * 60 * 1000),
- },
- });
-
- if (currentChatCount >= systemLimit) {
- writeResponseChunk(response, {
- id: uuidv4(),
- type: "abort",
- textResponse: null,
- sources: [],
- close: true,
- error: `You have met your maximum 24 hour chat quota of ${systemLimit} chats set by the instance administrators. Try again later.`,
- });
- return;
- }
- }
- }
+ return;
}
await streamChatWithWorkspace(
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index ae807fe2ba..ccdb50ec85 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -490,8 +490,6 @@ function systemEndpoints(app) {
await SystemSettings._updateSettings({
multi_user_mode: true,
- limit_user_messages: false,
- message_limit: 25,
});
await BrowserExtensionApiKey.migrateApiKeysToMultiUser(user.id);
diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js
index 8e3a617675..0c67bf2f41 100644
--- a/server/models/systemSettings.js
+++ b/server/models/systemSettings.js
@@ -16,8 +16,6 @@ function isNullOrNaN(value) {
const SystemSettings = {
protectedFields: ["multi_user_mode"],
publicFields: [
- "limit_user_messages",
- "message_limit",
"footer_data",
"support_email",
"text_splitter_chunk_size",
@@ -33,8 +31,6 @@ const SystemSettings = {
"meta_page_favicon",
],
supportedFields: [
- "limit_user_messages",
- "message_limit",
"logo_filename",
"telemetry_id",
"footer_data",
diff --git a/server/models/user.js b/server/models/user.js
index a149a45ea6..e6915d9dcd 100644
--- a/server/models/user.js
+++ b/server/models/user.js
@@ -1,6 +1,17 @@
const prisma = require("../utils/prisma");
const { EventLogs } = require("./eventLogs");
+/**
+ * @typedef {Object} User
+ * @property {number} id
+ * @property {string} username
+ * @property {string} password
+ * @property {string} pfpFilename
+ * @property {string} role
+ * @property {boolean} suspended
+ * @property {number|null} dailyMessageLimit
+ */
+
const User = {
usernameRegex: new RegExp(/^[a-z0-9_-]+$/),
writable: [
@@ -10,6 +21,7 @@ const User = {
"pfpFilename",
"role",
"suspended",
+ "dailyMessageLimit",
],
validations: {
username: (newValue = "") => {
@@ -32,12 +44,24 @@ const User = {
}
return String(role);
},
+ dailyMessageLimit: (dailyMessageLimit = null) => {
+ if (dailyMessageLimit === null) return null;
+ const limit = Number(dailyMessageLimit);
+ if (isNaN(limit) || limit < 1) {
+ throw new Error(
+ "Daily message limit must be null or a number greater than or equal to 1"
+ );
+ }
+ return limit;
+ },
},
// validations for the above writable fields.
castColumnValue: function (key, value) {
switch (key) {
case "suspended":
return Number(Boolean(value));
+ case "dailyMessageLimit":
+ return value === null ? null : Number(value);
default:
return String(value);
}
@@ -48,7 +72,12 @@ const User = {
return { ...rest };
},
- create: async function ({ username, password, role = "default" }) {
+ create: async function ({
+ username,
+ password,
+ role = "default",
+ dailyMessageLimit = null,
+ }) {
const passwordCheck = this.checkPasswordComplexity(password);
if (!passwordCheck.checkedOK) {
return { user: null, error: passwordCheck.error };
@@ -58,7 +87,7 @@ const User = {
// Do not allow new users to bypass validation
if (!this.usernameRegex.test(username))
throw new Error(
- "Username must be only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
+ "Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
);
const bcrypt = require("bcrypt");
@@ -68,6 +97,8 @@ const User = {
username: this.validations.username(username),
password: hashedPassword,
role: this.validations.role(role),
+ dailyMessageLimit:
+ this.validations.dailyMessageLimit(dailyMessageLimit),
},
});
return { user: this.filterFields(user), error: null };
@@ -135,7 +166,7 @@ const User = {
return {
success: false,
error:
- "Username must be only contain lowercase letters, numbers, underscores, and hyphens with no spaces",
+ "Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces",
};
const user = await prisma.users.update({
@@ -260,6 +291,29 @@ const User = {
return { checkedOK: true, error: "No error." };
},
+
+ /**
+ * Check if a user can send a chat based on their daily message limit.
+ * This limit is system wide and not per workspace and only applies to
+ * multi-user mode AND non-admin users.
+ * @param {User} user The user object record.
+ * @returns {Promise} True if the user can send a chat, false otherwise.
+ */
+ canSendChat: async function (user) {
+ const { ROLES } = require("../utils/middleware/multiUserProtected");
+ if (!user || user.dailyMessageLimit === null || user.role === ROLES.admin)
+ return true;
+
+ const { WorkspaceChats } = require("./workspaceChats");
+ const currentChatCount = await WorkspaceChats.count({
+ user_id: user.id,
+ createdAt: {
+ gte: new Date(new Date() - 24 * 60 * 60 * 1000), // 24 hours
+ },
+ });
+
+ return currentChatCount < user.dailyMessageLimit;
+ },
};
module.exports = { User };
diff --git a/server/prisma/migrations/20241003192954_init/migration.sql b/server/prisma/migrations/20241003192954_init/migration.sql
new file mode 100644
index 0000000000..e3d26d35c4
--- /dev/null
+++ b/server/prisma/migrations/20241003192954_init/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "dailyMessageLimit" INTEGER;
diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma
index 276b9eaf9d..b961308885 100644
--- a/server/prisma/schema.prisma
+++ b/server/prisma/schema.prisma
@@ -67,6 +67,7 @@ model users {
seen_recovery_codes Boolean? @default(false)
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
+ dailyMessageLimit Int?
workspace_chats workspace_chats[]
workspace_users workspace_users[]
embed_configs embed_configs[]
@@ -309,4 +310,4 @@ model browser_extension_api_keys {
user users? @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@index([user_id])
-}
\ No newline at end of file
+}
diff --git a/server/prisma/seed.js b/server/prisma/seed.js
index c58e455697..202ac04b32 100644
--- a/server/prisma/seed.js
+++ b/server/prisma/seed.js
@@ -4,8 +4,6 @@ const prisma = new PrismaClient();
async function main() {
const settings = [
{ label: "multi_user_mode", value: "false" },
- { label: "limit_user_messages", value: "false" },
- { label: "message_limit", value: "25" },
{ label: "logo_filename", value: "anything-llm.png" },
];
diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json
index 6107331d59..07955bc35d 100644
--- a/server/swagger/openapi.json
+++ b/server/swagger/openapi.json
@@ -693,52 +693,6 @@
}
},
"/v1/admin/preferences": {
- "get": {
- "tags": [
- "Admin"
- ],
- "description": "Show all multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.",
- "parameters": [],
- "responses": {
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "example": {
- "settings": {
- "limit_user_messages": false,
- "message_limit": 10
- }
- }
- }
- }
- }
- },
- "401": {
- "description": "Instance is not in Multi-User mode. Method denied"
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/InvalidAPIKey"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/InvalidAPIKey"
- }
- }
- }
- },
- "500": {
- "description": "Internal Server Error"
- }
- }
- },
"post": {
"tags": [
"Admin"
@@ -788,8 +742,7 @@
"content": {
"application/json": {
"example": {
- "limit_user_messages": true,
- "message_limit": 5
+ "support_email": "support@example.com"
}
}
}