[Series] Xây dựng Hệ thống Bất động sản với Node.js & TypeScript - Bài 4: Quản lý Profile & "Người gác cổng" Middleware
Trong một hệ thống, không phải ai cũng có quyền xem thông tin của ai. Để lấy được thông tin cá nhân của mình, người dùng phải chìa ra "tấm thẻ" JWT mà chúng ta đã cấp ở Bài 2. Hôm nay, mình sẽ hướng dẫn anh em xây dựng một "người gác cổng" (Middleware) để kiểm soát việc này.
1. Xây dựng "Người gác cổng" (Auth Middleware)
Middleware này có nhiệm vụ: Chặn mọi request không có Token, kiểm tra Token có hợp lệ không, và trích xuất thông tin người dùng gắn vào req để các hàm sau sử dụng.
File: src/middlewares/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
// Định nghĩa Interface mở rộng cho Request để chứa thông tin user
export interface AuthRequest extends Request {
user?: {
userId: number;
email: string;
};
}
export const authenticate = (req: AuthRequest, res: Response, next: NextFunction): void => {
const authHeader = req.headers.authorization;
// Kiểm tra Header Authorization có dạng 'Bearer <token>' không
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json({ error: 'Bạn cần đăng nhập để thực hiện hành động này 🔐' });
return;
}
const token = authHeader.split(' ')[1];
try {
// Giải mã token
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {
userId: number;
email: string;
};
// Gắn thông tin đã giải mã vào request
req.user = decoded;
next(); // Cho phép đi tiếp vào Controller
} catch (err) {
res.status(401).json({ error: 'Phiên làm việc hết hạn hoặc Token không hợp lệ ❌' });
}
};
2. Controller xử lý Profile
Ở đây có một lưu ý nhỏ: Cột balance của chúng ta là kiểu BigInt (để chứa số tiền lớn). JavaScript mặc định không thể JSON.stringify kiểu này, nên chúng ta cần convert nó sang String.
File: src/controllers/user.controller.ts
import { Response } from 'express';
import { AuthRequest } from '../middlewares/auth.middleware';
import prisma from '../prisma/client';
// 1. Lấy thông tin cá nhân
export const getProfile = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const user = await prisma.users.findUnique({
where: { id: req.user!.userId },
select: {
id: true,
email: true,
fullname: true,
phone: true,
avatar: true,
balance: true,
score: true,
emailVerified: true,
phoneVerified: true,
idPricing: true
}
});
if (!user) {
res.status(404).json({ error: "Không tìm thấy người dùng" });
return;
}
// Xử lý lỗi BigInt cho balance
const safeUser = {
...user,
balance: user.balance.toString()
};
res.status(200).json(safeUser);
} catch (err) {
res.status(500).json({ error: "Lỗi hệ thống khi lấy thông tin" });
}
};
// 2. Cập nhật thông tin
export const updateProfile = async (req: AuthRequest, res: Response): Promise<void> => {
const { fullname, phone, avatar } = req.body;
try {
const updatedUser = await prisma.users.update({
where: { id: req.user!.userId },
data: { fullname, phone, avatar }
});
res.status(200).json({
message: 'Cập nhật thông tin thành công ✨',
user: {
...updatedUser,
balance: updatedUser.balance.toString()
}
});
} catch (err) {
res.status(500).json({ error: 'Lỗi khi cập nhật thông tin' });
}
};
3. Khai báo Route & Validation
Để dữ liệu gửi lên sạch sẽ, mình dùng thêm Schema cho việc Update.
File: src/schemas/user.schema.ts
import { z } from 'zod';
export const updateProfileSchema = z.object({
fullname: z.string().min(2, "Tên quá ngắn"),
phone: z.string().min(8, "Số điện thoại không hợp lệ").optional(),
avatar: z.string().url("Link avatar không hợp lệ").optional().or(z.literal('')),
});
File: src/routes/user.routes.ts
import { Router } from 'express';
import * as UserController from '../controllers/user.controller';
import { authenticate } from '../middlewares/auth.middleware';
import { validate } from '../middlewares/validate.middleware';
import { updateProfileSchema } from '../schemas/user.schema';
const router = Router();
// Các route này đều cần 'authenticate' gác cổng
router.get('/me', authenticate, UserController.getProfile);
router.put('/me', authenticate, validate(updateProfileSchema), UserController.updateProfile);
export default router;
Đừng quên đăng ký route này vào app.ts nhé:
import userRoutes from './routes/user.routes';
app.use('/api/users', userRoutes);
4. Hướng dẫn Test Postman (Chuẩn không cần chỉnh)
Nhiều anh em hay bị lỗi 401 Unauthorized ở bước này, hãy làm theo đúng quy trình:
Bước 1: Lấy Token (Login) Chạy API Đăng nhập ở Bài 2.
Copy đoạn mã trong trường token trả về.
Bước 2: Test Get Profile
Method: GET
URL: http://localhost:3000/api/users/me
Tab Authorization:
Chọn Type: Bearer Token
Dán đoạn mã Token vừa copy vào ô Token.
Nhấn Send. Bạn sẽ thấy thông tin cá nhân của chính mình hiện ra.
Bước 3: Test Update Profile
Method: PUT
URL: http://localhost:3000/api/users/me
Tab Authorization: Vẫn để Bearer Token như trên.
Tab Body (raw JSON):
{
"fullname": "Huy Hoàng Senior",
"phone": "0333444555",
"avatar": "https://i.pravatar.cc/300"
}
Nhấn Send. Dữ liệu trong Database sẽ được cập nhật ngay lập tức!
5. Tổng kết
Chúc mừng anh em! Chúng ta đã xây dựng xong cơ chế bảo mật cơ bản và quản lý thông tin người dùng. Đây là nền tảng để những bài sau chúng ta xác định được: "Ai là người đang đăng tin bất động sản này?".
All rights reserved