ExpressJs

⌘K
  1. Home
  2. Docs
  3. ExpressJs
  4. Session & activity l...
  5. Simple Session & Activity management

Simple Session & Activity management

creating a Session Management system from scratch in Node.js using TypeORM and Express.js. This system will include user authentication, session handling, session expiration, activity logging, and JWT-based authentication.

We’ll cover the following parts in detail:

Part 1: Setting Up the Environment and Project Structure

Part 2: Database Setup with Entities (User, Session, ActivityLog)

Part 3: User Registration and Authentication with JWT

Part 4: Session Management and Activity Logging

Part 5: Middleware for Session Validation

Part 6: Testing the Full Workflow (Login, Session Validation, Activity Logging, Logout)

Part 1: Setting Up the Environment and Project Structure

Step 1: Install Required Packages

First, make sure you have Node.js installed. Then, create a new project and install the required dependencies:

mkdir node-session-management
cd node-session-management
npm init -y

Now install the required packages:

npm install express typeorm reflect-metadata mysql2 bcryptjs jsonwebtoken dotenv

Here’s what these packages are for:

  • express: Web framework for handling routes.
  • typeorm: ORM for database operations.
  • reflect-metadata: Used by TypeORM for entity reflection.
  • pg: PostgreSQL driver for TypeORM.
  • bcryptjs: Library for hashing passwords.
  • jsonwebtoken: JWT for user authentication.
  • dotenv: For managing environment variables.

Step 2: Project Structure

Let’s set up the basic structure for our project:

node-session-management

├── src
   ├── config
      └── database.js
   ├── controllers
      └── userController.js
   ├── entities
      ├── User.js
      ├── Session.js
      └── ActivityLog.js
   ├── middleware
      └── authMiddleware.js
   ├── routes
      └── userRoutes.js
   ├── app.js
   └── server.js
├── .env
└── package.json

Step 3: Configure the Database Connection

In src/config/database.js, configure TypeORM to connect to your PostgreSQL database:

const { DataSource } = require('typeorm');
const User = require('../entities/User');
const Session = require('../entities/Session');
const ActivityLog = require('../entities/ActivityLog');
require('dotenv').config();

const AppDataSource = new DataSource({
  type: 'mysql',
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  username: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  synchronize: true, // Automatically syncs the DB schema (don't use in production)
  logging: true,
  entities: [User, Session, ActivityLog],
});

module.exports = AppDataSource;

Step 4: Environment Variables

Create a .env file in the root of your project and add the following:

DB_HOST=localhost
DB_PORT=3306
DB_USER=your_username
DB_PASS=your_password
DB_NAME=session_management
JWT_SECRET=your_jwt_secret

Part 2: Database Setup with Entities (User, Session, ActivityLog)

In this section, we’ll create our database entities: User, Session, and ActivityLog.

Step 1: Create the User Entity

In src/entities/User.js:

const { EntitySchema } = require('typeorm');

module.exports = new EntitySchema({
  name: 'User',
  tableName: 'users',
  columns: {
    id: {
      type: 'int',
      primary: true,
      generated: true,
    },
    username: {
      type: 'varchar',
      unique: true,
      nullable: false,
    },
    email: {
      type: 'varchar',
      unique: true,
      nullable: false,
    },
    password: {
      type: 'varchar',
      nullable: false,
    },
  },
  relations: {
    sessions: {
      type: 'one-to-many',
      target: 'Session',
      inverseSide: 'user',
    },
    activityLogs: {
      type: 'one-to-many',
      target: 'ActivityLog',
      inverseSide: 'user',
    },
  },
});

Step 2: Create the Session Entity

In src/entities/Session.js:

const { EntitySchema } = require('typeorm');

module.exports = new EntitySchema({
  name: 'Session',
  tableName: 'sessions',
  columns: {
    id: {
      type: 'int',
      primary: true,
      generated: true,
    },
    loginTime: {
      type: 'timestamp',
      createDate: true,
    },
    validUntil: {
      type: 'timestamp',
      nullable: true,
    },
    isActive: {
      type: 'boolean',
      default: true,
    },
  },
  relations: {
    user: {
      type: 'many-to-one',
      target: 'User',
      joinColumn: { name: 'user_id' },
      nullable: false,
      onDelete: 'CASCADE',
    },
  },
});

Step 3: Create the ActivityLog Entity

In src/entities/ActivityLog.js:

const { EntitySchema } = require('typeorm');

module.exports = new EntitySchema({
  name: 'ActivityLog',
  tableName: 'activity_logs',
  columns: {
    id: {
      type: 'int',
      primary: true,
      generated: true,
    },
    activityType: {
      type: 'varchar',
      nullable: false,
    },
    ipAddress: {
      type: 'varchar',
      nullable: true,
    },
    timestamp: {
      type: 'timestamp',
      createDate: true,
    },
  },
  relations: {
    user: {
      type: 'many-to-one',
      target: 'User',
      joinColumn: { name: 'user_id' },
      nullable: false,
    },
  },
});

Part 3: User Registration and Authentication with JWT

Step 1: Create User Registration

