mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2025-04-17 18:18:11 +00:00
Daily message limit per user (#2417)
* set message limit per user * remove old limit user messages + unused admin page * fix daily message validation * refactor message limit input refactor canSendChat on user to a method on user model --------- Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
parent
15ca5e8103
commit
be6289d141
19 changed files with 172 additions and 345 deletions
|
@ -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} />}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -80,9 +80,6 @@ export default {
|
|||
return `/fine-tuning`;
|
||||
},
|
||||
settings: {
|
||||
system: () => {
|
||||
return `/settings/system-preferences`;
|
||||
},
|
||||
users: () => {
|
||||
return `/settings/users`;
|
||||
},
|
||||
|
|
|
@ -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([]),
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "dailyMessageLimit" INTEGER;
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" },
|
||||
];
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue