You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

369 lines
11 KiB

11 months ago
11 months ago
10 months ago
11 months ago
11 months ago
11 months ago
  1. const prisma = require("../utils/prisma");
  2. const { EventLogs } = require("./eventLogs");
  3. const bcrypt = require("bcrypt");
  4. /**
  5. * @typedef {Object} User
  6. * @property {number} id
  7. * @property {string} username
  8. * @property {string} password
  9. * @property {string} pfpFilename
  10. * @property {string} role
  11. * @property {boolean} suspended
  12. * @property {number|null} dailyMessageLimit
  13. */
  14. const User = {
  15. usernameRegex: new RegExp(/^[a-z0-9_-]+$/),
  16. writable: [
  17. // Used for generic updates so we can validate keys in request body
  18. "username",
  19. "password",
  20. "pfpFilename",
  21. "role",
  22. "suspended",
  23. "dailyMessageLimit",
  24. ],
  25. validations: {
  26. username: (newValue = "") => {
  27. try {
  28. if (String(newValue).length > 100)
  29. throw new Error("Username cannot be longer than 100 characters");
  30. if (String(newValue).length < 2)
  31. throw new Error("Username must be at least 2 characters");
  32. return String(newValue);
  33. } catch (e) {
  34. throw new Error(e.message);
  35. }
  36. },
  37. role: (role = "default") => {
  38. const VALID_ROLES = ["default", "admin", "manager"];
  39. if (!VALID_ROLES.includes(role)) {
  40. throw new Error(
  41. `Invalid role. Allowed roles are: ${VALID_ROLES.join(", ")}`
  42. );
  43. }
  44. return String(role);
  45. },
  46. dailyMessageLimit: (dailyMessageLimit = null) => {
  47. if (dailyMessageLimit === null) return null;
  48. const limit = Number(dailyMessageLimit);
  49. if (isNaN(limit) || limit < 1) {
  50. throw new Error(
  51. "Daily message limit must be null or a number greater than or equal to 1"
  52. );
  53. }
  54. return limit;
  55. },
  56. },
  57. // validations for the above writable fields.
  58. castColumnValue: function (key, value) {
  59. switch (key) {
  60. case "suspended":
  61. return Number(Boolean(value));
  62. case "dailyMessageLimit":
  63. return value === null ? null : Number(value);
  64. default:
  65. return String(value);
  66. }
  67. },
  68. filterFields: function (user = {}) {
  69. const { password, ...rest } = user;
  70. return { ...rest };
  71. },
  72. create: async function ({
  73. username,
  74. password,
  75. role = "default",
  76. dailyMessageLimit = null,
  77. }) {
  78. const passwordCheck = this.checkPasswordComplexity(password);
  79. if (!passwordCheck.checkedOK) {
  80. return { user: null, error: passwordCheck.error };
  81. }
  82. try {
  83. // Do not allow new users to bypass validation
  84. if (!this.usernameRegex.test(username))
  85. throw new Error(
  86. "Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
  87. );
  88. const bcrypt = require("bcrypt");
  89. const hashedPassword = bcrypt.hashSync(password, 10);
  90. const user = await prisma.users.create({
  91. data: {
  92. username: this.validations.username(username),
  93. password: hashedPassword,
  94. role: this.validations.role(role),
  95. dailyMessageLimit:
  96. this.validations.dailyMessageLimit(dailyMessageLimit),
  97. },
  98. });
  99. // const vue = this.filterFields(user);
  100. console.log("6666666666",user);
  101. return { user: this.filterFields(user), error: null };
  102. } catch (error) {
  103. console.error("FAILED TO CREATE USER.", error.message);
  104. return { user: null, error: error.message };
  105. }
  106. },
  107. createNew: async function ({
  108. username,
  109. password,
  110. role = "default",
  111. dailyMessageLimit = null,
  112. },prisma) {
  113. const passwordCheck = this.checkPasswordComplexity(password);
  114. console.log("6666666666",passwordCheck)
  115. if (!passwordCheck.checkedOK) {
  116. return { user: null, error: passwordCheck.error };
  117. }
  118. try {
  119. // Do not allow new users to bypass validation
  120. if (!this.usernameRegex.test(username))
  121. throw new Error(
  122. "Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
  123. );
  124. const bcrypt = require("bcrypt");
  125. const hashedPassword = bcrypt.hashSync(password, 10);
  126. const user = await prisma.users.create({
  127. data: {
  128. username: this.validations.username(username),
  129. password: hashedPassword,
  130. role: this.validations.role(role),
  131. dailyMessageLimit:
  132. this.validations.dailyMessageLimit(dailyMessageLimit),
  133. },
  134. });
  135. // const vue = this.filterFields(user);
  136. console.log("6666666666",user);
  137. return { user: this.filterFields(user), error: null };
  138. } catch (error) {
  139. console.error("FAILED TO CREATE USER.", error.message);
  140. return { user: null, error: error.message };
  141. }
  142. },
  143. // Log the changes to a user object, but omit sensitive fields
  144. // that are not meant to be logged.
  145. loggedChanges: function (updates, prev = {}) {
  146. const changes = {};
  147. const sensitiveFields = ["password"];
  148. Object.keys(updates).forEach((key) => {
  149. if (!sensitiveFields.includes(key) && updates[key] !== prev[key]) {
  150. changes[key] = `${prev[key]} => ${updates[key]}`;
  151. }
  152. });
  153. return changes;
  154. },
  155. update: async function (userId, updates = {}) {
  156. try {
  157. if (!userId) throw new Error("No user id provided for update");
  158. const currentUser = await prisma.users.findUnique({
  159. where: { id: parseInt(userId) },
  160. });
  161. if (!currentUser) return { success: false, error: "User not found" };
  162. // Removes non-writable fields for generic updates
  163. // and force-casts to the proper type;
  164. Object.entries(updates).forEach(([key, value]) => {
  165. if (this.writable.includes(key)) {
  166. if (this.validations.hasOwnProperty(key)) {
  167. updates[key] = this.validations[key](
  168. this.castColumnValue(key, value)
  169. );
  170. } else {
  171. updates[key] = this.castColumnValue(key, value);
  172. }
  173. return;
  174. }
  175. delete updates[key];
  176. });
  177. if (Object.keys(updates).length === 0)
  178. return { success: false, error: "No valid updates applied." };
  179. // Handle password specific updates
  180. if (updates.hasOwnProperty("password")) {
  181. const passwordCheck = this.checkPasswordComplexity(updates.password);
  182. if (!passwordCheck.checkedOK) {
  183. return { success: false, error: passwordCheck.error };
  184. }
  185. const bcrypt = require("bcrypt");
  186. updates.password = bcrypt.hashSync(updates.password, 10);
  187. }
  188. if (
  189. updates.hasOwnProperty("username") &&
  190. currentUser.username !== updates.username &&
  191. !this.usernameRegex.test(updates.username)
  192. )
  193. return {
  194. success: false,
  195. error:
  196. "Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces",
  197. };
  198. const user = await prisma.users.update({
  199. where: { id: parseInt(userId) },
  200. data: updates,
  201. });
  202. await EventLogs.logEvent(
  203. "user_updated",
  204. {
  205. username: user.username,
  206. changes: this.loggedChanges(updates, currentUser),
  207. },
  208. userId
  209. );
  210. return { success: true, error: null };
  211. } catch (error) {
  212. console.error(error.message);
  213. return { success: false, error: error.message };
  214. }
  215. },
  216. // Explicit direct update of user object.
  217. // Only use this method when directly setting a key value
  218. // that takes no user input for the keys being modified.
  219. _update: async function (id = null, data = {}) {
  220. if (!id) throw new Error("No user id provided for update");
  221. try {
  222. const user = await prisma.users.update({
  223. where: { id },
  224. data,
  225. });
  226. return { user, message: null };
  227. } catch (error) {
  228. console.error(error.message);
  229. return { user: null, message: error.message };
  230. }
  231. },
  232. get: async function (clause = {}) {
  233. try {
  234. const user = await prisma.users.findFirst({ where: clause });
  235. return user ? this.filterFields({ ...user }) : null;
  236. } catch (error) {
  237. console.error(error.message);
  238. return null;
  239. }
  240. },
  241. getNew: async function (clause = {},prisma) {
  242. try {
  243. const user = await prisma.users.findFirst({ where: clause });
  244. return user ? this.filterFields({ ...user }) : null;
  245. } catch (error) {
  246. console.error(error.message);
  247. return null;
  248. }
  249. },
  250. // Returns user object with all fields
  251. _get: async function (clause = {}) {
  252. try {
  253. const user = await prisma.users.findFirst({ where: clause });
  254. return user ? { ...user } : null;
  255. } catch (error) {
  256. console.error(error.message);
  257. return null;
  258. }
  259. },
  260. count: async function (clause = {}) {
  261. try {
  262. const count = await prisma.users.count({ where: clause });
  263. return count;
  264. } catch (error) {
  265. console.error(error.message);
  266. return 0;
  267. }
  268. },
  269. delete: async function (clause = {}) {
  270. try {
  271. await prisma.users.deleteMany({ where: clause });
  272. return true;
  273. } catch (error) {
  274. console.error(error.message);
  275. return false;
  276. }
  277. },
  278. where: async function (clause = {}, limit = null) {
  279. try {
  280. const users = await prisma.users.findMany({
  281. where: clause,
  282. ...(limit !== null ? { take: limit } : {}),
  283. });
  284. return users.map((usr) => this.filterFields(usr));
  285. } catch (error) {
  286. console.error(error.message);
  287. return [];
  288. }
  289. },
  290. checkPasswordComplexity: function (passwordInput = "") {
  291. const passwordComplexity = require("joi-password-complexity");
  292. // Can be set via ENV variable on boot. No frontend config at this time.
  293. // Docs: https://www.npmjs.com/package/joi-password-complexity
  294. const complexityOptions = {
  295. min: process.env.PASSWORDMINCHAR || 8,
  296. max: process.env.PASSWORDMAXCHAR || 250,
  297. lowerCase: process.env.PASSWORDLOWERCASE || 0,
  298. upperCase: process.env.PASSWORDUPPERCASE || 0,
  299. numeric: process.env.PASSWORDNUMERIC || 0,
  300. symbol: process.env.PASSWORDSYMBOL || 0,
  301. // reqCount should be equal to how many conditions you are testing for (1-4)
  302. requirementCount: process.env.PASSWORDREQUIREMENTS || 0,
  303. };
  304. const complexityCheck = passwordComplexity(
  305. complexityOptions,
  306. "password"
  307. ).validate(passwordInput);
  308. if (complexityCheck.hasOwnProperty("error")) {
  309. let myError = "";
  310. let prepend = "";
  311. for (let i = 0; i < complexityCheck.error.details.length; i++) {
  312. myError += prepend + complexityCheck.error.details[i].message;
  313. prepend = ", ";
  314. }
  315. return { checkedOK: false, error: myError };
  316. }
  317. return { checkedOK: true, error: "No error." };
  318. },
  319. /**
  320. * Check if a user can send a chat based on their daily message limit.
  321. * This limit is system wide and not per workspace and only applies to
  322. * multi-user mode AND non-admin users.
  323. * @param {User} user The user object record.
  324. * @returns {Promise<boolean>} True if the user can send a chat, false otherwise.
  325. */
  326. canSendChat: async function (user) {
  327. const { ROLES } = require("../utils/middleware/multiUserProtected");
  328. if (!user || user.dailyMessageLimit === null || user.role === ROLES.admin)
  329. return true;
  330. const { WorkspaceChats } = require("./workspaceChats");
  331. const currentChatCount = await WorkspaceChats.count({
  332. user_id: user.id,
  333. createdAt: {
  334. gte: new Date(new Date() - 24 * 60 * 60 * 1000), // 24 hours
  335. },
  336. });
  337. return currentChatCount < user.dailyMessageLimit;
  338. },
  339. };
  340. module.exports = { User };