In src/controllers/userController.js:

const { getRepository } = require('typeorm');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../entities/User');
const Session = require('../entities/Session');
const ActivityLog = require('../entities/ActivityLog');

exports.register = async (req, res) => {
  const { username, email, password } = req.body;
  const userRepository = getRepository(User);

  try {
    const hashedPassword = bcrypt.hashSync(password, 10);
    const user = await userRepository.save({
      username,
      email,
      password: hashedPassword,
    });

    return res.status(201).json({ message: 'User registered successfully', user });
  } catch (error) {
    return res.status(500).json({ message: 'Registration failed', error });
  }
};

Step 2: Create User Login with JWT

exports.login = async (req, res) => {
    const { email, password } = req.body;
    const userRepository = getRepository(User);
    const sessionRepository = getRepository(Session);
    const activityLogRepository = getRepository(ActivityLog);

    try {
        // Find the user by email
        const user = await userRepository.findOne({ where: { email } });

        if (!user || !(await bcrypt.compare(password, user.password))) {
            return res.status(401).json({ message: 'Invalid credentials' });
        }

        // Set session expiration time
        const sessionDuration = 60 * 60 * 1000; // 1 hour in milliseconds
        const validUntil = new Date(Date.now() + sessionDuration);

        // Create a new session
        const session = sessionRepository.create({
            user,
            validUntil,
            isActive: true,
        });
        await sessionRepository.save(session);

        // Log the login activity
        await activityLogRepository.save({
            user,
            activityType: 'LOGIN',
            ipAddress: req.ip,
        });

        // Generate JWT token
        const token = jwt.sign(
            { id: user.id, sessionId: session.id },
            process.env.JWT_SECRET,
            { expiresIn: '1h' }  // Token expiration should match session duration
        );

        return res.status(200).json({ token, message: 'Login successful' });

    } catch (error) {
        return res.status(500).json({ message: 'Login failed', error });
    }
};

Part 4: Session Management and Activity Logs

In this section, we’ll dive into how to manage user sessions, handle session expiration, and log user activity such as login and logout. We’ll also create middlewares to validate user sessions and ensure session security using JWT.

Step 1: Middleware for Session Validation

We need to create middleware that checks if the user’s session is still valid. This middleware will inspect the JWT token, decode it, check if the session is active, and if the session has not expired.

authMiddleware.js (Session Validation)

In src/middleware/authMiddleware.js:

exports.authenticateToken = async (req, res, next) => {
    const token = req.headers['authorization'];
  
    if (!token) return res.status(401).json({ message: 'Access token required' });

    try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);

        const sessionRepository = getRepository(Session);
        const session = await sessionRepository.findOne({ where: { id: decoded.sessionId, isActive: true } });

        if (!session) {
            return res.status(403).json({ message: 'Session expired or invalid' });
        }

        // Check if session has expired
        if (new Date() > session.validUntil) {
            session.isActive = false;  // Mark session as inactive
            await sessionRepository.save(session);  // Update session

            return res.status(403).json({ message: 'Session has expired. Please log in again.' });
        }

        // Attach user info to the request
        req.user = { id: decoded.id, sessionId: decoded.sessionId };
        next();

    } catch (err) {
        return res.status(403).json({ message: 'Invalid or expired token' });
    }
};

This middleware extracts the JWT from the Authorization header, verifies it, and checks if the session exists and is still valid. If the session is invalid or expired, it blocks access.

Step 2: Logging User Activities

Each time the user logs in or out, we’ll log the activity in the ActivityLog table. We already log the login activity when the user logs in, so now we’ll handle the logout activity.

User Logout with Activity Log

In src/controllers/userController.js, we will create a logout function that deactivates the session and logs the logout activity.

exports.logout = async (req, res) => {
  const sessionRepository = getRepository(Session);
  const activityLogRepository = getRepository(ActivityLog);

  try {
    // Invalidate the current session
    const session = await sessionRepository.findOne({ where: { id: req.user.sessionId } });
    session.isActive = false;
    await sessionRepository.save(session);

    // Log the logout activity
    await activityLogRepository.save({
      user: { id: req.user.id },  // Only user ID is needed here
      activityType: 'LOGOUT',
      ipAddress: req.ip,
    });

    return res.status(200).json({ message: 'Logout successful' });

  } catch (error) {
    return res.status(500).json({ message: 'Logout failed', error });
  }
};

This function deactivates the current session by setting isActive to false and logs the user’s logout activity.

Step 3: Protecting Routes with Middleware

Now that we have our authMiddleware ready, we can protect our routes to ensure that only authenticated users can access them. Let’s apply this middleware to any route that requires authentication, like viewing user profile or updating account information.

Apply Middleware to Protected Routes

In src/routes/userRoutes.js:

const express = require('express');
const userController = require('../controllers/userController');
const { authenticateToken } = require('../middleware/authMiddleware');
const router = express.Router();

router.post('/register', userController.register);
router.post('/login', userController.login);
router.post('/logout', authenticateToken, userController.logout);

// Example of protected routes
router.get('/profile', authenticateToken, userController.getProfile);
router.put('/profile', authenticateToken, userController.updateProfile);

