wordpress_blog

This is a dynamic to static website.

Complete User Registration, Login & Logout with React JWT, Bcrypt Password | Nodejs | Mern Stack

Learning From Youtube Channel: Code Info
Video: Complete User Registration, Login & Logout with React JWT, Bcrypt Password | Nodejs | Mern Stack
Thank you.

專案開始和安裝

Server

  • 建立 authentication-app 資料夾
  • 在 authentication-app 資料夾裡面建立 client 資料夾
  • 在 authentication-app 資料夾裡面建立 server 資料夾
  • 移動到 server 資料夾 – cd server
  • npm 初始化建立 package.json 檔案
    description: authentication app
    auther: your name
  • 安裝 express 套件 – npm i express
  • 在 server 資料夾裡面建立 index.js 檔案
  • 修改 index.js 檔案
  • 安裝套件
    npm i mongoose cors jsonwebtoken
  • 修改 index.js 檔案
  • 執行終端機 – nodemon index.js
  • 開啟 MongoDB Compass > connection
  • 在 server 資料夾裡面建立 models 資料夾
  • 在 models 資料夾裡面建立 userModel.js 檔案
  • 修改 userModel.js 檔案
  • 在 server 資料夾裡面建立 controllers 資料夾
  • 在 controllers 資料夾裡面建立 authController.js 檔案
  • 修改 index.js 檔案,GLOBAL ERROR HANDLER
  • 在 server 資料夾裡面建立 utils 資料夾
  • 在 utils 資料夾裡面建立 appError.js 檔案
  • 修改 appError.js 檔案
  • 修改 authController.js 檔案
  • 安裝 bcryptjs 套件 – npm i bcryptjs
  • 在 server 資料夾裡面建立 routes 資料夾
  • 在 routes 資料夾裡面建立 authRoute.js 檔案
  • 修改 index.js 檔案,新增 authRouter 變數、修改 ROUTE
  • 使用 API 測試工具
    http://localhost:3000/api/auth/signup,POST 方法
    Body > JSON
  • 修改 authController.js 檔案,LOGGING USER
  • 使用 API 測試工具
    http://localhost:3000/api/auth/login,POST 方法
    Body > JSON
  • 修改 authController.js 檔案,trycatch
  • 使用 API 測試工具
    http://localhost:3000/api/auth/login,POST 方法
  • 使用 API 測試工具
    http://localhost:3000/api/auth/signup,POST 方法
    Body > JSON
  • 修改 authController.js 檔案
// server/index.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const authRouter = require('./routes/authRoute');
const app = express();

// 1. MIDDLEWARES
app.use(cors());
app.use(express.json());

// 2. ROUTE
app.use('/api/auth', authRouter);

// 3. MONGO DB CONNECTION
mongoose.connect('mongodb://127.0.0.1:27017/authentication')
  .then(() => console.log('Connected to MongoDB!'))
  .catch((error) => console.error('Failed to connect to MongoDB:', error));

// 4. GLOBAL ERROR HANDLER
app.use((err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';

  res.status(err.statusCode).json({
    status: err.status,
    message: err.message,
  });
});

// 5. SERVER
const PORT = 3000;
app.listen(PORT, () => {
  console.log(`App running on ${PORT}`);
});
// models/userModel.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
  },
  email: {
    type: String,
    unique: true,
    required: true,
  },
  role: {
    type: String,
    default: 'user',
  },
  password: {
    type: String,
    required: true,
  },
});

const User = mongoose.model('User', userSchema);

module.exports = User;
// server/utils/appError.js
class createError extends Error{
  constructor(message, statusCode) {
    super(message);

    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';

    Error.captureStackTrace(this, this.constructor);
  }
}

module.exports = createError;
// server/controllers/authController.js
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

const User = require('../models/userModel');
const createError = require('../utils/appError');

