Uncategorized

Full Stack CRUD Application with JWT Token Authentication, Login, and Dashboard Option (Node.js, Express, MongoDB, Redux Toolkit, JWT)

In this tutorial, we’ll walk through building a full-stack CRUD (Create, Read, Update, Delete) application that also includes JWT token-based authentication, a login system, and a protected dashboard option. The project will be built using Node.js, Express, MongoDB, Redux Toolkit, and JWT.

What You’ll Learn

  1. How to set up a RESTful API with Express and MongoDB.
  2. How to implement JWT token authentication for login and access control.
  3. How to create React components that connect with the backend.
  4. How to manage state using Redux Toolkit for handling user data.
  5. How to implement CRUD operations (Create, Read, Update, Delete) for user management.

Why These Technologies?

  • Node.js: Server-side JavaScript runtime for building scalable web apps.
  • Express: A minimal, flexible Node.js web framework for building RESTful APIs.
  • MongoDB: A NoSQL database that allows flexible data storage.
  • Redux Toolkit: A library that simplifies state management in React applications.
  • JWT (JSON Web Token): A secure method for transmitting information as a JSON object between client and server.

 

Backend: Building the REST API with JWT Authentication

We will start by setting up the backend with Node.js, Express, and MongoDB. Additionally, we will implement JWT authentication for securing routes.

Step 1: Install Dependencies

Run the following commands to install the required dependencies in your backend project folder:

npm init -y
npm install express mongoose body-parser cors dotenv jsonwebtoken bcryptjs

Step 2: Create .env File

In your project’s root directory, create a .env file to manage sensitive information like your JWT secret key and MongoDB URI.

.env:

JWT_SECRET=your_jwt_secret_key
MONGODB_URI=mongodb://127.0.0.1:27017/usercrud
PORT=5000

Step 3: Set Up the Server and Middleware

  1. server.js: The core of your backend application where we initialize the Express server, connect to MongoDB, and set up routes.
  2. middleware/authMiddleware.js: A middleware to verify JWT tokens.

Create middleware/authMiddleware.js

const jwt = require('jsonwebtoken');
require('dotenv').config();

const authenticateToken = (req, res, next) => {
  const token = req.header('Authorization') && req.header('Authorization').split(' ')[1];
  if (!token) return res.status(401).send('Access Denied');

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.status(403).send('Invalid Token');
    req.user = user;
    next();
  });
};

module.exports = authenticateToken;

Create server.js

const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const cors = require('cors');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const dotenv = require('dotenv');
const authenticateToken = require('./middleware/authMiddleware');

dotenv.config();

const app = express();
const PORT = process.env.PORT || 5000;

app.use(cors());
app.use(bodyParser.json());

// MongoDB Connection
mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('MongoDB connected'))
  .catch(err => console.error(err));

// User Schema
const userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String,
  email: { type: String, unique: true },
  password: String,
});

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

// Register Route
app.post('/register', async (req, res) => {
  const { firstName, lastName, email, password } = req.body;
  const salt = await bcrypt.genSalt(10);
  const hashedPassword = await bcrypt.hash(password, salt);

  const newUser = new User({ firstName, lastName, email, password: hashedPassword });
  try {
    await newUser.save();
    res.status(201).json(newUser);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
});

// Login Route
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  if (!user) return res.status(400).send('User not found');

  const isMatch = await bcrypt.compare(password, user.password);
  if (!isMatch) return res.status(400).send('Invalid credentials');

  const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
  res.json({ token });
});

// Protected Dashboard Route
app.get('/dashboard', authenticateToken, (req, res) => {
  res.send(`Hello, ${req.user.firstName}! Welcome to your dashboard.`);
});

