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:
- Index Created:
{ login_at: 1 }, { expireAfterSeconds: 7776000 } - Background Task: MongoDB runs every 60 seconds
- Deletion: Removes documents where
login_at + 90 days < now - 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β
- Authentication - Complete auth system
- Caching - Performance optimization
- Logging - Request/response logging
Questions? Check the GitHub Discussions or open an issue.