// REGISTER USER
exports.signup = async (req, res, next) => {
  try {
    const user = await User.findOne({ email: req.body.email });

    if (user) {
      return next(new createError('User already exists!', 400));
    }
    const hashedPassword = await bcrypt.hash(req.body.password, 12);

    const newUser = await User.create({
      ...req.body,
      password: hashedPassword,
    });

    // Assign JWT ( json web token)
    const token = jwt.sign({_id: newUser._id}, "secretkey123", {
      expiresIn: '90d',
    });

    res.status(201).json({
      status: 'success',
      message: 'User registered successfully',
      token,
      user: {
        _id: newUser._id,
        name: newUser.name,
        email: newUser.email,
        role: newUser.role,
      },
    });
  } catch (error) {
    next(error);
  }
};

// LOGGING USER
exports.login = async (req, res, next) => {
  try {
    const { email, password } = req.body;

    const user = await User.findOne({ email });

    if (!user) return next(new createError('User not found!', 404));

    const isPasswordValid = await bcrypt.compare(password, user.password);

    if (!isPasswordValid) {
      return next(new createError('Invalid email or password', 401));
    }

    const token = jwt.sign({_id: user._id}, "secretkey123", {
      expiresIn: '90d',
    });

    res.status(200).json({
      status: 'success',
      token,
      message: 'Logged in successfully',
      user: {
        _id: user._id,
        name: user.name,
        email: user.email,
        role: user.role,
      },
    })
  } catch (error) {
    next(error);
  }
};
// server/routes/authRoute.js
const express = require('express');
const authController = require('../controllers/authController');

const router = express.Router();

router.post('/signup', authController.signup);
router.post('/login', authController.login);

module.exports = router;
// Body > JSON - 1
{
  "name": "test user",
  "email": "test1@gmail.com",
  "password": "123",
}
// Body > JSON - 2
{
  "name": "test user",
  "email": "test2@gmail.com",
  "password": "test@123"
}
// Body > JSON - 3
{
  "name": "test user 3",
  "email": "test3@gmail.com",
  "password": "123"
}

Client

  • 分別開兩個終端機
    1個是 server、1個是 client
    移動到 server – cd server
    移動到 cleint – cd client
  • 使用 Vite 快速建立 React
    npm create vite@latest .
  • Select a framework: Reactt
    Select a variant: JavaScript
    npm install
    npm run dev
  • 搜尋 Ant Design
  • 使用 npm 安裝 antd、react-router-dom 套件
    npm i antd react-router-dom
  • 修改 src/App.jsx 檔案
    使用 rafce 片段快速建立程式碼
  • 在 src 資料夾裡面建立 Auth 資料夾
  • 在 Auth 資料夾裡面建立 Register.jsx 檔案
  • 使用 rafce 片段快速建立程式碼
    修改 Auth/Register.jsx 檔案
  • 在 Auth 資料夾裡面建立 Login.jsx 檔案
  • 使用 rafce 片段快速建立程式碼
    修改 Auth/Login.jsx 檔案
  • 在 src 資料夾裡面建立 pages 資料夾
  • 在 pages 資料夾裡面建立 Dashboard.jsx 檔案
  • 使用 rafce 片段快速建立程式碼
    修改 pages/Dashboard.jsx 檔案
  • 修改 App.jsx 檔案
  • 修改 App.css 檔案
  • 修改 Register.jsx 檔案,修改 <Card>、<Flex>
  • 修改 App.css 檔案, <form>
  • 修改 Register.jsx 檔案,img 部分
  • 下載 register.png 檔案,尋找替代的圖片
  • 修改 App.css 檔案,.auth-image
  • 修改 Register.jsx 檔案,Alert 部分
  • 修改App.css 檔案 .alert 部分
  • 修改 Register.jsx 檔案,loading 部分
  • 複製 Register.jsx 檔案,<Card> 部分、import 部分
    貼到 Login.jsx 檔案
  • 修改 Login.jsx 檔案
  • 下載 login.png 檔案,尋找替代的圖片
  • 在 src 資料夾裡面建立 hooks 資料夾
  • 在 src 資料夾裡面建立 contexts 資料夾
  • 在 hooks 資料夾裡面建立 useSignup.js 檔案
  • 使用 rafce 片段快速建立程式碼
    修改 useSignup.js 檔案
  • 在建立帳戶的頁面填寫表單測試 console 查詢
  • 在 contexts 資料夾裡面建立 AuthContext.js 檔案
  • 使用 rafce 片段快速建立程式碼
    修改 AuthContext.js 檔案
  • 修改 userSignup.js 檔案
  • 刪除 index.css 檔案
    修改 main.jsx 檔案
  • 修改 useSignup.js 檔案
  • 修改 Register.jsx 檔案,registerUser、loading、error 部分
  • AuthContext.js 檔案名稱修改成 AuthContext.jsx 檔案
  • 修改 main.jsx 檔案,匯入 AuthProvider
  • 修改 useSignup.jsx 檔案,匯入 useAuth
  • 修改 AuthContext.jsx 檔案,匯入 useState
  • Debug 排除錯誤程式碼
  • 在建立帳戶的頁面填寫資料送出測試
  • 執行終端機 server – nodemon index.js
  • 修改 useSignup.js 檔案,增加 headers、修改 setLoading 改為 true
  • 修改 App.jsx 檔案,<Route />
  • 輸入 http://localhost:5173/dashboard 前往 dashboard 頁面
  • 修改 Dashboard.jsx 檔案
  • 在 hook 資料夾裡面建立 useLogin.js 檔案
  • 修改 useLogin.js 檔案
    複製 useSignup.js 檔案程式碼貼到 useLogin 檔案修改
  • 修改 Login.jsx 檔案
  • 修改 useLogin.js 檔案,修改 res.status 狀態碼
  • 測試註冊和登入功能是否能正常運行
  • 修改 Dashboard.jsx 檔案
  • 修改 App.css 檔案
  • 測試註冊、登入、登出功能是否都能正常運作
