Skip to main content

Login History with TTL

Express Base API automatically tracks all user login sessions in a separate MongoDB collection with automatic cleanup after 90 days using TTL (Time-To-Live) indexes.

Features​

  • πŸ“Š Automatic Tracking - Every login creates a history entry
  • ⏰ Auto Cleanup - MongoDB TTL deletes entries after 90 days
  • 🌍 Location Tracking - IP-based geolocation
  • πŸ“± Device Detection - Browser, OS, platform information
  • πŸ”’ Separate Collection - Unlimited history without document size limits
  • πŸš€ Fast Queries - Indexed for performance
  • πŸ“ˆ Statistics - Login analytics and insights

Why Separate Collection?​

Previous Approach (embedded array):

// ❌ Problem: Unlimited array growth
auth: {
user: ObjectId,
login_history: [
{ ip: '1.2.3.4', login_at: Date, ... },
{ ip: '1.2.3.4', login_at: Date, ... },
// ... can grow indefinitely
]
}

Issues:

  • MongoDB document size limit (16MB)
  • Slow queries as array grows
  • Inefficient indexing
  • No automatic cleanup

New Approach (separate collection):

// βœ… Solution: Separate collection with TTL
login_histories: {
_id: ObjectId,
auth: ObjectId, // Reference
user: ObjectId, // Reference
ip: string,
login_at: Date, // TTL index on this field
// ... other fields
}

Benefits:

  • No document size limits
  • Automatic cleanup after 90 days
  • Fast indexed queries
  • Scalable to millions of logins

Architecture​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Login β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Create Login History Entry β”‚
β”‚ - User info β”‚
β”‚ - Device info (browser, OS) β”‚
β”‚ - Location (IP-based) β”‚
β”‚ - Token references β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ MongoDB: login_histories β”‚
β”‚ TTL Index: expires after 90 daysβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data Model​

Schema​

interface ILoginHistory extends BaseDocument {
auth: ObjectId | IAuth; // Reference to auth document
user: ObjectId | IUser; // Reference to user document
ip: string; // IP address
token: string; // Access token
refresh_token: string; // Refresh token
login_at: Date; // Login timestamp (TTL field)
devices?: DeviceInfo; // Device information
locations?: LocationInfo; // Location information
user_agent?: string; // Full user agent string
}

interface DeviceInfo {
browser?: {
name: string; // e.g., "Chrome"
version: string; // e.g., "120.0.0.0"
major: string; // e.g., "120"
};
os?: {
name: string; // e.g., "Linux"
version: string; // e.g., "x86_64"
};
platform?: string; // e.g., "Linux x86_64"
source?: string; // e.g., "Desktop"
}

interface LocationInfo {
country?: string; // e.g., "United States"
region?: string; // e.g., "California"
city?: string; // e.g., "San Francisco"
timezone?: string; // e.g., "America/Los_Angeles"
}

MongoDB Model​

const loginHistorySchema = {
auth: {
type: Schema.Types.ObjectId,
ref: 'auths',
required: true,
index: true,
},
user: {
type: Schema.Types.ObjectId,
ref: 'users',
required: true,
index: true,
},
ip: {
type: String,
required: true,
},
token: {
type: String,
required: true,
},
refresh_token: {
type: String,
required: true,
index: true, // For fast logout
},
login_at: {
type: Date,
required: true,
default: Date.now,
index: true, // TTL index
},
devices: {
browser: {
name: { type: String, required: false },
version: { type: String, required: false },
major: { type: String, required: false },
},
os: {
name: { type: String, required: false },
version: { type: String, required: false },
},
platform: { type: String, required: false },
source: { type: String, required: false },
},
locations: {
country: { type: String, required: false },
region: { type: String, required: false },
city: { type: String, required: false },
timezone: { type: String, required: false },
},
user_agent: {
type: String,
required: false,
},
};

// TTL Index - MongoDB automatically deletes after 90 days
LoginHistory.schema.index(
{ login_at: 1 },
{ expireAfterSeconds: 7776000 } // 90 days = 7,776,000 seconds
);

// Compound index for efficient queries
LoginHistory.schema.index({ user: 1, login_at: -1 });

TTL (Time-To-Live) Index​

How It Works​

