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

1 year ago
1 year ago
1 year ago
1 year ago
1 year 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 };