Skip to main content

Command Palette

Search for a command to run...

Node js -day 5

Published
16 min readView as Markdown
B

Student | Tech enthusiast | Aspiring software developer

EJS Layout Templates

EJS supports a concept called partials, which allows you to reuse parts of your HTML across multiple pages. This is particularly useful for layouts like headers, footers, or navigation menus.

A typical layout template contains placeholders for dynamic content (<%- body %> in Express.js convention) and reusable components like the navbar and footer.

Install ejs layout and setup express ejs layout as follow

npm instal express-ejs-layouts cookie-parser jsonwebtoken bcrypt
const express = require('express');
const bodyParser = require('body-parser');
const router = require('./router.js')
const app = express();
const dbconnect = require('./dbconnect.js')
const expressLayouts = require('express-ejs-layouts');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const cookieparser = require('cookie-parser')




// Middleware
app.use(bodyParser.urlencoded({ extended: true }));
app.use(expressLayouts);
app.use(cookieParser())
app.use(router)



app.set('view engine', 'ejs');
app.set('views', './views');
app.set('layout', 'layout'); 

dbconnect()



app.listen(3000, () => console.log('Server is running on http://localhost:3000'));

Now in views create layout.ejs
and use the following ejs code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Login Page</title>
  <!-- Tailwind CSS via CDN -->
  <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 font-sans flex flex-col h-[100vh]">

  <!-- Navbar -->
  <header class="bg-indigo-600 text-white shadow-md">
    <div class="max-w-7xl mx-auto px-4 py-4">
      <div class="flex items-center justify-between">
        <a href="/" class="text-xl font-bold">MyApp</a>
        <nav class="space-x-4">
          <a href="/" class="hover:text-indigo-300">Home</a>
          <a href="/about" class="hover:text-indigo-300">About</a>
          <a href="/contact" class="hover:text-indigo-300">Contact</a>
        </nav>
      </div>
    </div>
  </header>

  <!-- Main Content -->
  <main class="my-6">
    <div class="max-w-7xl mx-auto px-4">
      <%- body %> <!-- Dynamic content goes here -->
    </div>
  </main>

  <!-- Footer -->
  <footer class="bg-gray-800 text-white py-4 mt-auto">
    <div class="text-center">
      <p>&copy; 2024 MyApp. All rights reserved.</p>
    </div>
  </footer>

</body>
</html>
router.get('/register', (req, res) => {
  res.render('register');
});

// Handle registration
  router.post('/register', async (req, res) => {
    const { username, email, password } = req.body;
    console.log(req.body)

    // Validation (silent)
    if (
      !username ||
      !email ||
      !password
    ) {
      console.log("error")
      return res.redirect('/register'); // Stay on the same page silently
    }

    try {
      const newUser = new User({ username, email, password });
      await newUser.save();
      console.log("user saved",newUser)
      res.redirect('/login'); // Redirect after successful registration
    } catch (err) {
      res.redirect('/register'); // Stay on the same page silently
    }
  });

register.ejs

 <div class="min-h-screen flex items-center justify-center">
    <div class="bg-white shadow-md rounded px-8 pt-6 pb-8 w-full max-w-md">
      <h1 class="text-2xl font-bold mb-6 text-center">Register</h1>

      <!-- Registration Form -->
      <form action="/register" method="POST">
        <div class="mb-4">
          <label for="username" class="block text-gray-700 text-sm font-bold mb-2">Username</label>
          <input type="text" id="username" name="username" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder="Enter your username" required>
        </div>

        <div class="mb-4">
          <label for="email" class="block text-gray-700 text-sm font-bold mb-2">Email</label>
          <input type="email" id="email" name="email" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder="Enter your email" required>
        </div>

        <div class="mb-4">
          <label for="password" class="block text-gray-700 text-sm font-bold mb-2">Password</label>
          <input type="password" id="password" name="password" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder="Enter your password" required>
        </div>

        <div class="mb-4">
          <label for="confirmPassword" class="block text-gray-700 text-sm font-bold mb-2">Confirm Password</label>
          <input type="password" id="confirmPassword" name="confirmPassword" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder="Confirm your password" required>
        </div>

        <div class="flex items-center justify-center">
          <button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
            Register
          </button>
        </div>
      </form>
    </div>
  </div>

What is Hashing?

Hashing is the process of converting data (like a password) into a fixed-length string of characters, which is typically irreversible. This means you cannot get the original input back from the hash.

