mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-19 12:40:09 +01:00
be6289d141
* 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>
320 lines
9.6 KiB
JavaScript
320 lines
9.6 KiB
JavaScript
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: [
|
|
// Used for generic updates so we can validate keys in request body
|
|
"username",
|
|
"password",
|
|
"pfpFilename",
|
|
"role",
|
|
"suspended",
|
|
"dailyMessageLimit",
|
|
],
|
|
validations: {
|
|
username: (newValue = "") => {
|
|
try {
|
|
if (String(newValue).length > 100)
|
|
throw new Error("Username cannot be longer than 100 characters");
|
|
if (String(newValue).length < 2)
|
|
throw new Error("Username must be at least 2 characters");
|
|
return String(newValue);
|
|
} catch (e) {
|
|
throw new Error(e.message);
|
|
}
|
|
},
|
|
role: (role = "default") => {
|
|
const VALID_ROLES = ["default", "admin", "manager"];
|
|
if (!VALID_ROLES.includes(role)) {
|
|
throw new Error(
|
|
`Invalid role. Allowed roles are: ${VALID_ROLES.join(", ")}`
|
|
);
|
|
}
|
|
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);
|
|
}
|
|
},
|
|
|
|
filterFields: function (user = {}) {
|
|
const { password, ...rest } = user;
|
|
return { ...rest };
|
|
},
|
|
|
|
create: async function ({
|
|
username,
|
|
password,
|
|
role = "default",
|
|
dailyMessageLimit = null,
|
|
}) {
|
|
const passwordCheck = this.checkPasswordComplexity(password);
|
|
if (!passwordCheck.checkedOK) {
|
|
return { user: null, error: passwordCheck.error };
|
|
}
|
|
|
|
try {
|
|
// Do not allow new users to bypass validation
|
|
if (!this.usernameRegex.test(username))
|
|
throw new Error(
|
|
"Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
|
|
);
|
|
|
|
const bcrypt = require("bcrypt");
|
|
const hashedPassword = bcrypt.hashSync(password, 10);
|
|
const user = await prisma.users.create({
|
|
data: {
|
|
username: this.validations.username(username),
|
|
password: hashedPassword,
|
|
role: this.validations.role(role),
|
|
dailyMessageLimit:
|
|
this.validations.dailyMessageLimit(dailyMessageLimit),
|
|
},
|
|
});
|
|
return { user: this.filterFields(user), error: null };
|
|
} catch (error) {
|
|
console.error("FAILED TO CREATE USER.", error.message);
|
|
return { user: null, error: error.message };
|
|
}
|
|
},
|
|
// Log the changes to a user object, but omit sensitive fields
|
|
// that are not meant to be logged.
|
|
loggedChanges: function (updates, prev = {}) {
|
|
const changes = {};
|
|
const sensitiveFields = ["password"];
|
|
|
|
Object.keys(updates).forEach((key) => {
|
|
if (!sensitiveFields.includes(key) && updates[key] !== prev[key]) {
|
|
changes[key] = `${prev[key]} => ${updates[key]}`;
|
|
}
|
|
});
|
|
|
|
return changes;
|
|
},
|
|
|
|
update: async function (userId, updates = {}) {
|
|
try {
|
|
if (!userId) throw new Error("No user id provided for update");
|
|
const currentUser = await prisma.users.findUnique({
|
|
where: { id: parseInt(userId) },
|
|
});
|
|
if (!currentUser) return { success: false, error: "User not found" };
|
|
// Removes non-writable fields for generic updates
|
|
// and force-casts to the proper type;
|
|
Object.entries(updates).forEach(([key, value]) => {
|
|
if (this.writable.includes(key)) {
|
|
if (this.validations.hasOwnProperty(key)) {
|
|
updates[key] = this.validations[key](
|
|
this.castColumnValue(key, value)
|
|
);
|
|
} else {
|
|
updates[key] = this.castColumnValue(key, value);
|
|
}
|
|
return;
|
|
}
|
|
delete updates[key];
|
|
});
|
|
|
|
if (Object.keys(updates).length === 0)
|
|
return { success: false, error: "No valid updates applied." };
|
|
|
|
// Handle password specific updates
|
|
if (updates.hasOwnProperty("password")) {
|
|
const passwordCheck = this.checkPasswordComplexity(updates.password);
|
|
if (!passwordCheck.checkedOK) {
|
|
return { success: false, error: passwordCheck.error };
|
|
}
|
|
const bcrypt = require("bcrypt");
|
|
updates.password = bcrypt.hashSync(updates.password, 10);
|
|
}
|
|
|
|
if (
|
|
updates.hasOwnProperty("username") &&
|
|
currentUser.username !== updates.username &&
|
|
!this.usernameRegex.test(updates.username)
|
|
)
|
|
return {
|
|
success: false,
|
|
error:
|
|
"Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces",
|
|
};
|
|
|
|
const user = await prisma.users.update({
|
|
where: { id: parseInt(userId) },
|
|
data: updates,
|
|
});
|
|
|
|
await EventLogs.logEvent(
|
|
"user_updated",
|
|
{
|
|
username: user.username,
|
|
changes: this.loggedChanges(updates, currentUser),
|
|
},
|
|
userId
|
|
);
|
|
return { success: true, error: null };
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
return { success: false, error: error.message };
|
|
}
|
|
},
|
|
|
|
// Explicit direct update of user object.
|
|
// Only use this method when directly setting a key value
|
|
// that takes no user input for the keys being modified.
|
|
_update: async function (id = null, data = {}) {
|
|
if (!id) throw new Error("No user id provided for update");
|
|
|
|
try {
|
|
const user = await prisma.users.update({
|
|
where: { id },
|
|
data,
|
|
});
|
|
return { user, message: null };
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
return { user: null, message: error.message };
|
|
}
|
|
},
|
|
|
|
get: async function (clause = {}) {
|
|
try {
|
|
const user = await prisma.users.findFirst({ where: clause });
|
|
return user ? this.filterFields({ ...user }) : null;
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
return null;
|
|
}
|
|
},
|
|
// Returns user object with all fields
|
|
_get: async function (clause = {}) {
|
|
try {
|
|
const user = await prisma.users.findFirst({ where: clause });
|
|
return user ? { ...user } : null;
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
count: async function (clause = {}) {
|
|
try {
|
|
const count = await prisma.users.count({ where: clause });
|
|
return count;
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
return 0;
|
|
}
|
|
},
|
|
|
|
delete: async function (clause = {}) {
|
|
try {
|
|
await prisma.users.deleteMany({ where: clause });
|
|
return true;
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
where: async function (clause = {}, limit = null) {
|
|
try {
|
|
const users = await prisma.users.findMany({
|
|
where: clause,
|
|
...(limit !== null ? { take: limit } : {}),
|
|
});
|
|
return users.map((usr) => this.filterFields(usr));
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
checkPasswordComplexity: function (passwordInput = "") {
|
|
const passwordComplexity = require("joi-password-complexity");
|
|
// Can be set via ENV variable on boot. No frontend config at this time.
|
|
// Docs: https://www.npmjs.com/package/joi-password-complexity
|
|
const complexityOptions = {
|
|
min: process.env.PASSWORDMINCHAR || 8,
|
|
max: process.env.PASSWORDMAXCHAR || 250,
|
|
lowerCase: process.env.PASSWORDLOWERCASE || 0,
|
|
upperCase: process.env.PASSWORDUPPERCASE || 0,
|
|
numeric: process.env.PASSWORDNUMERIC || 0,
|
|
symbol: process.env.PASSWORDSYMBOL || 0,
|
|
// reqCount should be equal to how many conditions you are testing for (1-4)
|
|
requirementCount: process.env.PASSWORDREQUIREMENTS || 0,
|
|
};
|
|
|
|
const complexityCheck = passwordComplexity(
|
|
complexityOptions,
|
|
"password"
|
|
).validate(passwordInput);
|
|
if (complexityCheck.hasOwnProperty("error")) {
|
|
let myError = "";
|
|
let prepend = "";
|
|
for (let i = 0; i < complexityCheck.error.details.length; i++) {
|
|
myError += prepend + complexityCheck.error.details[i].message;
|
|
prepend = ", ";
|
|
}
|
|
return { checkedOK: false, error: myError };
|
|
}
|
|
|
|
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 };
|