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.

321 lines
9.7 KiB

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