// src/App.jsx
import React from 'react';
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import './App.css';
import Register from './Auth/Register';
import Login from './Auth/Login';
import Dashboard from './pages/Dashboard';
import { useAuth } from './contexts/AuthContext';

const App = () => {
  const {isAuthenticated} = useAuth();
  return (
    <Router>
      <Routes>
        <Route
          path='/'
          element={
            !isAuthenticated ? <Register /> : <Navigate to='/dashboard' />
          } 
        />
        <Route
          path='/login'
          element={ !isAuthenticated ? <Login /> : <Navigate to='/dashboard' />}
        />
        <Route
          path='/dashboard'
          element={ isAuthenticated ? <Dashboard /> : <Login />}
        />
      </Routes>
    </Router>
  )
}

export default App
// Auth/Register.jsx
import React from 'react'
import { Alert, Card, Flex, Form, Typography, Input, Spin, Button } from 'antd';
import { Link } from 'react-router-dom';
import registerImage from '../assets/register.jpg';
import useSignup from '../hooks/useSignup';

const Register = () => {
  const { loading, error, registerUser } = useSignup();
  const handleRegister = (values) => {
    registerUser(values);
  };

  return (
    <Card className='form-container'>
      <Flex gap='large' align='center'>
        {/* Form */}
        <Flex vertical flex={1}>
          <Typography.Title level={3} strong className='title'>
            Create an account
          </Typography.Title>
          <Typography.Text type='secondary' strong className='slogan'>Join for exclusive access</Typography.Text>
          <Form
            layout="vertical"
            onFinish={handleRegister}
            autoComplete="off">
              <Form.Item
                label="Full Name"
                name="name"
                rules={[
                  {
                    required: true,
                    message: 'Please input your full name!'
                  },
                ]}>
                <Input size='large' placeholder="Enter your full name" />
              </Form.Item>
              <Form.Item
                label="Email"
                name="email"
                rules={[
                  {
                    required: true,
                    message: 'Please input your Email!'
                  },
                  {
                    type: 'email',
                    message: 'The input is not valid Email'
                  }
                ]}>
                <Input size='large' placeholder="Enter your email" />
              </Form.Item>
              <Form.Item
                label="Password"
                name="password"
                rules={[
                  {
                    required: true,
                    message: 'Please input your Password!'
                  },
                ]}>
                <Input.Password size='large' placeholder="Enter your password" />
              </Form.Item>
              <Form.Item
                label="Password"
                name="passwordConfirm"
                rules={[
                  {
                    required: true,
                    message: 'Please input your Confirm Password!'
                  },
                ]}>
                <Input.Password size='large' placeholder="Re-enter your password" />
              </Form.Item>

              {error && (
                  <Alert
                    description={error}
                    type='error'
                    showIcon
                    closable
                    className='alert'
                  />
                )}

              <Form.Item>
                <Button
                  type={`${loading ? '' : 'primary'}`}
                  htmlType="submit"
                  size="large"
                  className="btn">
                    {loading ? <Spin /> : 'Create Account'}
                  </Button>
              </Form.Item>
              <Form.Item>
                <Link to="/login">
                  <Button size="large" className='btn'>Sign In</Button>
                </Link>
              </Form.Item>
          </Form>
        </Flex>

        {/* Image */}
        <Flex flex={1}>
          <img src={registerImage} className='auth-image' />
        </Flex>
      </Flex>
    </Card>
  )
}

