Node js -day 5
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>© 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:
Generate Salt: bcrypt creates a random salt (a random string) and combines it with the password.
Hashing: The salt and password are processed through bcrypt, resulting in a hash that is difficult to reverse.
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:
Install bcrypt:
npm install bcryptHash 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
Install Dependencies: You'll need to install
jsonwebtokenfor 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:
Check if the user exists by comparing the email from the request with the stored email in the database.
Compare the hashed password with the one stored in the database using bcrypt.
Generate a JWT if authentication is successful.
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. Ifnext()is not called, the request-response cycle is incomplete, and the request may hang.
Types of Middleware
Application-Level Middleware:
- Middleware functions applied to all routes in the application, usually defined with
app.use().
- Middleware functions applied to all routes in the application, usually defined with
Route-Specific Middleware:
- Middleware functions that are applied to specific routes. They are passed as the second argument to route handlers.
Error-Handling Middleware:
- These handle errors that occur in the application. They have four arguments:
(err, req, res, next).
- These handle errors that occur in the application. They have four arguments:
Built-in Middleware:
- Express provides some built-in middleware such as
express.json()(parses JSON requests) andexpress.static()(serves static files).
- Express provides some built-in middleware such as
Third-party Middleware:
- Middleware created by third parties like
body-parser,cookie-parser,morgan, etc.
- Middleware created by third parties like
Why Use Custom Middleware?
Custom middleware allows you to:
Modify Request and Response: Middleware can be used to add properties to the
reqobject or alter theresobject. For example, you can attach user information to thereqobject after verifying the user's token.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.
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).
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
Logging Requests: Log details about every incoming request (e.g., method, URL, timestamp).
Authentication: Verify if a user is authenticated (e.g., checking for a JWT token or session).
Authorization: Ensure that the user has permission to access a specific route.
Input Validation: Check if the data provided by the user in a request (e.g., form submission) is valid.
Error Handling: Catch and handle any errors that occur during the request processing.
Example Workflow of Middleware
Request enters the server: The server receives a request and passes it to the first middleware function.
Middleware 1 (e.g., Logging): Logs the request details.
Middleware 2 (e.g., Authentication): Checks if the user is authenticated, adds user data to
reqif authenticated.Middleware 3 (e.g., Authorization): Verifies if the user has permission to access the requested resource.
Route handler: Once the request passes through all middlewares, it reaches the final route handler, which sends the response.
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
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.
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).
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.
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:
Session Data Exposure: It makes the session data (
req.session) available globally to all templates or views.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
res.locals.session = req.session:Session data: This line makes the
req.sessiondata available globally in all your EJS templates.res.localsis an object that holds variables that are accessible in all views rendered by Express. By settingres.locals.session, you ensure that session data (like user authentication status or user preferences) is accessible to your views (EJS templates).
res.locals.user = null:- User initialization: Initializes the
uservariable asnull. This is done because initially, we don't have any user data.
- User initialization: Initializes the
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).
- Decoded data: The decoded JWT typically contains user data (e.g., user ID, username, email) which is added to
Error Handling for Invalid Tokens:
- If the token is invalid or has expired, an error is logged, and the
userremainsnull(no valid user data). This is useful for debugging, as it provides visibility into issues with the token.
- If the token is invalid or has expired, an error is logged, and the
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.
- This is called to pass control to the next middleware function in the stack or to the route handler. Without calling
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
}
});