Why Do We Need Hashing for Passwords?

  • Security: Hashing ensures that passwords are stored in a secure way. Even if the database is compromised, the attacker only sees the hash, not the actual password.

  • One-Way Transformation: Once a password is hashed, it can't be reversed, adding an extra layer of security.

  • Prevents Storing Plain Text Passwords: Storing passwords in plain text is a major security risk, and hashing helps protect users' sensitive information.


Using bcrypt for Hashing Passwords

bcrypt is a library that allows us to hash passwords in a secure way. It also includes a feature called salting, which adds randomness to the password hash, making it more resistant to attacks.

How bcrypt Works:

  1. Generate Salt: bcrypt creates a random salt (a random string) and combines it with the password.

  2. Hashing: The salt and password are processed through bcrypt, resulting in a hash that is difficult to reverse.

  3. Storing: Only the hash (and salt) is stored in the database, not the original password.

Using bcrypt in the Registration Route:

To use bcrypt for password hashing, follow these steps:

  1. Install bcrypt:

     npm install bcrypt
    
  2. Hash the Password: When a user registers, hash the password before saving it to the database.

add this into the top of router page

const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')

now,change the register controller to :

router.post('/register', async (req, res) => {
  const { username, email, password } = req.body;

  // Validation (silent)
  if (!username || !email || !password) {
    return res.redirect('/register'); // Stay on the same page silently if validation fails
  }

  try {
    // Hash the password using bcrypt
    const hashedPassword = await bcrypt.hash(password, 10); // 10 is the salt rounds

    // Create a new user with the hashed password
    const newUser = new User({
      username,
      email,
      password: hashedPassword, // Save the hashed password, not the plain text one
    });

    // Save the new user to the database
    await newUser.save();

    console.log('User saved', newUser);
    res.redirect('/login'); // Redirect after successful registration
  } catch (err) {
    console.error(err);
    res.redirect('/register'); // Stay on the same page silently if error occurs
  }
});

Login System Using Cookies and JSON Web Tokens (JWT)

When building a login system, you have a few options for managing user sessions and authentication. Two common methods are using cookies and using JSON Web Tokens (JWT). In this tutorial, I'll guide you through both of them.

1. Cookies for Authentication

  • Cookies store small pieces of data in the user's browser, and they are sent automatically with every HTTP request to the server.

  • Cookies can be used to store session data, like a session ID or user information, which can be read on the server to verify the user's identity.

2. JSON Web Tokens (JWT)

  • JWT is a token-based authentication method. A JWT contains encoded data that can be verified and trusted because it is signed. The token is passed back and forth between the client and the server, allowing the server to authenticate requests without needing a session.

  • JWTs are stateless, meaning the server doesn't need to keep track of user sessions. The token itself carries the necessary data (e.g., user ID).


Installation

  1. Install Dependencies: You'll need to install jsonwebtoken for JWT-based authentication, and cookie-parser if you're also using cookies.

     npm install jsonwebtoken cookie-parser
    

Step-by-Step Login System with Cookies and JWT

1. Set Up JWT (JSON Web Token)

We’ll start by creating a route to handle the login. When a user submits their login credentials (e.g., email and password), the server will:

  • Verify the user's credentials.

  • If valid, the server will issue a JWT and set a cookie to store it in the browser.

  • The JWT can then be used for subsequent requests to verify that the user is logged in.

2. Update router.js to Include Login Route with JWT

In the login process, we will:

  1. Check if the user exists by comparing the email from the request with the stored email in the database.

  2. Compare the hashed password with the one stored in the database using bcrypt.

  3. Generate a JWT if authentication is successful.

  4. Send the JWT to the client and store it in a cookie.

Here's how you can implement the login functionality:

Controller for login

router.get('/login', (req, res) => {
  res.render('login');
   // Clear the message after displaying it
});

// Login route (POST) - Handle login logic
router.post('/login', async (req, res) => {
  let { username, password } = req.body;

  // Check if username exists
  const user = await User.findOne({ username });
  if (!user) {

    return res.redirect('/login');
  }

  // Compare the provided password with the stored hashed password
  const verifyUser = await bcrypt.compare(password, user.password);
  if (!verifyUser) {

    return res.redirect('/login');
  }

  // Generate JWT token
  const token = jwt.sign(
    { id: user._id, username: user.username, email: user.email },
    'biseshadhikari123'
  );

  // Set the token as a cookie
  res.cookie('token', token); 
  return res.redirect('/');
});

