[Series Bất động sản] Phần 2: Quên mật khẩu & Quản lý Hồ sơ với Node.js + Prisma
Chào các bạn, mình đã quay trở lại!
Ở Phần 1, chúng ta đã cùng nhau thiết lập xong hệ thống Đăng ký/Đăng nhập. Tuy nhiên, một hệ thống thực tế không thể thiếu tính năng giúp người dùng "tìm lại chính mình" khi lỡ quên mật khẩu, hoặc cập nhật ảnh đại diện lung linh để đi chốt đơn đất nền.
Trong phần này, chúng ta sẽ tận dụng triệt để các trường resetPwdToken và resetPwdExpiry mà chúng ta đã thiết kế trong Database Schema để xây dựng một luồng Forgot Password an toàn.
1. Cài đặt thêm thư viện Gửi Mail
Để gửi link reset mật khẩu, chúng ta cần Nodemailer.
npm install nodemailer
npm install @types/nodemailer --save-dev
Cập nhật file .env:
SMTP_HOST=smtp.mailtrap.io
SMTP_PORT=2525
SMTP_USER=your_user
SMTP_PASS=your_password
FRONTEND_URL=http://localhost:3000
2. Mailer Utility
Tạo file để cấu hình việc gửi mail.
File: src/utils/mailer.ts
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
export const sendResetPasswordEmail = async (email: string, token: string) => {
const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${token}`;
await transporter.sendMail({
from: '"BatDongSan Support" <noreply@bds.com>',
to: email,
subject: "Khôi phục mật khẩu của bạn",
html: `<b>Vui lòng click vào link sau để reset mật khẩu:</b> <a href="${resetUrl}">${resetUrl}</a>`,
});
};
3. Cập nhật Service: Logic Quên & Reset mật khẩu
Chúng ta sẽ thêm logic xử lý token và cập nhật profile.
File: src/services/auth.service.ts (Bổ sung)
import crypto from 'crypto';
import prisma from '../prisma/client';
import bcrypt from 'bcryptjs';
// ... (registerUser và loginUser giữ nguyên)
export const forgotPassword = async (email: string) => {
const user = await prisma.users.findUnique({ where: { email } });
if (!user) throw new Error('Không tìm thấy người dùng với email này');
const resetToken = crypto.randomBytes(32).toString('hex');
const expiry = new Date(Date.now() + 3600000); // 1 giờ sau sẽ hết hạn
await prisma.users.update({
where: { email },
data: {
resetPwdToken: resetToken,
resetPwdExpiry: expiry
}
});
return resetToken;
};
export const resetPassword = async (token: string, newPass: string) => {
const user = await prisma.users.findFirst({
where: {
resetPwdToken: token,
resetPwdExpiry: { gt: new Date() } // Kiểm tra token còn hạn
}
});
if (!user) throw new Error('Token không hợp lệ hoặc đã hết hạn');
const hashedPassword = await bcrypt.hash(newPass, 10);
await prisma.users.update({
where: { id: user.id },
data: {
password: hashedPassword,
resetPwdToken: null,
resetPwdExpiry: null
}
});
};
export const updateProfile = async (userId: number, data: { fullname?: string, phone?: string, avatar?: string }) => {
return await prisma.users.update({
where: { id: userId },
data: data
});
};
4. Validation Schema mới
File: src/schemas/auth.schema.ts (Bổ sung)
export const forgotPasswordSchema = z.object({
email: z.string().email()
});
export const resetPasswordSchema = z.object({
token: z.string(),
newPassword: z.string().min(6)
});
export const updateProfileSchema = z.object({
fullname: z.string().optional(),
phone: z.string().optional(),
avatar: z.string().url().optional()
});
5. Controller: Điều phối hành động
File: src/controllers/auth.controller.ts (Bổ sung)
import { Request, Response } from 'express';
import * as authService from '../services/auth.service';
import { sendResetPasswordEmail } from '../utils/mailer';
export const forgotPassword = async (req: Request, res: Response) => {
try {
const token = await authService.forgotPassword(req.body.email);
await sendResetPasswordEmail(req.body.email, token);
res.json({ message: 'Link reset mật khẩu đã được gửi qua email' });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
};
export const resetPassword = async (req: Request, res: Response) => {
try {
await authService.resetPassword(req.body.token, req.body.newPassword);
res.json({ message: 'Mật khẩu đã được cập nhật thành công' });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
};
export const updateProfile = async (req: any, res: Response) => {
try {
// Lưu ý: req.user.userId lấy từ Middleware xác thực JWT (sẽ làm ở phần sau)
const result = await authService.updateProfile(req.user.userId, req.body);
res.json({ message: 'Cập nhật hồ sơ thành công', user: result });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
};
6. Cập nhật Routes
File: src/routes/auth.routes.ts
import { Router } from 'express';
import * as authCtrl from '../controllers/auth.controller';
import { validate } from '../middlewares/validate.middleware';
import * as schemas from '../schemas/auth.schema';
// Import thêm middleware authenticate (giả định bạn đã có để bảo vệ route update)
const router = Router();
router.post('/register', validate(schemas.registerSchema), authCtrl.register);
router.post('/login', validate(schemas.loginSchema), authCtrl.login);
router.post('/forgot-password', validate(schemas.forgotPasswordSchema), authCtrl.forgotPassword);
router.post('/reset-password', validate(schemas.resetPasswordSchema), authCtrl.resetPassword);
// Route này cần thêm middleware verify token để lấy userId
// router.put('/update-profile', authenticate, validate(schemas.updateProfileSchema), authCtrl.updateProfile);
export default router;
Kết quả đạt được
Với việc hoàn thiện Phần 2, hệ thống Bất động sản của chúng ta đã có:
Luồng Forgot Password: Sinh token ngẫu nhiên, lưu vào DB và gửi email thật qua SMTP.
Reset Password: Xác thực token có còn trong hạn 1 giờ hay không trước khi cho đổi mật khẩu.
Update Profile: Cho phép môi giới cập nhật số điện thoại và ảnh đại diện.
All rights reserved