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.

319 lines
9.6 KiB

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. return { user: this.filterFields(user), error: null };
  99. } catch (error) {
  100. console.error("FAILED TO CREATE USER.", error.message);
  101. return { user: null, error: error.message };
  102. }
  103. },
  104. // Log the changes to a user object, but omit sensitive fields
  105. // that are not meant to be logged.
  106. loggedChanges: function (updates, prev = {}) {
  107. const changes = {};
  108. const sensitiveFields = ["password"];
  109. Object.keys(updates).forEach((key) => {
  110. if (!sensitiveFields.includes(key) && updates[key] !== prev[key]) {
  111. changes[key] = `${prev[key]} => ${updates[key]}`;
  112. }
  113. });
  114. return changes;
  115. },
  116. update: async function (userId, updates = {}) {
  117. try {
  118. if (!userId) throw new Error("No user id provided for update");
  119. const currentUser = await prisma.users.findUnique({
  120. where: { id: parseInt(userId) },
  121. });
  122. if (!currentUser) return { success: false, error: "User not found" };
  123. // Removes non-writable fields for generic updates
  124. // and force-casts to the proper type;
  125. Object.entries(updates).forEach(([key, value]) => {
  126. if (this.writable.includes(key)) {
  127. if (this.validations.hasOwnProperty(key)) {
  128. updates[key] = this.validations[key](
  129. this.castColumnValue(key, value)
  130. );
  131. } else {
  132. updates[key] = this.castColumnValue(key, value);
  133. }
  134. return;
  135. }
  136. delete updates[key];
  137. });
  138. if (Object.keys(updates).length === 0)
  139. return { success: false, error: "No valid updates applied." };
  140. // Handle password specific updates
  141. if (updates.hasOwnProperty("password")) {
  142. const passwordCheck = this.checkPasswordComplexity(updates.password);
  143. if (!passwordCheck.checkedOK) {
  144. return { success: false, error: passwordCheck.error };
  145. }
  146. const bcrypt = require("bcrypt");
  147. updates.password = bcrypt.hashSync(updates.password, 10);
  148. }
  149. if (
  150. updates.hasOwnProperty("username") &&
  151. currentUser.username !== updates.username &&
  152. !this.usernameRegex.test(updates.username)
  153. )
  154. return {
  155. success: false,
  156. error:
  157. "Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces",
  158. };
  159. const user = await prisma.users.update({
  160. where: { id: parseInt(userId) },
  161. data: updates,
  162. });
  163. await EventLogs.logEvent(
  164. "user_updated",
  165. {
  166. username: user.username,
  167. changes: this.loggedChanges(updates, currentUser),
  168. },
  169. userId
  170. );
  171. return { success: true, error: null };
  172. } catch (error) {
  173. console.error(error.message);
  174. return { success: false, error: error.message };
  175. }
  176. },
  177. // Explicit direct update of user object.
  178. // Only use this method when directly setting a key value
  179. // that takes no user input for the keys being modified.
  180. _update: async function (id = null, data = {}) {
  181. if (!id) throw new Error("No user id provided for update");
  182. try {
  183. const user = await prisma.users.update({
  184. where: { id },
  185. data,
  186. });
  187. return { user, message: null };
  188. } catch (error) {
  189. console.error(error.message);
  190. return { user: null, message: error.message };
  191. }
  192. },
  193. get: async function (clause = {}) {
  194. try {
  195. const user = await prisma.users.findFirst({ where: clause });
  196. return user ? this.filterFields({ ...user }) : null;
  197. } catch (error) {
  198. console.error(error.message);
  199. return null;
  200. }
  201. },
  202. // Returns user object with all fields
  203. _get: async function (clause = {}) {
  204. try {
  205. const user = await prisma.users.findFirst({ where: clause });
  206. return user ? { ...user } : null;
  207. } catch (error) {
  208. console.error(error.message);
  209. return null;
  210. }
  211. },
  212. count: async function (clause = {}) {
  213. try {
  214. const count = await prisma.users.count({ where: clause });
  215. return count;
  216. } catch (error) {
  217. console.error(error.message);
  218. return 0;
  219. }
  220. },
  221. delete: async function (clause = {}) {
  222. try {
  223. await prisma.users.deleteMany({ where: clause });
  224. return true;
  225. } catch (error) {
  226. console.error(error.message);
  227. return false;
  228. }
  229. },
  230. where: async function (clause = {}, limit = null) {
  231. try {
  232. const users = await prisma.users.findMany({
  233. where: clause,
  234. ...(limit !== null ? { take: limit } : {}),
  235. });
  236. return users.map((usr) => this.filterFields(usr));
  237. } catch (error) {
  238. console.error(error.message);
  239. return [];
  240. }
  241. },
  242. checkPasswordComplexity: function (passwordInput = "") {
  243. const passwordComplexity = require("joi-password-complexity");
  244. // Can be set via ENV variable on boot. No frontend config at this time.
  245. // Docs: https://www.npmjs.com/package/joi-password-complexity
  246. const complexityOptions = {
  247. min: process.env.PASSWORDMINCHAR || 8,
  248. max: process.env.PASSWORDMAXCHAR || 250,
  249. lowerCase: process.env.PASSWORDLOWERCASE || 0,
  250. upperCase: process.env.PASSWORDUPPERCASE || 0,
  251. numeric: process.env.PASSWORDNUMERIC || 0,
  252. symbol: process.env.PASSWORDSYMBOL || 0,
  253. // reqCount should be equal to how many conditions you are testing for (1-4)
  254. requirementCount: process.env.PASSWORDREQUIREMENTS || 0,
  255. };
  256. const complexityCheck = passwordComplexity(
  257. complexityOptions,
  258. "password"
  259. ).validate(passwordInput);
  260. if (complexityCheck.hasOwnProperty("error")) {
  261. let myError = "";
  262. let prepend = "";
  263. for (let i = 0; i < complexityCheck.error.details.length; i++) {
  264. myError += prepend + complexityCheck.error.details[i].message;
  265. prepend = ", ";
  266. }
  267. return { checkedOK: false, error: myError };
  268. }
  269. return { checkedOK: true, error: "No error." };
  270. },
  271. /**
  272. * Check if a user can send a chat based on their daily message limit.
  273. * This limit is system wide and not per workspace and only applies to
  274. * multi-user mode AND non-admin users.
  275. * @param {User} user The user object record.
  276. * @returns {Promise<boolean>} True if the user can send a chat, false otherwise.
  277. */
  278. canSendChat: async function (user) {
  279. const { ROLES } = require("../utils/middleware/multiUserProtected");
  280. if (!user || user.dailyMessageLimit === null || user.role === ROLES.admin)
  281. return true;
  282. const { WorkspaceChats } = require("./workspaceChats");
  283. const currentChatCount = await WorkspaceChats.count({
  284. user_id: user.id,
  285. createdAt: {
  286. gte: new Date(new Date() - 24 * 60 * 60 * 1000), // 24 hours
  287. },
  288. });
  289. return currentChatCount < user.dailyMessageLimit;
  290. },
  291. };
  292. module.exports = { User };