Đăng nhập

📘 BÀI 21: ĐĂNG NHẬP / ĐĂNG KÝ NGƯỜI DÙNG + JWT AUTH

Đăng nhập, Đăng ký với JWT – Vue + NodeJS + MongoDB giúp ứng dụng của bạn bảo vệ dữ liệu cá nhân, cho phép mỗi người dùng chỉ xem và quản lý todo của riêng họ.

Mục tiêu:

  • Tạo API đăng ký và đăng nhập bằng NodeJS + MongoDB
  • Sử dụng JWT để xác thực người dùng
  • Vue lưu token trong localStorage
  • Bảo vệ API: mỗi người chỉ thấy todo của mình

Phần 1: Cập nhật Backend

1.1 Cài thêm thư viện:

npm install bcryptjs jsonwebtoken

1.2 Thêm User schema

// models/User.js
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')

const userSchema = new mongoose.Schema({
  username: { type: String, unique: true },
  password: String
})

// Mã hóa mật khẩu trước khi lưu
userSchema.pre('save', async function (next) {
  if (!this.isModified('password')) return next()
  this.password = await bcrypt.hash(this.password, 10)
  next()
})

userSchema.methods.comparePassword = function (pass) {
  return bcrypt.compare(pass, this.password)
}

module.exports = mongoose.model('User', userSchema)

1.3 Cập nhật index.js – tạo routes auth

const User = require('./models/User')
const jwt = require('jsonwebtoken')
const SECRET = 'vue-app-secret-key' // => nên dùng .env

// Đăng ký
app.post('/auth/register', async (req, res) => {
  const { username, password } = req.body
  try {
    const user = new User({ username, password })
    await user.save()
    res.json({ message: 'Đăng ký thành công' })
  } catch (e) {
    res.status(400).json({ error: 'Username đã tồn tại' })
  }
})

// Đăng nhập
app.post('/auth/login', async (req, res) => {
  const { username, password } = req.body
  const user = await User.findOne({ username })
  if (!user || !(await user.comparePassword(password))) {
    return res.status(401).json({ error: '❌ Sai tài khoản hoặc mật khẩu' })
  }

  const token = jwt.sign({ userId: user._id }, SECRET, { expiresIn: '7d' })
  res.json({ token })
})

1.4 Middleware xác thực JWT

function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1]
  if (!token) return res.status(401).json({ error: 'Unauthorized' })
  try {
    const decoded = jwt.verify(token, SECRET)
    req.userId = decoded.userId
    next()
  } catch (e) {
    return res.status(403).json({ error: 'Token không hợp lệ' })
  }
}

1.5 Gắn vào route /todos

const Todo = mongoose.model('Todo', {
  title: String,
  completed: Boolean,
  createdAt: { type: Date, default: Date.now },
  userId: String
})

// Đổi toàn bộ route /todos:
app.get('/todos', authMiddleware, async (req, res) => {
  const todos = await Todo.find({ userId: req.userId }).sort({ createdAt: -1 })
  res.json(todos)
})

app.post('/todos', authMiddleware, async (req, res) => {
  const todo = new Todo({ ...req.body, userId: req.userId })
  await todo.save()
  res.json(todo)
})

app.put('/todos/:id', authMiddleware, async (req, res) => {
  const todo = await Todo.findOneAndUpdate(
    { _id: req.params.id, userId: req.userId },
    req.body,
    { new: true }
  )
  res.json(todo)
})

app.delete('/todos/:id', authMiddleware, async (req, res) => {
  await Todo.findOneAndDelete({ _id: req.params.id, userId: req.userId })
  res.json({ message: 'Deleted' })
})

Phần 2: Cập nhật Vue Frontend

2.1 Tạo store authStore.js

import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    token: localStorage.getItem('token') || '',
    username: '',
    error: ''
  }),
  actions: {
    async login(username, password) {
      const res = await fetch('http://localhost:5000/auth/login', {
        method: 'POST',
        body: JSON.stringify({ username, password }),
        headers: { 'Content-Type': 'application/json' }
      })
      const data = await res.json()
      if (res.ok) {
        this.token = data.token
        localStorage.setItem('token', data.token)
        this.error = ''
      } else {
        this.error = data.error || 'Lỗi đăng nhập'
      }
    },
    async register(username, password) {
      const res = await fetch('http://localhost:5000/auth/register', {
        method: 'POST',
        body: JSON.stringify({ username, password }),
        headers: { 'Content-Type': 'application/json' }
      })
      const data = await res.json()
      if (!res.ok) {
        this.error = data.error || 'Lỗi đăng ký'
      }
    },
    logout() {
      this.token = ''
      localStorage.removeItem('token')
    }
  }
})

2.2 Dùng token khi gọi todo API

Trong todoStore.js, thay dòng fetch(API_URL) bằng:

fetch(API_URL, {
  headers: {
    Authorization: 'Bearer ' + localStorage.getItem('token')
  }
})

Áp dụng tương tự cho POST, PUT, DELETE.

2.3 Tạo component login/register đơn giản

Ví dụ: LoginView.vue – bạn có thể gọi authStore.login(...)
Và sau khi login thành công → gọi todoStore.fetchTodos() lại.

Bạn đã học được:

Kỹ năngVai trò
JWT AuthBảo mật API, phân quyền người dùng
Pinia authStoreQuản lý đăng nhập toàn cục
MiddlewareChặn truy cập nếu chưa login
Token lưu trong localStorageGhi nhớ đăng nhập

Bài tập mở rộng:

  1. Làm giao diện đẹp hơn cho login / register
  2. Hiển thị “Xin chào [username]” sau khi đăng nhập
  3. Tự động logout khi token hết hạn (dùng jwt-decode để kiểm tra)

Thảo luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Đăng ký nhận tin mới

Nhận bài học, tài nguyên và cơ hội việc làm qua email hàng tuần.

Chúng tôi cam kết không spam. Bạn có thể hủy bất cứ lúc nào.