for login.ejs



  <div class=" mx-auto w-1/2 p-8 space-y-4 bg-white rounded-lg shadow-lg">
    <h2 class="text-2xl font-bold text-center text-gray-800">Login</h2>



    <!-- Login Form -->
    <form action="/login" method="POST" class="space-y-4">

      <!-- Username Input -->
      <div>
        <label for="username" class="block text-sm font-medium text-gray-700">Username</label>
        <input 
          type="text" 
          name="username" 
          id="username" 
          placeholder="Enter your username" 
          class="w-full px-4 py-2 mt-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
          required
        />
      </div>

      <!-- Password Input -->
      <div>
        <label for="password" class="block text-sm font-medium text-gray-700">Password</label>
        <input 
          type="password" 
          name="password" 
          id="password" 
          placeholder="Enter your password" 
          class="w-full px-4 py-2 mt-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
          required
        />
      </div>

      <!-- Submit Button -->
      <button 
        type="submit" 
        class="w-full py-2 mt-4 text-white bg-blue-500 hover:bg-blue-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
      >
        Login
      </button>
    </form>

    <!-- Link to Register -->
    <p class="text-center text-sm text-gray-600 mt-4">
      Don't have an account? 
      <a href="/register" class="text-blue-500 hover:underline">Sign up here</a>
    </p>
  </div>

What is Custom Middleware in Express?

In Express.js, middleware refers to functions that have access to the request (req), response (res), and the next function in the application’s request-response cycle. Middleware functions are executed in the order they are defined.

A custom middleware is simply a middleware function that you define in your application to perform specific tasks. These tasks can include handling authentication, logging, modifying the request or response, or managing errors.

Basic Structure of Middleware

A middleware function typically looks like this:

function customMiddleware(req, res, next) {
    // Perform some actions
    next();  // Pass control to the next middleware or route handler
}
  • req: The request object, containing details about the incoming HTTP request (e.g., headers, body, parameters).

  • res: The response object, used to send back a response to the client.

  • next: A function that, when called, passes control to the next middleware in the stack. If next() is not called, the request-response cycle is incomplete, and the request may hang.

Types of Middleware

  1. Application-Level Middleware:

    • Middleware functions applied to all routes in the application, usually defined with app.use().
  2. Route-Specific Middleware:

    • Middleware functions that are applied to specific routes. They are passed as the second argument to route handlers.
  3. Error-Handling Middleware:

    • These handle errors that occur in the application. They have four arguments: (err, req, res, next).
  4. Built-in Middleware:

    • Express provides some built-in middleware such as express.json() (parses JSON requests) and express.static() (serves static files).
  5. Third-party Middleware:

    • Middleware created by third parties like body-parser, cookie-parser, morgan, etc.

Why Use Custom Middleware?

Custom middleware allows you to:

  1. Modify Request and Response: Middleware can be used to add properties to the req object or alter the res object. For example, you can attach user information to the req object after verifying the user's token.

  2. Perform Specific Tasks: Custom middleware allows you to centralize specific tasks, such as logging, error handling, or data validation, without repeating code in multiple route handlers.

  3. Authorization and Authentication: Middleware is commonly used for checking if a user is logged in (authentication) or if they have the proper permissions to access a route (authorization).

  4. Error Handling: Middleware can catch errors and handle them in a structured way, providing custom error messages or logging them for debugging.

Common Use Cases for Custom Middleware

  1. Logging Requests: Log details about every incoming request (e.g., method, URL, timestamp).

  2. Authentication: Verify if a user is authenticated (e.g., checking for a JWT token or session).

  3. Authorization: Ensure that the user has permission to access a specific route.

  4. Input Validation: Check if the data provided by the user in a request (e.g., form submission) is valid.

  5. Error Handling: Catch and handle any errors that occur during the request processing.

Example Workflow of Middleware

  1. Request enters the server: The server receives a request and passes it to the first middleware function.

  2. Middleware 1 (e.g., Logging): Logs the request details.

  3. Middleware 2 (e.g., Authentication): Checks if the user is authenticated, adds user data to req if authenticated.

  4. Middleware 3 (e.g., Authorization): Verifies if the user has permission to access the requested resource.

  5. Route handler: Once the request passes through all middlewares, it reaches the final route handler, which sends the response.

  6. Error Handling (if necessary): If an error occurs at any point, it can be passed to the error-handling middleware.

Authentication middleware

