+1

[Series] Xây dựng Hệ thống Bất động sản với Node.js & TypeScript - Bài 2: Triển khai Authentication (Đăng ký & Đăng nhập)

Chào anh em! Ở bài 1 chúng ta đã có Database. Nhưng để người dùng có thể đăng tin, lưu tin hay đánh giá, họ cần một "tấm thẻ căn cước" trong hệ thống. Bài viết này sẽ giúp bạn xây dựng hệ thống Auth bảo mật, có validate dữ liệu chặt chẽ và cấu trúc code chuẩn chỉnh.

1. Kiến trúc thư mục và Cài đặt

Một project lớn cần sự ngăn nắp. Chúng ta sẽ tổ chức code theo pattern Controller - Service - Route.

Cài đặt Dependencies Mở terminal và "vã" ngay bộ lệnh này:

# Khởi tạo project
npm init -y

# Dependencies chính
npm install express bcryptjs jsonwebtoken zod dotenv @prisma/client

# Dev Dependencies (Cho TypeScript)
npm install prisma typescript ts-node-dev @types/express @types/bcryptjs @types/jsonwebtoken --save-dev

2. Cấu hình Prisma & Schema

Chúng ta sẽ sử dụng Prisma làm ORM để tương tác với Database đã thiết kế ở Bài 1.

File: prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql" // Có thể thay bằng mysql nếu bạn thích
  url      = env("DATABASE_URL")
}

model users {
  id             Int       @id @default(autoincrement())
  email          String    @unique
  fullname       String
  phone          String
  emailVerified  Boolean   @default(false)
  phoneVerified  Boolean   @default(false)
  password       String
  avatar         String?
  balance        BigInt    @default(0)
  score          Int       @default(0)
  resetPwdToken  String?
  resetPwdExpiry DateTime?
  idPricing      Int       @default(1) // Mặc định gói miễn phí
}

Lưu ý: Sau khi sửa schema, đừng quên chạy lệnh: npx prisma generatenpx prisma migrate dev --name init_auth

3. Validation với Zod & Middleware

Đừng bao giờ tin tưởng dữ liệu từ Client! Chúng ta dùng Zod để định nghĩa schema và một Middleware để chặn các request "lỗi" ngay từ cửa ngõ.

File: src/schemas/auth.schema.ts

import { z } from 'zod';

export const registerSchema = z.object({
  email: z.string().email("Email không hợp lệ"),
  password: z.string().min(6, "Mật khẩu phải ít nhất 6 ký tự"),
  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ệ")
});

export const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(6)
});

File: src/middlewares/validate.middleware.ts

import { Request, Response, NextFunction } from 'express';
import { ZodSchema } from 'zod';

export const validate = (schema: ZodSchema) => {
  return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      schema.parse(req.body);
      next();
    } catch (e: any) {
      res.status(400).json({ error: e.errors });
    }
  };
    };

4. Xử lý Logic tại Service Layer

Service là nơi chứa "trí thông minh" của ứng dụng. Chúng ta xử lý Hash password bằng bcryptjs và tạo Token bằng jsonwebtoken.

File: src/services/auth.service.ts

import prisma from '../prisma/client';
import bcrypt from 'bcryptjs';
import { signToken } from '../utils/jwt';

export const registerUser = async (email: string, password: string, fullname: string, phone: string) => {
  const existingUser = await prisma.users.findUnique({ where: { email } });
  if (existingUser) throw new Error('Email đã tồn tại trên hệ thống');

  const hashedPassword = await bcrypt.hash(password, 10);

  const newUser = await prisma.users.create({
    data: { email, password: hashedPassword, fullname, phone, idPricing: 1 }
  });

  const token = signToken({ userId: newUser.id, email: newUser.email });
  return { user: newUser, token };
};

export const loginUser = async (email: string, password: string) => {
  const user = await prisma.users.findUnique({ where: { email } });
  if (!user) throw new Error('Email hoặc mật khẩu không chính xác');

  const isMatch = await bcrypt.compare(password, user.password);
  if (!isMatch) throw new Error('Email hoặc mật khẩu không chính xác');

  const token = signToken({ userId: user.id, email: user.email });
  return { user, token };
};

5. Hoàn thiện Controller & Routes

Cuối cùng, chúng ta kết nối mọi thứ lại với nhau.

File: src/controllers/auth.controller.ts

import { Request, Response } from 'express';
import { registerUser, loginUser } from '../services/auth.service';

export const register = async (req: Request, res: Response) => {
  try {
    const { email, password, fullname, phone } = req.body;
    const result = await registerUser(email, password, fullname, phone);
    
    // Lưu ý: BigInt cần convert sang String trước khi trả về JSON
    res.status(201).json(JSON.parse(JSON.stringify(result, (key, value) =>
      typeof value === 'bigint' ? value.toString() : value
    )));
  } catch (err: any) {
    res.status(400).json({ error: err.message });
  }
};
// ... Tương tự cho login

File: src/routes/auth.routes.ts

import { Router } from 'express';
import { register, login } from '../controllers/auth.controller';
import { validate } from '../middlewares/validate.middleware';
import { registerSchema, loginSchema } from '../schemas/auth.schema';

const router = Router();

router.post('/register', validate(registerSchema), register);
router.post('/login', validate(loginSchema), login);

export default router;

6. Tổng kết

Vậy là chúng ta đã hoàn thành hệ thống Authentication cơ bản nhưng cực kỳ chắc chắn cho dự án Bất động sản:

Bảo mật: Password được hash 10 vòng với Bcrypt.

Xác thực: Sử dụng JWT (JSON Web Token) thời hạn 7 ngày.

Tin cậy: Dữ liệu đầu vào được Validate nghiêm ngặt bởi Zod.


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í