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:
- Registration:
- User sends a POST request to
/register
withusername
,email
, andpassword
. - The user is created, and a success message is returned.
- User sends a POST request to
- Login:
- User sends a POST request to
/login
withemail
andpassword
. - A JWT token is returned, along with a session creation in the database.
- The user’s login activity is logged.
- User sends a POST request to
- Accessing Protected Routes:
- The user accesses protected routes like
/profile
by including the JWT in theAuthorization
header. - The
authMiddleware
verifies the token, checks the session, and allows access.
- The user accesses protected routes like
- Logout:
- User sends a POST request to
/logout
with the JWT. - The session is deactivated, and the user’s logout activity is logged.
- User sends a POST request to
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
- 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.
- Install
node-cron
:
npm install node-cron
- 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();
});