module.exports = router;

Here, the /profile route is protected by the authenticateToken middleware, which ensures that only authenticated users with a valid session can access it.


Step 4: Implementing the Get Profile and Update Profile Features

Let’s implement the getProfile and updateProfile features for authenticated users.

Get Profile

In src/controllers/userController.js:

exports.getProfile = async (req, res) => {
  const userRepository = getRepository(User);

  try {
    const user = await userRepository.findOne({
      where: { id: req.user.id },
      select: ['id', 'username', 'email'],  // Only return specific fields
    });

    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }

    return res.status(200).json(user);

  } catch (error) {
    return res.status(500).json({ message: 'Error retrieving profile', error });
  }
};

This method retrieves the user’s profile based on the decoded JWT token from the request.

Update Profile

exports.updateProfile = async (req, res) => {
  const { username, email } = req.body;
  const userRepository = getRepository(User);

  try {
    let user = await userRepository.findOne({ where: { id: req.user.id } });

    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }

    user.username = username || user.username;
    user.email = email || user.email;
    
    await userRepository.save(user);

    return res.status(200).json({ message: 'Profile updated successfully', user });

  } catch (error) {
    return res.status(500).json({ message: 'Error updating profile', error });
  }
};

This function allows the user to update their username and email.


Step 5: Testing the Full Workflow

Let’s walk through the workflow for a user:

  1. Registration:
    • User sends a POST request to /register with username, email, and password.
    • The user is created, and a success message is returned.
  2. Login:
    • User sends a POST request to /login with email and password.
    • A JWT token is returned, along with a session creation in the database.
    • The user’s login activity is logged.
  3. Accessing Protected Routes:
    • The user accesses protected routes like /profile by including the JWT in the Authorization header.
    • The authMiddleware verifies the token, checks the session, and allows access.
  4. Logout:
    • User sends a POST request to /logout with the JWT.
    • The session is deactivated, and the user’s logout activity is logged.


Part 6: Testing the Full Workflow

Let’s recap the full workflow and testing steps.

Session Cleanup: Expired sessions are periodically cleaned from the database using the cleanup script.\

User Logs In: A session is created with a 1-hour expiration (validUntil field is set).

User Makes Requests: The session expiration is checked on every request. If the session is valid, it can be optionally extended based on activity.

Session Expires: After 1 hour of inactivity, the session will expire, and the user will be automatically logged out.

Step-by-Step Guide

  1. Create a Separate Cleanup Script

First, create a new file called cleanupSessions.js in your project directory. This file will contain the session cleanup logic and can be run manually or scheduled to run periodically.

cleanupSessions.js

const { createConnection, getRepository, LessThan } = require('typeorm');
const Session = require('./entities/Session');  // Adjust the path to your session entity

// Function to clean up expired sessions
const cleanupExpiredSessions = async () => {
    try {
        const connection = await createConnection();  // Database connection
        const sessionRepository = getRepository(Session);

        // Find and delete expired sessions (expired and inactive sessions)
        const expiredSessions = await sessionRepository.find({
            where: {
                validUntil: LessThan(new Date()),  // Expired sessions
                isActive: false,  // Inactive sessions only
            },
        });

        if (expiredSessions.length > 0) {
            await sessionRepository.remove(expiredSessions);  // Remove expired sessions
            console.log(`Removed ${expiredSessions.length} expired sessions.`);
        } else {
            console.log('No expired sessions found.');
        }

        await connection.close();  // Close database connection
    } catch (error) {
        console.error('Error during session cleanup:', error);
    }
};

// Schedule the cleanup task to run every 5 minutes (300,000 milliseconds)
setInterval(() => {
    console.log('Running session cleanup...');
    cleanupExpiredSessions();
}, 5 * 60 * 1000);  // 5 minutes

The node-cron library allows more flexibility in scheduling jobs with cron-like syntax. You can install it and use it to schedule tasks in your app.

  1. Install node-cron:
npm install node-cron
  1. Set up a Cron Job in Your Application:

Add the following code in your main file (e.g., app.js):

const { createConnection, getRepository, LessThan } = require('typeorm');
const Session = require('./entities/Session');  // Adjust the path to your session entity
const cron = require('node-cron');

// Function to clean up expired sessions
const cleanupExpiredSessions = async () => {
    try {
        const connection = await createConnection();
        const sessionRepository = getRepository(Session);

        const expiredSessions = await sessionRepository.find({
            where: {
                validUntil: LessThan(new Date()),
                isActive: false,
            },
        });

        if (expiredSessions.length > 0) {
            await sessionRepository.remove(expiredSessions);
            console.log(`Removed ${expiredSessions.length} expired sessions.`);
        } else {
            console.log('No expired sessions found.');
        }

        await connection.close();
    } catch (error) {
        console.error('Error during session cleanup:', error);
    }
};

// Schedule the cleanup task to run every 5 minutes using node-cron
cron.schedule('*/5 * * * *', () => {
    console.log('Running scheduled session cleanup...');
    cleanupExpiredSessions();
});

How can we help?