const jwt = require('jsonwebtoken');

const authenticateUser = (req, res, next) => {
    try {
        // Retrieve the token from cookies
        const token = req.cookies.token;
        if (!token) {
            req.session.message = "Unauthorized access. Please login.";
            return res.redirect('/login');
        }

        // Verify the token
        const decoded = jwt.verify(token, 'biseshadhikari123'); 

        req.user = decoded;


        next(); // Continue to the route handler
    } catch (error) {
        console.error('Authentication error:', error);
        req.session.message = "Session expired or invalid token. Please login again.";
        res.redirect('/login');
    }
};

const notauthenticateUser = (req, res, next) => {
    try {
        // Retrieve the token from cookies
        const token = req.cookies.token;
        console.log(token)
        if (!token) {
            next()
        }
        else{ 
            return res.redirect('/')
        }

        // Verify the token

    } catch (error) {
        console.error('Authentication error:', error);
        req.session.message = "Session expired or invalid token. Please login again.";
        res.redirect('/login');
    }
};


module.exports = {authenticateUser,notauthenticateUser}

What is a Session in Express?

In Express, a session is a mechanism for storing data on the server-side for a user throughout their interaction with a web application. The session typically involves a unique identifier (session ID) that is stored in the browser's cookies, which the server uses to track the user.

When a user logs in or performs an action that modifies their session data, Express creates a session object that stores the information (e.g., username, user preferences, authentication status). This session data can be accessed by the application on subsequent requests.

How Sessions Work

  1. Session ID: When a user first interacts with the server, the server generates a unique session ID. This ID is typically stored in a cookie on the user's browser.

  2. Session Data: The server stores data related to the user (e.g., user details, authentication status) in memory or a store like a database or in-memory store (e.g., Redis).

  3. Session Persistence: On each subsequent request, the browser sends the session ID stored in the cookie back to the server. The server uses this ID to retrieve the user's session data.

  4. Session Expiration: Sessions are typically time-bound. After a certain amount of inactivity or a set period, the session may expire, and the user may need to re-authenticate.

Why Sessions are Useful

  • Authentication: Sessions are commonly used to manage user authentication. After logging in, the user’s information (e.g., username, roles) is stored in the session, and they don’t need to re-enter their credentials for each request.

  • State Persistence: Sessions allow you to store user-specific data that persists across requests (e.g., shopping cart items, user preferences).

  • Security: Sessions provide a secure way to store sensitive information (like user credentials or roles) because the actual data is stored on the server, and only a session ID is stored on the client-side.

Using Sessions in Express

To manage sessions in Express, you need to use the express-session middleware. This middleware is responsible for creating and managing the session ID and storing session data.

Installing express-session

To start using sessions in Express, you need to install the express-session package:

npm install express-session

setting up session

const session = require('express-session');  // Import express-session
app.use(session({ 
    secret:'h;adfjh;adf', 

}))

This middleware is used to handle session data and JWT authentication in an Express application. It performs two tasks:

  1. Session Data Exposure: It makes the session data (req.session) available globally to all templates or views.

  2. JWT Verification: It checks if there is a JWT (JSON Web Token) present in the request cookies, verifies it, and then adds the decoded user information (if the token is valid) to the response object.

Let me break it down step-by-step:

Middleware Overview

app.use((req, res, next) => {
    res.locals.session = req.session;  // Make session data available globally
    res.locals.user = null;  // Initialize `user` to null

    // Check if a token is present in cookies
    if (req.cookies.token) {
        try {
            // Decode the token using the secret key and verify its validity
            const decoded = jwt.verify(req.cookies.token, "biseshadhikari123");
            res.locals.user = decoded;  // Set user data globally if the token is valid
        } catch (error) {
            console.error("Invalid token:", error);  // Log the error if the token is invalid
        }
    }

    next();  // Pass control to the next middleware or route handler
});

