Node.js + TypeScript + MongoDB: JWT Authentication - Xác thực người dùng sử dụng JWT (Phần 1)



Lời nói đầu

Chào các bạn, đã quá lâu rồi chưa viết lách lại vì vậy đợt này mình sẽ viết một series về nodejs typescript,  trong bài học này, các bạn sẽ tìm hiểu cách xây dựng backend Node.js bằng TypeScript, triển khai xác thực và ủy quyền người dùng bằng JWT, lưu trữ dữ liệu trong Redis và tạo các Docker container bằng docker-compose.


Kiến trúc JWT Authentication được xây dựng với:

    • Node.js – một ngôn ngữ kịch bản chạy bằng JavaScript
    • Expressjs – hoạt động như một framework của Node.js
    • Typegoose – hoạt động như một bao bọc xung quanh Mongoose cho phép chúng ta viết các mô hình Mongoose với các lớp TypeScript.
    • Mongoose – một ODM (Object Document Mapping) để truy cập và thay đổi cơ sở dữ liệu
    • Bcryptjs – để băm mật khẩu
    • JsonWebToken – tạo JWT (Json Web Tokens)
    • Redis – là nơi lưu trữ caching để lưu trữ phiên người dùng
    • MongoDB – là cơ sở dữ liệu NoSQL
    • Zod – để xác thực đầu vào của người dùng
    • cors –  để cho phép Chia sẻ Tài nguyên giữa  backend và frontend.
    Yêu cầu tiên quyết
    • Node.js - để viết mã logic phía backend
    • Docker -  cho phép chúng ta đóng gói ứng dụng vào các container
    • MongoDB compass (tùy chọn) - Giao diện đồ họa cho việc truy vấn, thay đổi dữ liệu, phân tích và tổng hợp dữ liệu từ MongoDB.
    • RedisInsight-v2(tùy chọn) - Giao diện đồ hòa cho việc truy vấn, thay đổi, phân tích và tổng hợp dữ liệu Redis
    Thiết lập môi trường phát triển (Tùy chọn)
    1. Download và install Node.js, truy cập vào đây để xem hướng dẫn chi tiết.
    2. Download và install Docker, truy cập vào đây để xem hướng dẫn chi tiết.
    3. Download và install MongoDB Compass, truy cập vào đây
    4. Download và install RedisInsight, truy cập vào đây
    Trong bài viết này tôi sẽ hướng dẫn viết các chức năng sau sử dụng node.js, Redis, MongoDB, Typegoose, Docker
    1. Đăng ký tài khoản mới với các trường Tên, Email, Mật khẩu và Xác nhận Mật khẩu.
    2. Đăng nhập bằng thông tin Email và Mật khẩu.
    3. Lấy thông tin hồ sơ  nếu người dùng đã đăng nhập.
    4. Người quản trị có thể lấy thông tin của tất cả người dùng trong cơ sở dữ liệu.
    Dưới đây là các API Endpoints cần thiết cho REST API này:


    Luồng xác thực JWT với Redis, MongoDB và Node.js

    Đây là quy trình Xác thực JWT mà chúng ta sẽ thực hiện trong bài viết này. Người dùng truy cập ứng dụng của chúng ta trong trình duyệt và cung cấp tên người dùng và mật khẩu để đăng nhập vào ứng dụng của chúng ta.

    Ứng dụng frontend sau đó sẽ gửi yêu cầu đến backend với thông tin đăng nhập của người dùng. Backend sẽ xác thực người dùng và gửi lại một số cookies nếu thông tin đăng nhập hợp lệ.



    Sơ đồ bên dưới minh họa luồng đăng nhập của người dùng trong ứng dụng của chúng ta



    Sơ đồ bên dưới hiển thị quy trình đăng ký người dùng trong quy trình Xác thực JWT.


    Cấu trúc dự án
    jwt_authentication_authorization_node/
    ├── .vscode/
    │ └── extensions.json
    ├── config/
    │ ├── custom-environment-variables.ts
    │ └── default.ts
    ├── http/
    │ ├── getUsers.http-request
    │ ├── login.http-request
    │ ├── me.http-request
    │ └── register.http-request
    ├── src/
    │ ├── controllers/
    │ │ ├── auth.controller.ts
    │ │ └── user.controller.ts
    │ ├── middleware/
    │ │ ├── deserializeUser.ts
    │ │ ├── requireUser.ts
    │ │ ├── restrictTo.ts
    │ │ └── validate.ts
    │ ├── models/
    │ │ └── user.model.ts
    │ ├── routes/
    │ │ ├── auth.route.ts
    │ │ └── user.route.ts
    │ ├── schema/
    │ │ └── user.schema.ts
    │ ├── services/
    │ │ └── user.service.ts
    │ ├── utils/
    │ │ ├── appError.ts
    │ │ ├── connectDB.ts
    │ │ ├── connectRedis.ts
    │ │ └── jwt.ts
    │ └── app.ts
    ├── .env
    ├── .gitignore
    ├── docker-compose.yml
    ├── package.json
    ├── tsconfig.json
    └── yarn.lock
    Setup project
    Tạo 1 project mới init project nodejs
    mkdir jwt_authentication_authorization_node
    cd jwt_authentication_authorization_node
    # with npm 
    npm init
    Chạy lệnh bên dưới để cài đặt TypeScript như một phụ thuộc phát triển. Điều này sẽ cho phép chúng ta biên dịch mã TypeScript thành mã JavaScript thuần sử dụng trình biên dịch TypeScript.
    npm install -D typescript
    Chạy lệnh sau để khởi tạo dự án TypeScript. Tệp tsconfig.json sẽ được tạo trong thư mục gốc của bạn.
    npx tsc --init

    Cấu hình tệp TypeScript tsconfig.json
    Thêm các tùy chọn cấu hình sau vào tệp tsconfig.json của bạn để cho phép chúng ta sử dụng các decorators và tính năng khác trong mã của chúng ta.
    {
      "compilerOptions": {
        "target": "es2016",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "module": "commonjs",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "strictPropertyInitialization": false,
        "skipLibCheck": true
      }
    }
    Các cấu hình quan trọng trong tsconfig.json
    • experimentalDecorators: true
    • emitDecoratorMetadata: true
    • strictPropertyInitialization: false
    Cài đặt các thư viện cần thiết
    Cài đặt các Dependencies
    # npm
    npm install @typegoose/typegoose bcryptjs config cookie-parser dotenv express jsonwebtoken lodash mongoose redis ts-node-dev zod cors
    • dotenv – tải các biến môi trường từ tệp .env vào process.env
    • @typegoose/typegoose – cho phép viết các mô hình Mongoose với các lớp TypeScript
    • bcryptjs – để băm dữ liệu mật khẩu
    • config – cho phép chúng ta cung cấp các kiểu TypeScript cho các biến môi trường chúng ta nhập từ tệp .env
    • cookie-parser – để phân tích cú pháp các cookie trong tiêu đề yêu cầu và gắn chúng vào req.cookies
    • jsonwebtoken – để ký và xác minh JWTs
    • lodash – chứa các tiện ích giúp đơn giản hóa các nhiệm vụ lập trình thông thường.
    • ts-node-dev – cho phép chúng ta chạy máy chủ. Một giải pháp thay thế khác là nodemon và ts-node.
    Cài đặt các devDependencies
    # npm
    npm install -D morgan typescript
    • morgan là một thư viện trong Node.js được sử dụng để ghi lại thông tin về các yêu cầu HTTP được gửi đến máy chủ (server) 
    Cài đặt các Type Definition 

    Các tệp định nghĩa kiểu này là cần thiết để TypeScript hoạt động bình thường.
    # npm
    npm install -D @types/bcryptjs @types/config @types/cookie-parser @types/express @types/jsonwebtoken @types/lodash @types/morgan @types/node @types/cors
    Khởi tạo và Khởi động Express Server
    Tạo thư mục src và trong đó tạo tệp app.ts. Sao chép và dán mã nguồn mẫu cho server Express.
    Đây là một phần của mã mẫu để bạn bắt đầu server Express:

    src/app.ts
    require('dotenv').config();
    import express from 'express';
    import config from 'config';
    
    const app = express();
    
    const port = config.get<number>('port');
    app.listen(port, () => {
      console.log(`Server started on port: ${port}`);
    });
    Trong đoạn mã nguồn ở trên, tôi đã nhập gói dotenv và cấu hình nó ở cấp độ cao nhất của tệp app.ts.
    Sau đó, tôi tạo một phiên bản của lớp express và gọi phương thức listen với cổng mà chúng ta muốn chạy máy chủ và một hàm gọi lại.
    Bởi vì chúng ta đang sử dụng các biến môi trường, việc tạo một tệp .env trong thư mục gốc là hợp lý.
    Bây giờ hãy tạo một tệp .env trong thư mục gốc và thêm đoạn mã sau:

    .env
    
    NODE_ENV=development
    MONGODB_USERNAME=inovationthinking
    MONGODB_PASSWORD=password123
    MONGODB_DATABASE_NAME=jwtAuth
    
    ACCESS_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBd0dybHFGalc3cjdIaXo4UEExQ0wzbVN1ZHJ2OHFIcEtOUEpqK1RMUEhxNkVSUHRtCkV6MklBRWhzZnlobW9RWHJ5YVRjQkpVMEJGeUVzemdabmpmM2JLSHBYWkM4cHhQemJJTEZ5RjkzREZJaUJINmEKdDBQblhiSERqK3pacTYyWXJrYzN4WTI2a0tOekNJOTJnMnJFbE5vUnhtVk9FcUFvT0xGdEt4VVZqMGE5bm42MAp2SFpTNmJwbnhTOFhCWUVBUGRKMWZZODZUYVRIaFpCczY5RWhkcUFFRTBVWFB2MHRDVzk3SkdVNmdFa0ZleUJaCjF5WjBMaDFQeXp1aUZsNFZXUExSZEpBTWdtcElTcC9nRWRxTTFHUkoreGtvMGhUOXFSVnRSUnc2M21IbGc5YWsKRUxGMUVjU01lME5uL0ZGUkpSV1h1VEdkQjd6WlVjbURDZ3V1YmowUnZERTMzQnNDNm1LbEFod1NjSUx1QU5sZAoxRThNZlVZQzRrenJ0eFdUZkdESDRXZGtaRllyTG84UlIzdWs2elJlaWZBZHJqTjlYTWZQZ3BneGVRRThQRE1KCmx2MmdkdEZ5eU8vbDhYZGhDVFMrSStLaGZCOWUwakRVVkMveHV1d3JGY1BaVndHNnhEL0RuaHdnelVFRjZSczIKbGpEejNjcHhKUC9oa2JFTUVPYi8vekx3V0drZjhSNENXY1UyNjFnbHorSERjZWVBNDV4bVhsa3djVkNCOUdCcwp6aGJNWXNqUU9SdGUrTXZUdU1aSmFQZnVYTW1XRWxhREo2QUFkMkRsUWJBVG82bVBGU0ErRFgxb1RrZVU1UmlECndsNXBZeWsrOHNKNHNOM2F4WHAyMkFVMVlEWVE0SXozd0lPM1Fmb1pIditQTXFKekI2VWY2N0ZML1RrQ0F3RUEKQVFLQ0FnQjE1Z25wNk9WcFRBUkFVZGNGRk9sZXp4b0hMcEJWT3ZrVkVDQXBwUFE3dkhyWE9hTUZ6d0h5Q201UQpTNVQydlFZSWU3ZEVKNWZEeEZ5YTQ1anUxU1FKci91cGxQSEMvZnA5Vm5PUm5zejNBNnhNVExiSDdCZHIxV3dhClYrblh3M3AxN3JWQm11SGhsZ1Q2RGMxMElJdHJHV01peVJmWldjRExYQXVrQmpzN213QzhpSzU5ZTVLNkc3bFIKbk5UaVRuU3piSzBJemlYUFJWUHJodDcyYnlHdDZjWVZlSlFSeUZjOEhNNjdNanR5TjB2Z2NhWWFxamt0dUZBWQpHdVhxQnFQVjZKSm1kWXowcStLM3R0WTRtazBJSnBzZC9BQ0RHTkdFTk5qTEs4ejJUYzJ2eG1pb3dkTVZtL1RuCjRobHBCUHBQV3Jlb2hibk43K3pJckV4YWIyWGtuRXlHTE5DSkdWK2FSSG5PYjF4UmM5emI2SVEyMFpvcGs3c3AKd1ZFMkxSRWJrbWJTUVhNbVcyYmxpR3h3NFJPdXdIaytORTZuVUF1M2k0WGdOd0pzTGtwSTNpUEFyaXpaMmxidwpVV2NPUC9kMG4rWndTOEFGM25TTWQ4SDBIRmlCNUtSSXhFaW11ZW1MWDVXNFh3ZjZpa0tRSDl3bkt0aU9kamdjCnJVT0ZIN2RuMm1SeW54bDR6a2YwT3Z6eWhWY3ZBUDUyUG9SVVYycUF5bjJmY0w2WjFIMStHMlRma29WVmpacU4KWHNGbUlHTjFLMzdROXN6YU4zTXByNFlvQzBadFBUYkJzK0FiUVd0dnEzb2dqdjZrV0xKazR2NldaUnZEK3AwQgpKVDFxaDNrc1FRZFlIdkkwT2RwN2tRNXpnRzlacVM1NXV3NTNIQUl2MjZ4R3VoWFR3UUtDQVFFQStNbU5xZkpTClMyQ0ZsTTQ3TUd4SFc0Wmh2Q1RuTDEweUVSUEh0YzFNN20vcVllaUNkNmJLWkpPbGN5b2VqSEoveXFwNGFNcHoKaG8wU21nMStHOGJrZmpNek42Wm1ha0cxYjVQMlp3alRrMWovaHluSFJrWG05eWMyVmpERTNUVlFYWFp6ODVwTAp3UGYxTDBoSWVGb2xSbUQySUl1OTh2WVZXdS9GMUMxak1HYnFMQnhaV1IvUnNJdFlnN3JlL0x3ekI1VnBTb0xICkR1dWQzRlBPMXR4NXRHOWpzT3N1OVR2dXZaTVljT1ZqWC9FSUVwblpSYXRUUklZam9aZSs2RFRoaDdGMTZFNlgKUkNXNjlxT2Vrd09UUlZGYXZqL25KcVZ3NkZPMkM4OEdIUjFGd3lFUU92N1doMDFFaEQ4WCtJS2tVOWRWS0FhWApzUG5Ma3ZjVGhVRitCUUtDQVFFQXhmNzZ1bThYam5OU2NYTTBUM2RXS1VidWFYNWszTjMzQ3ZzYTZ6TmhISFZ5Cno4SDljamZORzJlanZHUU5vNXdZVGoxRkZMSFdzamt5ekxHWXlxS2NLRkJxbnZHck9KUVRyWC8ra21lcDJBK08KZzdhdkt1ZGR4NVNLSjV3UVE4bnEwV0FxVUhRNENvVURTRVVlNHFIemE4SDZvNjduemNwcW9IbTZoOGo1a1hqYwpEWTlqT0lYakFsSzgySDQ0UE5QWG1rMWo0YXdidzVwL3JtS2V0OE9lSWsvTkNpclRCMUdFZVJndkVMMXZYUHVyCnZHZzFOY2c0Vmp3RUFnOUtoQ1FScCtqb1pwTmFlQllXQWFodXpuTXhpaU9HODhHWE5iWXJTSHg5Wk9SRXk4aXIKZkk4U3BSaE1xTlNwQ3RNemJVWno3Vm9GV2JaeEFTeDBEdXVOSW1YMHBRS0NBUUVBNXFKYTNPaVMxK1AwRWg0WgppdXRtUDNmVmxRaVU5VGl0V0YyQTc0NFNPcHl2cVBKV09Mdjd0citWU3EwS1F1Tkdpc2Y3OWhGd2haUzBZUElQCkxZcjFlZlRYRDBrSWVvck51MUZzeE5uTzRqTklON0pJVldJcUdvZFVmUlNhL0FNWHJIMUtRdE9RVktUSnZIcUQKREdkdFZOQkFlNjF3ZXhNY2V2LzY0cGJzOUFzRUhiNXVLZ3d3WlR6WTRzM1RPSEx6ejV6NFRpWHNpVzF1RzducAo3dy9YRjZtSHZwUllKT25aaWc4YVFsYTFDRlUzU1o4c1o4VEszYVNJMVo0S1VkUHNHOUlzM3g0MFp1MmZaRlFNCmhuZHpDSGpCNmNydDY4ckZYK3R5d1lHN1JqUkQzd0FBdnVCT1dvSUwrWmxRRElaMzltMlNPUmZiZWlvb1NlY1oKUnBpUFRRS0NBUUFlUUNmVXBqYUdLQzUzY08rVUdKcU1jZTdwSlV1Sngwd0FYSDh2WWtrN0RPSyt4VmZReEovTQp5UmZtSjY5QnlRNlpuWmpaWVpaNDRtNVZnZWpqUk5idy9lQmNhbllMamV3M3ZPK0xOTlZwVW04bXhwbWF4NEMzCmhvVlpLZW4rUVhKa0RQcEtFb2VoYTlNbGpwSDZkRjM1bjhpSWk2ZVU5SkUzOVlFL1Q4QjVybXFJazlqSUFRUy8KRFI4WFFLbWMrWXplWVdhYVN5NXV3ME13eEphVll3amRHeTRybUlGbmc5Zm1uSUJNWVhVTFV0UlpVOTZWV2dMcApnZi9teEtsUTZTWGRicU5iVUxZbzFNOEY3OU1HTGVscXZxVFd4MFF3QzZZdlMvM29sVXZCaXVaUWdKZUxxOXZDCmk4Tk1DUnE1WG1ORjUxUWI4ZGp3SWZlVmMvMjdQTEtWQW9JQkFIQ05zQ0FCMi96RVc2bXZ4cDdHSUVXVzdGNnMKZ1gxaTdBVXRpZG12ZEtORCs3WFN4VHRnc1M4ai9DZGNabmJZRWNYdEVWeU1PeUU2MFVDSlJQZktzSmJBdXhDYQpWdmJRWFNrTW9MblZNaXI2aUZxalJ0Mitka0hMTC9iVU1Sa1hTSDAzT0pHSXZTOEovbVRBVGhtOUF6N2hkN3BTCm1UNlNHdFFhWDhhbHdoVVgzT1p6V2JWazVHZXEvMWFybVo4cEhBeTkxdWNHMW9rQy9ZRkE3RDFOM2pGOWVQVGwKMVBJVFlLVDJiQmhTMm1HcVM4VkVDWGZ4dFJDenpOVHZCTUhJbS94ckx2WFZYdmYxUVVCM1BXelA2alhseTlrNwpEbm1meE5YSVFubis5Z2cvcHlNU0t0OHNXN3AyMUVuSUR5WjFCSWtydS9YVjY5SEpTWkpZeUhKYURnbz0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0=
    ACCESS_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUF3R3JscUZqVzdyN0hpejhQQTFDTAozbVN1ZHJ2OHFIcEtOUEpqK1RMUEhxNkVSUHRtRXoySUFFaHNmeWhtb1FYcnlhVGNCSlUwQkZ5RXN6Z1puamYzCmJLSHBYWkM4cHhQemJJTEZ5RjkzREZJaUJINmF0MFBuWGJIRGorelpxNjJZcmtjM3hZMjZrS056Q0k5MmcyckUKbE5vUnhtVk9FcUFvT0xGdEt4VVZqMGE5bm42MHZIWlM2YnBueFM4WEJZRUFQZEoxZlk4NlRhVEhoWkJzNjlFaApkcUFFRTBVWFB2MHRDVzk3SkdVNmdFa0ZleUJaMXlaMExoMVB5enVpRmw0VldQTFJkSkFNZ21wSVNwL2dFZHFNCjFHUkoreGtvMGhUOXFSVnRSUnc2M21IbGc5YWtFTEYxRWNTTWUwTm4vRkZSSlJXWHVUR2RCN3paVWNtRENndXUKYmowUnZERTMzQnNDNm1LbEFod1NjSUx1QU5sZDFFOE1mVVlDNGt6cnR4V1RmR0RINFdka1pGWXJMbzhSUjN1awo2elJlaWZBZHJqTjlYTWZQZ3BneGVRRThQRE1KbHYyZ2R0Rnl5Ty9sOFhkaENUUytJK0toZkI5ZTBqRFVWQy94CnV1d3JGY1BaVndHNnhEL0RuaHdnelVFRjZSczJsakR6M2NweEpQL2hrYkVNRU9iLy96THdXR2tmOFI0Q1djVTIKNjFnbHorSERjZWVBNDV4bVhsa3djVkNCOUdCc3poYk1Zc2pRT1J0ZStNdlR1TVpKYVBmdVhNbVdFbGFESjZBQQpkMkRsUWJBVG82bVBGU0ErRFgxb1RrZVU1UmlEd2w1cFl5ays4c0o0c04zYXhYcDIyQVUxWURZUTRJejN3SU8zClFmb1pIditQTXFKekI2VWY2N0ZML1RrQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==
    
    Trong tệp .env, tôi đã thêm hai khóa public và private là các khóa token được mã hóa base64.
    Tôi cũng cung cấp các thông tin đăng nhập cơ sở dữ liệu MongoDB mà chúng ta sẽ cần cho container mongo docker.
    Tôi sẽ chỉ cho bạn cách tạo ra các khóa riêng tư và công khai sau này. Đồng thời, hãy chắc chắn rằng bạn thay đổi các thông tin đăng nhập cơ sở dữ liệu.
    Tiếp theo, hãy tạo một thư mục config trong thư mục gốc và tạo hai tệp có tên là default.ts và custom-environment-variables.ts trong thư mục config.
    Mở tệp default.ts và thêm mã sau đây:

    config/default.ts
    export default {
      port: 8000,
      accessTokenExpiresIn: 15,
      origin: 'http://localhost:3000',
    };
    Tiếp theo, hãy thêm mã sau vào tệp custom-environment-variables.ts.

    config/custom-environment-variables.ts
    export default {
      dbName: 'MONGODB_USERNAME',
      dbPass: 'MONGODB_PASSWORD',
      accessTokenPrivateKey: 'ACCESS_TOKEN_PRIVATE_KEY',
      accessTokenPublicKey: 'ACCESS_TOKEN_PUBLIC_KEY',
    };
    Tệp custom-environment-variables.ts sẽ cho phép chúng ta nhập các biến môi trường mà chúng ta đã định nghĩa trong tệp .env.

    Bây giờ hãy thêm lệnh start vào tệp package.json.
    "scripts": {
        "start": "ts-node-dev --respawn --transpile-only src/app.ts"
      }
    Nếu bạn đã làm theo tất cả các hướng dẫn ở trên, tệp pack.json cuối cùng của bạn sẽ trông như thế này:
    {
      "name": "jwt_authentication_authorization_nod",
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "scripts": {
        "start": "ts-node-dev --respawn --transpile-only src/app.ts"
      },
        ....
    }
    Cuối cùng, mở terminal của bạn và chạy tập lệnh bắt đầu để khởi động server Express.
    npm start
    
    Nhấp vào liên kết http://localhost:8000/healthChecker này và bạn sẽ thấy liên kết này trong tab mới.
    Khi bạn thấy thông báo Chào mừng, điều đó có nghĩa là bạn đã làm đúng mọi thứ.

    Cài đặt Redis and MongoDB with Docker Compose

    Tôi giả định rằng bạn đã cài đặt docker trên máy tính của mình. Trong thư mục gốc, tạo tệp docker-compose.yml và thêm mã bên dưới.
    docker-compose.yml
    version: '3.8'
    services:
      mongo:
        image: mongo:latest
        container_name: mongo
        environment:
          MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USERNAME}
          MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD}
          MONGO_INITDB_DATABASE: ${MONGODB_DATABASE_NAME}
        env_file:
          - ./.env
        volumes:
          - mongo:/data/db
        ports:
          - '6000:27017'
      redis:
        image: redis:latest
        container_name: redis
        ports:
          - '7000:6379'
        volumes:
          - redis:/data
    volumes:
      mongo:
      redis:
    Chú ý là phiên bản docker-compose của bạn phải > 2 nhé, nếu sử dụng version < 2 vui lòng update lên theo lệnh sau

    sudo apt update sudo apt install -y curl
      sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
        sudo chmod +x /usr/local/bin/docker-compose


          Bây giờ, hãy mở terminal của bạn và chạy lệnh bên dưới để sinh ra các container Mongo và Redis.
          # start the docker containers
          docker-compose up -d

          Kết nối với MongoDB Docker Container với Mongoose
          Vì chúng ta đã thiết lập và chạy cơ sở dữ liệu MongoDB, nên chúng ta cần kết nối ứng dụng Express của mình với Mongoose. Trong thư mục src, tạo một thư mục utils và trong thư mục utils này, hãy tạo một tệp connectDB.ts và dán mã này vào đó.
          src/utils/connectDB.ts
          import mongoose from 'mongoose';
          import config from 'config';
          
          const dbUrl = `mongodb://${config.get('dbName')}:${config.get(
            'dbPass'
          )}@localhost:6000/jwtAuth?authSource=admin`;
          
          const connectDB = async () => {
            try {
              await mongoose.connect(dbUrl);
              console.log('Database connected...');
            } catch (error: any) {
              console.log(error.message);
              setTimeout(connectDB, 5000);
            }
          };
          
          export default connectDB;
          
          URL kết nối cơ sở dữ liệu có cấu trúc như sau:
          const dbUrl = `mongodb://username:password@host:port/database?authSource=admin`
          
          NAMEPLACEHOLDERDESCRIPTION
          HosthostThe domain of the database server, eg: localhost
          PortportThe port on which the database server is running on, eg: 27017
          UserusernameThe database username
          PasswordpasswordThe password of the database user
          DatabasedatabaseThe name of the database
          OptionsauthSourceThe database to use when authenticating with user and pass
          Trên là bảng giải thích các attributes cần thiết

          Kết nối với Redis Docker Container
          Tiếp theo, hãy kết nối ứng dụng express của chúng ta với Redis container. Trong thư mục utils, tạo một tệp connectRedis.ts mới, sau đó sao chép và dán đoạn mã bên dưới vào đó.
          src/utils/connectRedis.ts
          import { createClient } from 'redis';
          
          const redisUrl = `redis://localhost:7000`;
          const redisClient = createClient({
            url: redisUrl,
          });
          
          const connectRedis = async () => {
            try {
              await redisClient.connect();
              console.log('Redis client connected...');
            } catch (err: any) {
              console.log(err.message);
              setTimeout(connectRedis, 5000);
            }
          };
          
          connectRedis();
          
          redisClient.on('error', (err) => console.log(err));
          
          export default redisClient;
          Dưới đây là phân tích những gì tôi đã thực hiện trong tệp connectRedis.ts:
          • Nhập hàm createClient từ thư viện redis.
          • Tạo chuỗi kết nối Redis và gán cho biến redisUrl.
          • Gọi hàm createClient và truyền vào một đối tượng với URL kết nối. Sau đó, tôi gán đối tượng được trả về bởi hàm createClient cho biến redisClient.
          • Tiếp theo, tôi đã tạo hàm connectRedis và gọi phương thức connect trên đối tượng redisClient.
          • Cuối cùng, tôi đã sử dụng setTimeout để gọi hàm connectRedis sau mỗi 5 giây khi kết nối thất bại.
          Bây giờ, hãy mở tệp app.ts và nhập hàm connectDB mà chúng ta đã định nghĩa trong tệp connectDB.ts, sau đó gọi nó dưới câu lệnh console.log() trong hàm callback mà chúng ta truyền vào hàm listen.
          src/app.ts
          require('dotenv').config();
          import express from 'express';
          import config from 'config';
          import connectDB from './utils/connectDB';
          
          const app = express();
          
          const port = config.get<number>('port');
          app.listen(port, () => {
            console.log(`Server started on port: ${port}`);
            // ? call the connectDB function here
            connectDB();
          });
          Bạn sẽ thấy thông báo kết nối DB trong terminal giả sử máy chủ của bạn vẫn đang chạy.

          Tạo database schema với Typegoose
          Bây giờ, hãy tạo một thư mục models trong thư mục src và trong thư mục models, hãy tạo tệp user.model.ts. 
          Typegoose sử dụng rất nhiều decorators để định nghĩa model Mongoose.
          src/models/user.model.ts
          import {
            DocumentType,
            getModelForClass,
            index,
            modelOptions,
            pre,
            prop,
          } from '@typegoose/typegoose';
          import bcrypt from 'bcryptjs';
          
          @index({ email: 1 })
          @pre<User>('save', async function () {
            // Hash password if the password is new or was updated
            if (!this.isModified('password')) return;
          
            // Hash password with costFactor of 12
            this.password = await bcrypt.hash(this.password, 12);
          })
          @modelOptions({
            schemaOptions: {
              // Add createdAt and updatedAt fields
              timestamps: true,
            },
          })
          
          // Export the User class to be used as TypeScript type
          export class User {
            @prop()
            name: string;
          
            @prop({ unique: true, required: true })
            email: string;
          
            @prop({ required: true, minlength: 8, maxLength: 32, select: false })
            password: string;
          
            @prop({ default: 'user' })
            role: string;
          
            // Instance method to check if passwords match
            async comparePasswords(hashedPassword: string, candidatePassword: string) {
              return await bcrypt.compare(candidatePassword, hashedPassword);
            }
          }
          
          // Create the user model from the User class
          const userModel = getModelForClass(User);
          export default userModel;
          Phân tích những gì tôi đã làm ở trên: 
          • Tôi đã tạo một class user và thêm tất cả các thuộc tính mà model của chúng tôi yêu cầu với các decorator Typegoose và export class đó. 
          • Tôi đã sử dụng utility function getModelForClass để tạo model Mongoose từ class User mà tôi đã xác định ở trên. 
          • Tôi đã sử dụng pre-save hook để băm mật khẩu chỉ khi mật khẩu mới hoặc đã được sửa đổi. 
          • Sau đó tôi đã thêm trường email làm index.
          Kết thúc phần 1, hẹn các bạn ở phần 2.

          Nhận xét

          Bài đăng phổ biến từ blog này

          Cài đặt SSL cho website sử dụng certbot

          Xây dựng một hệ thống comment real-time hoặc chat đơn giản sử dụng Pusher

          CÁC BÀI TẬP SQL CƠ BẢN - PART 1

          Xây dựng một hệ thống tracking hành vi người dùng (phần 1)

          Xây dựng một hệ thống tracking hành vi người dùng (phần 2)

          Enterprise architecture trên 1 tờ A4

          Web caching (P2)

          Bàn về async/await trong vòng lặp javascript

          Web caching (P1)

          Cài đặt môi trường để code website Rails