hackx
Backend From Zero (17 / 35)

Password reset

The brief requires this. Flow is simple: email a short-lived token, let them set a new password, kill the token.

router.post("/forgot-password", async (req, res) => {
  const email = String(req.body.email ?? "").trim().toLowerCase();
  const generic = { message: "if the account exists, a reset link was sent" };
  const user = await prisma.user.findUnique({ where: { email } });
  if (!user) return res.json(generic);

  const token = crypto.randomBytes(32).toString("hex");
  await prisma.emailToken.create({
    data: {
      token,
      purpose: "reset",
      expiresAt: new Date(Date.now() + 18e5),
      userId: user.id,
    },
  });
  await sendMail({
    to: email,
    subject: "Reset password",
    text: `${process.env.APP_URL}/reset-password?token=${token}`,
  });
  res.json(generic);
});

router.post("/reset-password", async (req, res) => {
  const token = String(req.body.token ?? "");
  const password = String(req.body.password ?? "");
  if (!token || password.length < 8) {
    return res.status(400).json({ error: "token and password required" });
  }

  const row = await prisma.emailToken.findUnique({ where: { token } });
  if (!row || row.purpose !== "reset" || row.usedAt) {
    return res.status(400).json({ error: "invalid token" });
  }
  if (row.expiresAt < new Date()) {
    return res.status(400).json({ error: "token expired" });
  }

  const passwordHash = await bcrypt.hash(password, 10);
  await prisma.user.update({ where: { id: row.userId }, data: { passwordHash } });
  await prisma.emailToken.update({ where: { token }, data: { usedAt: new Date() } });
  res.json({ ok: true });
});

We always return the same "if it exists…" message on forgot — no email fishing.