Detailed Explanation

  1. res.locals.session = req.session:

    • Session data: This line makes the req.session data available globally in all your EJS templates.

    • res.locals is an object that holds variables that are accessible in all views rendered by Express. By setting res.locals.session, you ensure that session data (like user authentication status or user preferences) is accessible to your views (EJS templates).

  2. res.locals.user = null:

    • User initialization: Initializes the user variable as null. This is done because initially, we don't have any user data.
  3. JWT Token Check (req.cookies.token):

    • Token extraction: This checks whether a JWT token exists in the request cookies. If a token is found, the server tries to verify and decode it.

    • JWT verification: The jwt.verify() function is used to decode and verify the JWT token using a secret key (in this case, "biseshadhikari123"). If the token is valid and not expired, it returns the decoded payload.

      • Decoded data: The decoded JWT typically contains user data (e.g., user ID, username, email) which is added to res.locals.user. This allows you to access the authenticated user’s information in your views (EJS templates).
  4. Error Handling for Invalid Tokens:

    • If the token is invalid or has expired, an error is logged, and the user remains null (no valid user data). This is useful for debugging, as it provides visibility into issues with the token.
  5. next():

    • This is called to pass control to the next middleware function in the stack or to the route handler. Without calling next(), the request would hang and never proceed.

Why is this middleware useful?

  • Session & JWT Synchronization: This middleware combines session-based authentication (via req.session) and token-based authentication (via JWT in cookies). It ensures that the application can handle both session and token-based user data in a unified way.

  • Global Access to User Data: By setting res.locals.user, the user data is accessible in all EJS templates, allowing for a dynamic user interface where you can show user-specific information like their name, profile, or authenticated status.

  • Consistent User Information: Whether the user is logged in via session or JWT, this middleware ensures their data is consistently available across the app, enhancing the user experience.

Use Case

  • If the user logs in with a session, their session data is available globally (in req.session).

  • If the user logs in using JWT, their user data is available globally (in res.locals.user) after verifying the token.

This middleware simplifies the process of handling authentication and ensures that the user data is available to the templates without having to write repetitive checks in each route.

Updating the profile

// profile.js (or within your router file)

router.get('/profile', async (req, res) => {
    // Check if the user is authenticated by checking req.user (from JWT)
    if (!req.user) {
        return res.redirect('/login');  // Redirect to login if the user is not authenticated
    }

    try {
        // Fetch the user from the database using the user ID from req.user
        const user = await User.findById(req.user.id);

        if (!user) {
            return res.redirect('/login');  // If user doesn't exist in DB, redirect to login
        }

        // Render the profile page and pass the user data
        res.render('profile', { user });
    } catch (err) {
        console.error('Error fetching user profile:', err);
        res.redirect('/login');  // Redirect if any error occurs
    }
});

profile.ejs

<!-- views/profile.ejs -->

<div class="max-w-2xl mx-auto bg-white p-8 mt-10 rounded-lg shadow-md">
    <h2 class="text-2xl font-semibold text-center">Profile</h2>

    <div class="mt-6">
        <p class="font-medium">Username: <span class="text-gray-700"><%= user.username %></span></p>
        <p class="font-medium">Email: <span class="text-gray-700"><%= user.email %></span></p>
    </div>

    <!-- Profile Update Form -->
    <form action="/profile/update" method="POST" class="mt-6">
        <h3 class="font-semibold text-lg mb-2">Update Your Profile</h3>
        <div class="mb-4">
            <label for="username" class="block text-gray-600">New Username</label>
            <input type="text" id="username" name="username" class="w-full px-4 py-2 mt-2 border rounded-md" value="<%= user.username %>" required>
        </div>
        <div class="mb-4">
            <label for="email" class="block text-gray-600">New Email</label>
            <input type="email" id="email" name="email" class="w-full px-4 py-2 mt-2 border rounded-md" value="<%= user.email %>" required>
        </div>
        <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded-md">Update Profile</button>
    </form>

    <!-- Delete Profile Form -->
    <form action="/profile/delete" method="POST" class="mt-6">
        <button type="submit" class="bg-red-500 text-white px-4 py-2 rounded-md">Delete Account</button>
    </form>
</div>
// profile.js (or within your router file)

router.post('/profile/update', async (req, res) => {
    const { username, email } = req.body;

    // Check if the user is authenticated
    if (!req.user) {
        return res.redirect('/login');  // Redirect to login if the user is not authenticated
    }

    try {
        // Fetch the user from the database using req.user.id
        const user = await User.findById(req.user.id);

        if (!user) {
            return res.redirect('/login');  // If user doesn't exist in DB, redirect to login
        }

        // Update the user's details
        user.username = username || user.username;
        user.email = email || user.email;

        // Save the updated user
        await user.save();

        // Redirect back to the profile page after updating
        res.redirect('/profile');
    } catch (err) {
        console.error('Error updating profile:', err);
        res.redirect('/profile');  // Redirect if there is an error
    }
});

More from this blog

Bisesh's Blog

24 posts