export default Register
// Auth/Login.jsx
import React from 'react'
import { Alert, Card, Flex, Form, Typography, Input, Spin, Button } from 'antd';
import { Link } from 'react-router-dom';
import loginImage from '../assets/login.jpg';
import useLogin from '../hooks/useLogin';

const Login = () => {
  const { error, loading, loginUser } = useLogin();
  const handleLogin = async (values) => {
    await loginUser(values);
  };

  return (
    <Card className='form-container'>
      <Flex gap='large' align='center'>
        {/* Image */}
        <Flex flex={1}>
          <img src={loginImage} className='auth-image' />
        </Flex>
        {/* Form */}
        <Flex vertical flex={1}>
          <Typography.Title level={3} strong className='title'>
            Sign In
          </Typography.Title>
          <Typography.Text type='secondary' strong className='slogan'>Unlock you world.</Typography.Text>
          <Form
            layout="vertical"
            onFinish={handleLogin}
            autoComplete="off">
              
              <Form.Item
                label="Email"
                name="email"
                rules={[
                  {
                    required: true,
                    message: 'Please input your Email!'
                  },
                  {
                    type: 'email',
                    message: 'The input is not valid Email'
                  }
                ]}>
                <Input size='large' placeholder="Enter your email" />
              </Form.Item>
              <Form.Item
                label="Password"
                name="password"
                rules={[
                  {
                    required: true,
                    message: 'Please input your Password!'
                  },
                ]}>
                <Input.Password size='large' placeholder="Enter your password" />
              </Form.Item>
              
              {error && (
                <Alert
                  description={error}
                  type='error'
                  showIcon
                  closable
                  className='alert'
                />
              )}

              <Form.Item>
                <Button
                  type={`${loading ? '' : 'primary'}`}
                  htmlType="submit"
                  size="large"
                  className="btn">
                    {loading ? <Spin /> : 'Sign In'}
                  </Button>
              </Form.Item>
              <Form.Item>
                <Link to="/">
                  <Button size="large" className='btn'>Create  an account</Button>
                </Link>
              </Form.Item>
          </Form>
        </Flex>
      </Flex>
    </Card>
  )
}

export default Login
// src/pages/Dashboard.jsx
import React from 'react'
import { Avatar ,Button, Card, Flex, Typography } from 'antd'
import { useAuth } from '../contexts/AuthContext'
import { UserOutlined } from '@ant-design/icons'