MongoDB's TTL index automatically deletes documents after a specified time:

  1. Index Created: { login_at: 1 }, { expireAfterSeconds: 7776000 }
  2. Background Task: MongoDB runs every 60 seconds
  3. Deletion: Removes documents where login_at + 90 days < now
  4. Automatic: No manual intervention needed

Example​

// Login on January 1, 2024
{
_id: "65abc123...",
login_at: ISODate("2024-01-01T10:00:00Z"),
// ... other fields
}

// MongoDB automatically deletes on April 1, 2024 (90 days later)

Benefits​

  • Zero Maintenance - Fully automatic
  • Consistent Performance - Database size stays bounded
  • Compliance - Automatic data retention policy
  • Audit Trail - 90-day login history for security

Customize TTL Duration​

Edit src/models/auth/login-history.model.ts:

// 30 days
LoginHistory.schema.index(
{ login_at: 1 },
{ expireAfterSeconds: 2592000 }
);

// 180 days (6 months)
LoginHistory.schema.index(
{ login_at: 1 },
{ expireAfterSeconds: 15552000 }
);

// 365 days (1 year)
LoginHistory.schema.index(
{ login_at: 1 },
{ expireAfterSeconds: 31536000 }
);

Login History Service​

Available Methods​

class LoginHistoryService extends BaseService<ILoginHistory> {
// Create login history entry
async createLoginHistory(data: CreateLoginHistoryData)

// Get user's login history
async getByUser(userId: string, options?)

// Get auth's login history
async getByAuth(authId: string, options?)

// Find by refresh token
async findByRefreshToken(refreshToken: string)

// Update token after refresh
async updateToken(refreshToken: string, newToken: string, newRefreshToken: string)

// Delete all user sessions
async deleteAllByUser(userId: string)

// Delete by refresh token
async deleteByRefreshToken(refreshToken: string)

// Get login statistics
async getLoginStats(userId: string)

// Manual cleanup (optional - TTL handles this)
async cleanup(daysToKeep?: number)
}

Usage Examples​

Get User's Login History​

import loginHistoryService from './src/services/auth/login-history.service';

// Get last 50 logins
const history = await loginHistoryService.getByUser(userId, {
limit: 50,
sort: { login_at: -1 },
populate: 'user',
});

console.log(history.data);
// [
// {
// _id: "65abc123...",
// user: { username: "johndoe", email: "john@example.com" },
// ip: "192.168.1.1",
// login_at: "2024-01-15T10:30:00Z",
// devices: {
// browser: { name: "Chrome", version: "120.0.0.0" },
// os: { name: "Linux" }
// },
// locations: {
// city: "San Francisco",
// country: "United States"
// }
// },
// // ... more entries
// ]

Get Login Statistics​

const stats = await loginHistoryService.getLoginStats(userId);

console.log(stats);
// {
// total_logins: 156,
// unique_ips: 8,
// unique_devices: 3,
// last_login: "2024-01-15T10:30:00.000Z",
// by_device: {
// "Desktop": 120,
// "Mobile": 36
// },
// by_location: {
// "San Francisco": 89,
// "New York": 45,
// "London": 22
// }
// }

Create Login History Entry​

// Automatically called on login in auth.service.ts
const loginHistory = await loginHistoryService.createLoginHistory({
auth: authId,
user: userId,
ip: req.ip,
token: accessToken,
refresh_token: refreshToken,
devices: {
browser: {
name: req.useragent.browser,
version: req.useragent.version,
},
os: {
name: req.useragent.os,
},
platform: req.useragent.platform,
source: req.useragent.source,
},
locations: {
country: req.geo?.country,
city: req.geo?.city,
region: req.geo?.region,
},
user_agent: req.headers['user-agent'],
});

Delete All User Sessions​

// Force logout all devices
const deleted = await loginHistoryService.deleteAllByUser(userId);

console.log(`Deleted ${deleted} active sessions`);

Integration with Authentication​

On Login​

// src/services/auth/auth.service.ts
async login(credentials) {
// ... authentication logic

// Create login history entry
await loginHistoryService.createLoginHistory({
auth: auth._id,
user: user._id,
ip: req.ip,
token: accessToken,
refresh_token: refreshToken,
devices: deviceInfo,
locations: locationInfo,
user_agent: req.headers['user-agent'],
});

return { token: accessToken, refresh_token: refreshToken };
}

On Logout​

