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.

103 lines
3.4 KiB

11 months ago
  1. const bcrypt = require("bcrypt");
  2. const { v4, validate } = require("uuid");
  3. const { User } = require("../../models/user");
  4. const {
  5. RecoveryCode,
  6. PasswordResetToken,
  7. } = require("../../models/passwordRecovery");
  8. async function generateRecoveryCodes(userId) {
  9. const newRecoveryCodes = [];
  10. const plainTextCodes = [];
  11. for (let i = 0; i < 4; i++) {
  12. const code = v4();
  13. const hashedCode = bcrypt.hashSync(code, 10);
  14. newRecoveryCodes.push({
  15. user_id: userId,
  16. code_hash: hashedCode,
  17. });
  18. plainTextCodes.push(code);
  19. }
  20. const { error } = await RecoveryCode.createMany(newRecoveryCodes);
  21. if (!!error) throw new Error(error);
  22. const { user: success } = await User._update(userId, {
  23. seen_recovery_codes: true,
  24. });
  25. if (!success) throw new Error("Failed to generate user recovery codes!");
  26. return plainTextCodes;
  27. }
  28. async function recoverAccount(username = "", recoveryCodes = []) {
  29. const user = await User.get({ username: String(username) });
  30. if (!user) return { success: false, error: "Invalid recovery codes." };
  31. // If hashes do not exist for a user
  32. // because this is a user who has not logged out and back in since upgrade.
  33. const allUserHashes = await RecoveryCode.hashesForUser(user.id);
  34. if (allUserHashes.length < 4)
  35. return { success: false, error: "Invalid recovery codes" };
  36. // If they tried to send more than two unique codes, we only take the first two
  37. const uniqueRecoveryCodes = [...new Set(recoveryCodes)]
  38. .map((code) => code.trim())
  39. .filter((code) => validate(code)) // we know that any provided code must be a uuid v4.
  40. .slice(0, 2);
  41. if (uniqueRecoveryCodes.length !== 2)
  42. return { success: false, error: "Invalid recovery codes." };
  43. const validCodes = uniqueRecoveryCodes.every((code) => {
  44. let valid = false;
  45. allUserHashes.forEach((hash) => {
  46. if (bcrypt.compareSync(code, hash)) valid = true;
  47. });
  48. return valid;
  49. });
  50. if (!validCodes) return { success: false, error: "Invalid recovery codes" };
  51. const { passwordResetToken, error } = await PasswordResetToken.create(
  52. user.id
  53. );
  54. if (!!error) return { success: false, error };
  55. return { success: true, resetToken: passwordResetToken.token };
  56. }
  57. async function resetPassword(token, _newPassword = "", confirmPassword = "") {
  58. const newPassword = String(_newPassword).trim(); // No spaces in passwords
  59. if (!newPassword) throw new Error("Invalid password.");
  60. if (newPassword !== String(confirmPassword))
  61. throw new Error("Passwords do not match");
  62. const resetToken = await PasswordResetToken.findUnique({
  63. token: String(token),
  64. });
  65. if (!resetToken || resetToken.expiresAt < new Date()) {
  66. return { success: false, message: "Invalid reset token" };
  67. }
  68. // JOI password rules will be enforced inside .update.
  69. const { error } = await User.update(resetToken.user_id, {
  70. password: newPassword,
  71. });
  72. // seen_recovery_codes is not publicly writable
  73. // so we have to do direct update here
  74. await User._update(resetToken.user_id, {
  75. seen_recovery_codes: false,
  76. });
  77. if (error) return { success: false, message: error };
  78. await PasswordResetToken.deleteMany({ user_id: resetToken.user_id });
  79. await RecoveryCode.deleteMany({ user_id: resetToken.user_id });
  80. // New codes are provided on first new login.
  81. return { success: true, message: "Password reset successful" };
  82. }
  83. module.exports = {
  84. recoverAccount,
  85. resetPassword,
  86. generateRecoveryCodes,
  87. };