requestPasswordReset.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import { Request, Response, NextFunction } from "express";
  2. import createHttpError from "http-errors";
  3. import { z } from "zod";
  4. import { fromError } from "zod-validation-error";
  5. import HttpCode from "@server/types/HttpCode";
  6. import { response } from "@server/lib";
  7. import { db } from "@server/db";
  8. import { passwordResetTokens, users } from "@server/db/schema";
  9. import { eq } from "drizzle-orm";
  10. import { alphabet, generateRandomString, sha256 } from "oslo/crypto";
  11. import { createDate } from "oslo";
  12. import logger from "@server/logger";
  13. import { TimeSpan } from "oslo";
  14. import config from "@server/lib/config";
  15. import { sendEmail } from "@server/emails";
  16. import ResetPasswordCode from "@server/emails/templates/ResetPasswordCode";
  17. import { hashPassword } from "@server/auth/password";
  18. export const requestPasswordResetBody = z
  19. .object({
  20. email: z
  21. .string()
  22. .email()
  23. .transform((v) => v.toLowerCase())
  24. })
  25. .strict();
  26. export type RequestPasswordResetBody = z.infer<typeof requestPasswordResetBody>;
  27. export type RequestPasswordResetResponse = {
  28. sentEmail: boolean;
  29. };
  30. export async function requestPasswordReset(
  31. req: Request,
  32. res: Response,
  33. next: NextFunction
  34. ): Promise<any> {
  35. const parsedBody = requestPasswordResetBody.safeParse(req.body);
  36. if (!parsedBody.success) {
  37. return next(
  38. createHttpError(
  39. HttpCode.BAD_REQUEST,
  40. fromError(parsedBody.error).toString()
  41. )
  42. );
  43. }
  44. const { email } = parsedBody.data;
  45. try {
  46. const existingUser = await db
  47. .select()
  48. .from(users)
  49. .where(eq(users.email, email));
  50. if (!existingUser || !existingUser.length) {
  51. return next(
  52. createHttpError(
  53. HttpCode.BAD_REQUEST,
  54. "A user with that email does not exist"
  55. )
  56. );
  57. }
  58. const token = generateRandomString(8, alphabet("0-9", "A-Z", "a-z"));
  59. await db.transaction(async (trx) => {
  60. await trx
  61. .delete(passwordResetTokens)
  62. .where(eq(passwordResetTokens.userId, existingUser[0].userId));
  63. const tokenHash = await hashPassword(token);
  64. await trx.insert(passwordResetTokens).values({
  65. userId: existingUser[0].userId,
  66. email: existingUser[0].email,
  67. tokenHash,
  68. expiresAt: createDate(new TimeSpan(2, "h")).getTime()
  69. });
  70. });
  71. const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${email}&token=${token}`;
  72. if (!config.getRawConfig().email) {
  73. logger.info(
  74. `Password reset requested for ${email}. Token: ${token}.`
  75. );
  76. }
  77. await sendEmail(
  78. ResetPasswordCode({
  79. email,
  80. code: token,
  81. link: url
  82. }),
  83. {
  84. from: config.getNoReplyEmail(),
  85. to: email,
  86. subject: "Reset your password"
  87. }
  88. );
  89. return response<RequestPasswordResetResponse>(res, {
  90. data: {
  91. sentEmail: true
  92. },
  93. success: true,
  94. error: false,
  95. message: "Password reset requested",
  96. status: HttpCode.OK
  97. });
  98. } catch (e) {
  99. logger.error(e);
  100. return next(
  101. createHttpError(
  102. HttpCode.INTERNAL_SERVER_ERROR,
  103. "Failed to process password reset request"
  104. )
  105. );
  106. }
  107. }