// utils/middlewares/auth/auth.middleware.ts
async logout(req, res) {
const { refresh_token } = req.body;

// Delete login history entry
await loginHistoryService.deleteByRefreshToken(refresh_token);

return response.success(res, null, 200, 'Logout successful');
}

On Password Reset​

// src/services/auth/auth.service.ts
async resetPassword(token, newPassword) {
// ... reset password logic

// Delete all user sessions (force re-login)
await loginHistoryService.deleteAllByUser(user._id);

// Invalidate all caches
await this.invalidateUserCache(user._id);
}

Admin Endpoints​

Get User's Login History​

GET /admin/user/:userId/login-history
Authorization: Bearer {admin_token}
x-api-key: {api_key}

Response:

{
"status_code": 200,
"status": "SUCCESS",
"data": [
{
"_id": "65abc123...",
"user": "65def456...",
"ip": "192.168.1.1",
"login_at": "2024-01-15T10:30:00.000Z",
"devices": {
"browser": {
"name": "Chrome",
"version": "120.0.0.0",
"major": "120"
},
"os": {
"name": "Linux",
"version": "x86_64"
},
"platform": "Linux x86_64",
"source": "Desktop"
},
"locations": {
"city": "San Francisco",
"country": "United States",
"region": "California",
"timezone": "America/Los_Angeles"
}
}
]
}

Get Login Statistics​

GET /admin/user/:userId/login-stats
Authorization: Bearer {admin_token}
x-api-key: {api_key}

Response:

{
"status_code": 200,
"status": "SUCCESS",
"data": {
"total_logins": 156,
"unique_ips": 8,
"unique_devices": 3,
"last_login": "2024-01-15T10:30:00.000Z"
}
}

Security Features​

Suspicious Activity Detection​

Monitor login patterns for anomalies:

async detectSuspiciousLogin(userId: string, currentIP: string) {
const history = await loginHistoryService.getByUser(userId, {
limit: 10,
sort: { login_at: -1 },
});

const recentIPs = history.data.map(h => h.ip);
const uniqueIPs = [...new Set(recentIPs)];

// New IP detected
if (!recentIPs.includes(currentIP)) {
// Send security alert email
await mailService.sendMail({
to: user.email,
subject: 'New Login from Unknown Location',
template: 'security-alert',
context: {
ip: currentIP,
usual_locations: uniqueIPs.slice(0, 3).join(', '),
},
});
}
}

Force Logout All Sessions​

async logoutAllDevices(userId: string) {
// Delete all login history
const deleted = await loginHistoryService.deleteAllByUser(userId);

// Invalidate all caches
await cacheService.delete(`user:auth:${userId}`);

return { deleted };
}

Performance Considerations​

Indexes​

Critical indexes for performance:

// Single field indexes
{ user: 1 } // Fast user queries
{ auth: 1 } // Fast auth queries
{ refresh_token: 1 } // Fast logout
{ login_at: 1 } // TTL cleanup

// Compound index
{ user: 1, login_at: -1 } // Fast user history with sorting

Query Optimization​

// βœ… Good - Use indexed fields
await LoginHistory.find({ user: userId })
.sort({ login_at: -1 })
.limit(50);

// ❌ Bad - Full collection scan
await LoginHistory.find({ 'devices.browser.name': 'Chrome' });

Monitoring​

Check TTL Index​

mongo
use your_database
db.login_histories.getIndexes()

Look for:

{
"v": 2,
"key": { "login_at": 1 },
"name": "login_at_1",
"expireAfterSeconds": 7776000
}

Monitor Collection Size​

db.login_histories.stats()
{
"count": 15432,
"size": 5432100,
"avgObjSize": 352,
"storageSize": 2097152
}

Troubleshooting​

TTL Not Deleting​

Cause: TTL background task runs every 60 seconds

Solution: Wait or manually trigger:

db.adminCommand({ compact: 'login_histories' })

Large Collection Size​

Cause: High login volume or long TTL

Solutions:

  • Reduce TTL duration (e.g., 30 days instead of 90)
  • Archive old data before deletion
  • Increase server resources

Slow Queries​

Cause: Missing indexes or inefficient queries

Solutions:

  • Ensure indexes exist: db.login_histories.getIndexes()
  • Use explain() to analyze queries
  • Add compound indexes for common query patterns

Next Steps​


Questions? Check the GitHub Discussions or open an issue.