// CRUD Routes for Users
app.get('/users', authenticateToken, async (req, res) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

app.post('/users', authenticateToken, async (req, res) => {
  const { firstName, lastName, email, password } = req.body;
  const newUser = new User({ firstName, lastName, email, password });
  try {
    await newUser.save();
    res.status(201).json(newUser);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
});

app.put('/users/:id', authenticateToken, async (req, res) => {
  const { id } = req.params;
  const { firstName, lastName, email, password } = req.body;
  try {
    const updatedUser = await User.findByIdAndUpdate(id, { firstName, lastName, email, password }, { new: true });
    res.json(updatedUser);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
});

app.delete('/users/:id', authenticateToken, async (req, res) => {
  const { id } = req.params;
  try {
    await User.findByIdAndDelete(id);
    res.json({ message: 'User deleted successfully' });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

// Start Server
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));

 


Frontend: React with Redux Toolkit and JWT Authentication

Now, let’s move on to the frontend. We’ll use React, Redux Toolkit, and Axios for state management and communication with the backend.

Step 1: Install Frontend Dependencies

Run the following command in your React project folder:

npm install @reduxjs/toolkit react-redux axios

Step 2: Set Up Redux Store

You’re right! I missed including the Login Redux page and the Routes Page in the previous tutorial. Let’s complete the tutorial by adding those important parts.


Login Redux Setup

For the login functionality, we’ll manage the authentication state using Redux and JWT. The login process will store the JWT token in localStorage and update the Redux store.

Step 1: Create a Slice for Auth

Create a Redux slice that will manage the user authentication state.

src/authSlice.js:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

const API_URL = 'http://localhost:5000/login';

// Login user
export const loginUser = createAsyncThunk('auth/loginUser', async (userData) => {
  const response = await axios.post(API_URL, userData);
  return response.data;  // Assuming the token is returned here
});

// Auth slice
const authSlice = createSlice({
  name: 'auth',
  initialState: {
    user: null,
    token: localStorage.getItem('token') || null,  // persist token
    status: 'idle',
    error: null,
  },
  reducers: {
    logout(state) {
      state.user = null;
      state.token = null;
      localStorage.removeItem('token');
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(loginUser.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(loginUser.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.token = action.payload.token;
        localStorage.setItem('token', action.payload.token);  // Save token in localStorage
      })
      .addCase(loginUser.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  },
});

export const { logout } = authSlice.actions;
export default authSlice.reducer;

 

Step 2: Set Up the Redux Store

Make sure the authSlice is included in the Redux store.

src/store.js:

import { configureStore } from '@reduxjs/toolkit';
import authReducer from './authSlice';
import userReducer from './userSlice';

export const store = configureStore({
  reducer: {
    auth: authReducer,
    users: userReducer,
  },
});

 


Login Page

Now, let’s create the Login Page that uses the Redux state and sends the login request to the backend.

src/LoginPage.js:

import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { loginUser } from './authSlice';
import { useHistory } from 'react-router-dom';

const LoginPage = () => {
  const dispatch = useDispatch();
  const history = useHistory();
  const { status, error } = useSelector((state) => state.auth);
  
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = (e) => {
    e.preventDefault();
    dispatch(loginUser({ email, password }));
  };

  if (status === 'succeeded') {
    history.push('/dashboard');
  }

  return (
    <div className="login-page">
      <h2>Login</h2>
      <form onSubmit={handleLogin}>
        <div>
          <label>Email</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
          />
        </div>
        <div>
          <label>Password</label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
          />
        </div>
        {error && <p>{error}</p>}
        <button type="submit" disabled={status === 'loading'}>
          {status === 'loading' ? 'Logging in...' : 'Login'}
        </button>
      </form>
    </div>
  );
};

export default LoginPage;

 

 

In this login page:

  • The user enters their email and password.
  • The Redux loginUser action is dispatched to handle the login request.
  • If login is successful, the user is redirected to the dashboard.

Routes Page

Next, let’s set up the Routes Page using React Router to manage page navigation. We’ll also make sure only authenticated users can access certain routes.

First, you need to install React Router if you haven’t done it yet:

npm install react-router-dom

Step 1: Set Up Routing

src/App.js:

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import LoginPage from './LoginPage';
import DashboardPage from './DashboardPage';
import ProtectedRoute from './ProtectedRoute';

const App = () => {
  return (
    <Router>
      <Switch>
        <Route path="/login" component={LoginPage} />
        <ProtectedRoute path="/dashboard" component={DashboardPage} />
        <Route path="/" exact>
          <h1>Welcome to the Full Stack App</h1>
        </Route>
      </Switch>
    </Router>
  );
};

export default App;

Step 2: Create a Protected Route

We want the Dashboard route to be protected, meaning that only authenticated users (with a valid JWT token) can access it.

src/ProtectedRoute.js:

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { useSelector } from 'react-redux';

const ProtectedRoute = ({ component: Component, ...rest }) => {
  const { token } = useSelector((state) => state.auth);

  return (
    <Route
      {...rest}
      render={(props) =>
        token ? <Component {...props} /> : <Redirect to="/login" />
      }
    />
  );
};

export default ProtectedRoute;

 

Here, we’re using React Router’s Route component to check if the user is authenticated (i.e., if they have a token). If they are not authenticated, they will be redirected to the Login page.

Step 3: Create the Dashboard Page

This is the page that will only be accessible to logged-in users.

src/DashboardPage.js:

import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';

const DashboardPage = () => {
  const { user } = useSelector((state) => state.auth);

  useEffect(() => {
    if (!user) {
      // Handle if the user is not found
    }
  }, [user]);

  return (
    <div className="dashboard">
      <h2>Welcome, {user ? user.firstName : 'User'}!</h2>
      <p>This is your dashboard.</p>
    </div>
  );
};

export default DashboardPage;

 

Conclusion

In this tutorial, we’ve built a full-stack CRUD application with JWT authentication, using Node.js, Express, MongoDB, Redux Toolkit, and JWT. We’ve learned how to set up secure login, create a protected dashboard, and implement Create, Read, Update, and Delete operations for user data.

Now you can expand upon this setup, including adding features like role-based authentication, user profiles, or improving the UI to make it even more interactive!

Leave a Reply

Your email address will not be published. Required fields are marked *