0

[Series Thực Chiến E-commerce] Bài 8: Tính năng "Cứu cánh" - Xử lý Quên Mật Khẩu & Gửi Email với Nodemailer

Chào anh em thiện lành! (Link bài 7)

Làm web bán hàng mà không có nút Quên mật khẩu thì đúng là đánh đố người dùng. Khách hàng não cá vàng lắm, nay đặt pass mai quên là chuyện thường ở huyện. Thế nên, bài toán đặt ra hôm nay là: Làm sao để hệ thống tự động sinh ra một cái link khôi phục, gửi thẳng vào Email của khách, và đảm bảo cái link đó chỉ sống được 15 phút?

Bài 8 này chúng ta sẽ xài đến hàng nóng: Thư viện nodemailer và module crypto tích hợp sẵn của Node.js. Anh em xắn tay áo lên nào!

1. Chuẩn bị "Tem thư" và "Người đưa thư" (Utils & .env)

Trước khi gửi mail, anh em phải có một tài khoản Gmail đóng vai trò là "Bot gửi thư" của hệ thống. Nhớ kỹ: Google hiện tại không cho dùng mật khẩu đăng nhập bình thường để gửi mail qua code nữa, anh em phải vào cài đặt bảo mật của Google để tạo App Password (Mật khẩu ứng dụng) nhé.

Vứt cấu hình vào file .env:

EMAIL_APP_PASSWORD=xxxxxxxxxxxxx
EMAIL_NAME=xxxxxxxxxxxxx@gmail.com
URL_SERVER=http://localhost:5000

Tiếp theo, tạo file utils/sendMail.js. Đây là anh shipper của chúng ta:

const nodemailer = require("nodemailer");

const sendMail = async ({ email, html }) => {
  // Cấu hình trạm trung chuyển (SMTP của Gmail)
  const transporter = nodemailer.createTransport({
    host: "smtp.gmail.com", 
    port: 587,
    secure: false, 
    auth: {
      user: process.env.EMAIL_NAME, 
      pass: process.env.EMAIL_APP_PASSWORD,
    },
  });

  // Tiến hành gửi email
  const info = await transporter.sendMail({
    from: '"E-commerce Support" <no-reply@ecommerce.com>', // Đổi tên cho chuyên nghiệp nhé anh em
    to: email, // Bắn vào email của khách
    subject: "Reset Password - E-commerce App", 
    html: html, // Nội dung chứa cái link reset
  });

  return info;
};

module.exports = sendMail;

2. Tuyệt chiêu bảo mật: Tạo Token Reset (Model)

Khi khách bấm "Quên mật khẩu", ta không thể gửi thẳng cái mật khẩu mới cho họ được (vì mình đã mã hóa nó ở Bài 3 rồi, có biết đâu mà gửi). Thay vào đó, ta cấp cho họ một cái "Token" đặc biệt để họ tự đổi pass.

Mở file models/user.js và thêm method này (nhớ require('crypto') ở đầu file nha):

const crypto = require('crypto');

userSchema.methods = {
  // ... (hàm isCorrectPassword bài trước)

  createPasswordChangedToken: function() {
    // 1. Sinh ra một chuỗi ngẫu nhiên dài 32 byte
    const resetToken = crypto.randomBytes(32).toString('hex');
    
    // 💡 KINH NGHIỆM XƯƠNG MÁU: Không lưu chuỗi raw này vào DB!
    // Phải băm (hash) nó ra bằng sha256 rồi mới lưu. 
    // Tránh trường hợp DB bị leak, hacker cũng không có token gốc để xài.
    this.passwordResetToken = crypto.createHash('sha256').update(resetToken).digest('hex');
    
    // 2. Set hạn sử dụng cho token là 15 phút tính từ hiện tại
    this.passwordResetExpires = Date.now() + 15 * 60 * 1000; 
    
    // 3. Trả về chuỗi raw để Controller nhét vào Email gửi đi
    return resetToken;
  }
};

3. Lắp ráp não bộ (Controller)

Giờ thì nối các mạch lại với nhau. Mởcontrollers/user.js:

const sendMail = require('../utils/sendMail');

const forgotPassword = asyncHandler(async (req, res) => {
  // Lấy email từ request. 
  // 💡 Góp ý nhỏ: Route là POST thì anh em nên lấy từ req.body thay vì req.query cho chuẩn RESTful nhé.
  const { email } = req.query; 
  if (!email) throw new Error('Missing email');

  // Check xem thằng này có tồn tại trong hệ thống không
  const user = await User.findOne({ email });
  if (!user) throw new Error('User not found');

  // Gọi Model sinh token và lưu ngay vào DB
  const resetToken = user.createPasswordChangedToken();
  await user.save(); // Chỗ này quan trọng, không save là DB không cập nhật token đâu

  // Cấu trúc nội dung Email
  const html = `Xin vui lòng click vào link dưới đây để thay đổi mật khẩu của bạn. Link này sẽ hết hạn sau 15 phút. 
  <a href="${process.env.URL_SERVER}/api/user/reset-password/${resetToken}">Nhấn vào đây để đổi mật khẩu</a>`;

  const data = { email, html };

  // Gọi anh shipper nodemailer đi giao hàng
  const rs = await sendMail(data);
  
  return res.status(200).json({
    success: true,
    message: 'Email khôi phục đã được gửi. Vui lòng check hộp thư!',
    rs
  });
});

4. Nối dây ra ngoài (Router)

Cuối cùng, vào routers/user.js mở cổng cho Frontend gọi:

router.post('/forgotpassword', ctrls.forgotPassword);

Lời kết

Bật Postman lên, gõ email của anh em vào test thử xem thư có phi thẳng về Inbox (hoặc mục Spam 😅) chưa nhé.

Flow chuẩn của hệ thống là vậy: Mã gốc gửi qua mail, mã băm (hash) lưu trong Database. Ở bài sau (hoặc khi anh em tự làm phần đổi mật khẩu), lúc người dùng click vào link, ta chỉ cần lấy cái mã trên URL, đem đi băm bằng sha256 rồi so sánh với cái mã trong DB là xong. Rất an toàn và chuyên nghiệp!

Xong phần User bình thường rồi. Nhưng một trang thương mại điện tử thì phải có Admin để quản lý khách hàng chứ nhỉ? Chuyển bị tinh thần qua Lession 9: Get All User (Lấy danh sách người dùng dành cho Admin) nào.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí