diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c6cac66db..cb3bac7f7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -22,7 +22,6 @@ const WorkspaceChat = lazy(() => import("@/pages/WorkspaceChat")); const AdminUsers = lazy(() => import("@/pages/Admin/Users")); const AdminInvites = lazy(() => import("@/pages/Admin/Invitations")); const AdminWorkspaces = lazy(() => import("@/pages/Admin/Workspaces")); -const AdminSystem = lazy(() => import("@/pages/Admin/System")); const AdminLogs = lazy(() => import("@/pages/Admin/Logging")); const AdminAgents = lazy(() => import("@/pages/Admin/Agents")); const GeneralChats = lazy(() => import("@/pages/GeneralSettings/Chats")); @@ -168,10 +167,6 @@ export default function App() { path="/settings/workspace-chats" element={<ManagerRoute Component={GeneralChats} />} /> - <Route - path="/settings/system-preferences" - element={<ManagerRoute Component={AdminSystem} />} - /> <Route path="/settings/invites" element={<ManagerRoute Component={AdminInvites} />} diff --git a/frontend/src/components/SettingsButton/index.jsx b/frontend/src/components/SettingsButton/index.jsx index 19a4a17aa..f53e675f1 100644 --- a/frontend/src/components/SettingsButton/index.jsx +++ b/frontend/src/components/SettingsButton/index.jsx @@ -29,9 +29,7 @@ export default function SettingsButton() { return ( <ToolTipWrapper id="open-settings"> <Link - to={ - !!user?.role ? paths.settings.system() : paths.settings.appearance() - } + to={paths.settings.appearance()} className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" aria-label="Settings" data-tooltip-id="open-settings" diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx index 97b088eca..367b21ab6 100644 --- a/frontend/src/components/SettingsSidebar/index.jsx +++ b/frontend/src/components/SettingsSidebar/index.jsx @@ -278,11 +278,6 @@ const SidebarOptions = ({ user = null, t }) => ( href: paths.settings.invites(), roles: ["admin", "manager"], }, - { - btnText: t("settings.system"), - href: paths.settings.system(), - roles: ["admin", "manager"], - }, ]} /> <Option diff --git a/frontend/src/components/UserMenu/AccountModal/index.jsx b/frontend/src/components/UserMenu/AccountModal/index.jsx index 9fac4aaeb..9fe7f60a2 100644 --- a/frontend/src/components/UserMenu/AccountModal/index.jsx +++ b/frontend/src/components/UserMenu/AccountModal/index.jsx @@ -135,7 +135,7 @@ export default function AccountModal({ user, hideModal }) { autoComplete="off" /> <p className="mt-2 text-xs text-white/60"> - Username must be only contain lowercase letters, numbers, + Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces </p> </div> diff --git a/frontend/src/pages/Admin/System/index.jsx b/frontend/src/pages/Admin/System/index.jsx deleted file mode 100644 index dba1872d3..000000000 --- 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 ( - <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> - <Sidebar /> - <div - style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} - className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll" - > - <form - onSubmit={handleSubmit} - onChange={() => setHasChanges(true)} - className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16" - > - <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> - <div className="items-center"> - <p className="text-lg leading-6 font-bold text-white"> - System Preferences - </p> - </div> - <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - These are the overall settings and configurations of your - instance. - </p> - </div> - {hasChanges && ( - <div className="flex justify-end"> - <CTAButton onClick={handleSubmit} className="mt-3 mr-0"> - {saving ? "Saving..." : "Save changes"} - </CTAButton> - </div> - )} - <div className="mt-4 mb-8"> - <div className="flex flex-col gap-y-1"> - <h2 className="text-base leading-6 font-bold text-white"> - Limit messages per user per day - </h2> - <p className="text-xs leading-[18px] font-base text-white/60"> - Restrict non-admin users to a number of successful queries or - chats within a 24 hour window. Enable this to prevent users from - running up OpenAI costs. - </p> - <div className="mt-2"> - <label className="relative inline-flex cursor-pointer items-center"> - <input - type="checkbox" - name="limit_user_messages" - value="yes" - checked={messageLimit.enabled} - onChange={(e) => { - setMessageLimit({ - ...messageLimit, - enabled: e.target.checked, - }); - }} - className="peer sr-only" - /> - <div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div> - <span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span> - </label> - </div> - </div> - {messageLimit.enabled && ( - <div className="mt-4"> - <label className="text-white text-sm font-semibold block mb-4"> - Message limit per day - </label> - <div className="relative mt-2"> - <input - type="number" - name="message_limit" - onScroll={(e) => e.target.blur()} - onChange={(e) => { - setMessageLimit({ - enabled: true, - limit: Number(e?.target?.value || 0), - }); - }} - value={messageLimit.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" - /> - </div> - </div> - )} - </div> - </form> - </div> - </div> - ); -} diff --git a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx index 3af7ebd4a..ebca2debb 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("")} /> <p className="mt-2 text-xs text-white/60"> - Username must be only contain lowercase letters, numbers, + Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces </p> </div> @@ -110,6 +116,12 @@ export default function NewUserModal({ closeModal }) { </select> <RoleHintDisplay role={role} /> </div> + <MessageLimitInput + role={role} + enabled={messageLimit.enabled} + limit={messageLimit.limit} + updateState={setMessageLimit} + /> {error && <p className="text-red-400 text-sm">Error: {error}</p>} <p className="text-white text-xs md:text-sm"> 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 ec234c2f4..b6c30a852 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" /> <p className="mt-2 text-xs text-white/60"> - Username must be only contain lowercase letters, numbers, + Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces </p> </div> @@ -103,6 +113,12 @@ export default function EditUserModal({ currentUser, user, closeModal }) { </select> <RoleHintDisplay role={role} /> </div> + <MessageLimitInput + role={role} + enabled={messageLimit.enabled} + limit={messageLimit.limit} + updateState={setMessageLimit} + /> {error && <p className="text-red-400 text-sm">Error: {error}</p>} </div> </div> diff --git a/frontend/src/pages/Admin/Users/index.jsx b/frontend/src/pages/Admin/Users/index.jsx index 408e794aa..ed9f8b3f1 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 }) { </div> ); } + +export function MessageLimitInput({ enabled, limit, updateState, role }) { + if (role === "admin") 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"> + Limit messages per day + </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-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div> + </label> + </div> + <p className="text-xs leading-[18px] font-base text-white/60"> + Restrict this user to a number of successful queries or chats within a + 24 hour window. + </p> + </div> + {enabled && ( + <div className="mt-4"> + <label className="text-white text-sm font-semibold block mb-4"> + Message limit per day + </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="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-60 p-2.5" + /> + </div> + </div> + )} + </div> + ); +} diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js index 28d36025c..554cc4599 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 994c8e416..cf3c310d8 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 11c9d1c11..c165a9cc0 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 64beefeb6..7e8a72b61 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 ae807fe2b..ccdb50ec8 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 8e3a61767..0c67bf2f4 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 a149a45ea..e6915d9dc 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<boolean>} 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 000000000..e3d26d35c --- /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 276b9eaf9..b96130888 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 c58e45569..202ac04b3 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 6107331d5..07955bc35 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" } } }