const Dashboard = () => {
  const { userData, logout } = useAuth();

  const handleLogout = async () => {
    await logout();
  };
  return (
    <Card className='profile-card'>
      <Flex vertical gap='small' align='center'>
        <Avatar size={150} icon={<UserOutlined />} className='avatar' />
        <Typography.Title
          level={2}
          strong
          className='username'>
            {userData.name}
        </Typography.Title>
        <Typography.Text type='secondary' strong>
          Email: {userData.email}
        </Typography.Text>
        <Typography.Text type='secondary'>
          Role: {userData.role}
        </Typography.Text>
        <Button size='large' type='primary' className='profile-btn' onClick={handleLogout}>Logout</Button>
      </Flex>
    </Card>
  )
}

export default Dashboard
// App.css
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800&display=swap');

* {
  margin: 0;
  padding: 0;
  font-family: 'Poppins', sans-serif;
}

body {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background: #cbdbff;
}

/* form */
.form-container {
  width: 1000px;
}

.title,
.slogan {
  text-align: center;
}

.btn {
  width: 100%;
}

.auth-image {
  width: 100%;
  border-radius: 8px;
}

.alert {
  margin-bottom: 1.5rem;
}

/* profile-card */
.profile-card {
  width: 500px;
}

.avatar {
  margin-bottom: 1.5rem;
}

.username {
  text-transform: capitalize;
}

.profile-btn {
  margin-top: 1.3rem;
  width: 100%;
}
// src/hooks/useSignup.js
import { useState } from 'react';
import { message } from 'antd';
import { useAuth } from '../contexts/AuthContext.jsx';

const useSignup = () => {
  const { login } = useAuth();
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(null);

  const registerUser = async (values) => {
    if (values.password !== values.passwordConfirm) {
      return setError('Passwords are not the same');
    }

    try {
      setError(null);
      setLoading(true);
      const res = await fetch('http://localhost:3000/api/auth/signup', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(values),
      });

      const data = await res.json();
      if (res.status === 201) {
        message.success(data.message);
        login(data.token, data.user);
      } else if (res.status === 400) {
        setError(data.message);
      } else {
        message.error('Registration failed');
      }
    } catch (error) {
      message.error('Registration Failed');
    } finally {
      setLoading(false);
    }
  };

  return { loading, error, registerUser };
}

export default useSignup
// src/contexts/AuthContext.jsx
import React, { createContext, useContext, useEffect, useState } from 'react'

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [token, setToken] = useState(null);
  const [userData, setUserData] = useState(null);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const storedData = JSON.parse(localStorage.getItem('user_data'));

  useEffect(() => {
    if (storedData) {
      const { userToken , user } = storedData;
      setToken(userToken);
      setUserData(user);
      setIsAuthenticated(true);
    }
  }, []);

  const login = (newToken, newData) => {
    localStorage.setItem(
      'user_data',
      JSON.stringify({ userToken: newToken, user: newData }),
    );

    setToken(newToken);
    setUserData(newData);
    setIsAuthenticated(true);
  };

  const logout = () => {
    localStorage.removeItem('user_data');
    setToken(null);
    setUserData(null);
    setIsAuthenticated(false);
  };
  return (
  <AuthContext.Provider value={{token, isAuthenticated, login, logout, userData}}>
    {children}
  </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);
// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { AuthProvider } from './contexts/AuthContext.jsx'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <AuthProvider>
      <App />
    </AuthProvider>
  </React.StrictMode>,
)
// src/hooks/useLogin.js
import { useState } from 'react';
import { message } from 'antd';
import { useAuth } from '../contexts/AuthContext.jsx';

const useLogin = () => {
  const { login } = useAuth();
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(null);

  const loginUser = async (values) => {
    try {
      setError(null);
      setLoading(true);
      const res = await fetch('http://localhost:3000/api/auth/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(values),
      });

      const data = await res.json();
      if (res.status === 200) {
        message.success(data.message);
        login(data.token, data.user);
      } else if (res.status === 404) {
        setError(data.message);
      } else {
        message.error('Registration failed');
      }
    } catch (error) {
      message.error('Registration Failed');
    } finally {
      setLoading(false);
    }
  };

  return { loading, error, loginUser };
}

export default useLogin