Prashikshak / API /src /services /activity.service.ts
Abhisingh-18's picture
Initial commit: Prashikshak - disaster management training platform
9a92a42
Raw
History Blame Contribute Delete
10.3 kB
import UserActivity, { ActivityAction, ActivityCategory } from '../model/userActivity.model';
import UserStats from '../model/userStats.model';
import { Types } from 'mongoose';
/**
* Service for querying user activities and generating reports
*/
export class ActivityService {
/**
* Get user's activity timeline
*/
static async getUserActivityTimeline(
userId?: string,
options: {
limit?: number;
skip?: number;
startDate?: Date;
endDate?: Date;
category?: ActivityCategory;
action?: ActivityAction;
role?: string;
} = {}
) {
const {
limit = 50,
skip = 0,
startDate,
endDate,
category,
action,
role
} = options;
const query: any = {};
// User filter
if (userId) {
query.user = userId;
} else if (role) {
// If role is specified, find all users with that role first
// This is more efficient than a massive lookup aggregation for this specific case
const User = (await import('../model/user.model')).default;
const usersWithRole = await User.find({ role }).select('_id');
const userIds = usersWithRole.map(u => u._id);
query.user = { $in: userIds };
}
// Date range filter
if (startDate || endDate) {
query.timestamp = {};
if (startDate) query.timestamp.$gte = startDate;
if (endDate) query.timestamp.$lte = endDate;
}
// Category filter
if (category) query.category = category;
// Action filter
if (action) query.action = action;
const [activities, total] = await Promise.all([
UserActivity.find(query)
.sort({ timestamp: -1 })
.limit(limit)
.skip(skip)
.populate('user', 'username email profilePhoto role')
.populate('metadata.eventId', 'title code')
.populate('metadata.formId', 'title')
.lean(),
UserActivity.countDocuments(query)
]);
return {
activities,
pagination: {
total,
limit,
skip,
pages: Math.ceil(total / limit)
}
};
}
/**
* Get user statistics
*/
static async getUserStats(userId: string) {
const stats = await UserStats.findOne({ user: userId });
if (!stats) {
// Create initial stats if not exists
return await UserStats.create({ user: userId });
}
return stats;
}
/**
* Get activity breakdown by category
*/
static async getActivityBreakdown(
userId: string,
startDate?: Date,
endDate?: Date
) {
const matchStage: any = { user: new Types.ObjectId(userId) };
if (startDate || endDate) {
matchStage.timestamp = {};
if (startDate) matchStage.timestamp.$gte = startDate;
if (endDate) matchStage.timestamp.$lte = endDate;
}
const breakdown = await UserActivity.aggregate([
{ $match: matchStage },
{
$group: {
_id: '$category',
count: { $sum: 1 },
lastActivity: { $max: '$timestamp' }
}
},
{ $sort: { count: -1 } }
]);
return breakdown;
}
/**
* Get daily activity counts for a user (for charts)
*/
static async getDailyActivityCounts(
userId: string,
days: number = 30
) {
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const dailyStats = await UserActivity.aggregate([
{
$match: {
user: new Types.ObjectId(userId),
timestamp: { $gte: startDate }
}
},
{
$group: {
_id: {
$dateToString: { format: '%Y-%m-%d', date: '$timestamp' }
},
count: { $sum: 1 },
categories: { $addToSet: '$category' }
}
},
{ $sort: { _id: 1 } }
]);
return dailyStats;
}
/**
* Get most active users (leaderboard)
*/
static async getActiveUsersLeaderboard(
criteria: 'eventsCreated' | 'eventsJoined' | 'loginCount' | 'totalOnlineTime' = 'eventsCreated',
limit: number = 10
) {
const leaderboard = await UserStats.find()
.sort({ [criteria]: -1 })
.limit(limit)
.populate('user', 'username email profilePhoto role organization')
.lean();
return leaderboard;
}
/**
* Calculate online time for a user
*/
static async calculateOnlineTime(userId: string, sessionId: string) {
const activities = await UserActivity.find({
user: userId,
sessionId,
timestamp: { $exists: true }
})
.sort({ timestamp: 1 })
.select('timestamp')
.lean();
if (activities.length < 2) return 0;
const firstActivity = activities[0].timestamp;
const lastActivity = activities[activities.length - 1].timestamp;
const sessionDuration = (lastActivity.getTime() - firstActivity.getTime()) / (1000 * 60); // minutes
// Update user stats
await UserStats.findOneAndUpdate(
{ user: userId },
{
$inc: { totalOnlineTime: sessionDuration },
$set: { lastOnlineTime: lastActivity }
}
);
return sessionDuration;
}
/**
* Get event participation history for a user
*/
static async getEventParticipationHistory(userId: string) {
const eventActivities = await UserActivity.find({
user: userId,
category: ActivityCategory.EVENT,
action: { $in: [ActivityAction.EVENT_JOIN, ActivityAction.EVENT_CHECKIN] }
})
.populate('metadata.eventId', 'title theme startDate endDate organization')
.sort({ timestamp: -1 })
.lean();
return eventActivities;
}
/**
* Get form submission history for a user
*/
static async getFormSubmissionHistory(userId: string) {
const formActivities = await UserActivity.find({
user: userId,
category: ActivityCategory.FORM,
action: ActivityAction.FORM_SUBMIT
})
.populate('metadata.formId', 'title event')
.sort({ timestamp: -1 })
.lean();
return formActivities;
}
/**
* Get verification activity (for organizations and admins)
*/
static async getVerificationActivity(userId: string) {
const verifications = await UserActivity.find({
user: userId,
category: ActivityCategory.VERIFICATION
})
.sort({ timestamp: -1 })
.lean();
const summary = {
trainersVerified: verifications.filter(v => v.action === ActivityAction.TRAINER_VERIFY).length,
trainersRejected: verifications.filter(v => v.action === ActivityAction.TRAINER_REJECT).length,
organizationsVerified: verifications.filter(v => v.action === ActivityAction.ORG_VERIFY).length,
organizationsRejected: verifications.filter(v => v.action === ActivityAction.ORG_REJECT).length
};
return { verifications, summary };
}
/**
* Export user activity to CSV
*/
static async exportUserActivityToCSV(userId: string, startDate?: Date, endDate?: Date) {
const query: any = { user: userId };
if (startDate || endDate) {
query.timestamp = {};
if (startDate) query.timestamp.$gte = startDate;
if (endDate) query.timestamp.$lte = endDate;
}
const activities = await UserActivity.find(query)
.sort({ timestamp: -1 })
.lean();
// Convert to CSV format
const headers = ['Timestamp', 'Action', 'Category', 'Description', 'Success', 'IP Address', 'Device'];
const rows = activities.map(activity => [
activity.timestamp.toISOString(),
activity.action,
activity.category,
activity.description,
activity.success ? 'Yes' : 'No',
activity.metadata?.ipAddress || 'N/A',
activity.metadata?.device || 'N/A'
]);
const csvContent = [
headers.join(','),
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
].join('\n');
return csvContent;
}
/**
* Get security alerts (failed logins, unauthorized access)
*/
static async getSecurityAlerts(userId: string, days: number = 7) {
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const alerts = await UserActivity.find({
user: userId,
success: false,
timestamp: { $gte: startDate },
action: {
$in: [
ActivityAction.LOGIN,
ActivityAction.UNAUTHORIZED_ACCESS,
ActivityAction.API_ERROR
]
}
})
.sort({ timestamp: -1 })
.lean();
return alerts;
}
/**
* Generate comprehensive user report
*/
static async generateUserReport(userId: string) {
const [stats, breakdown, recentActivities, securityAlerts] = await Promise.all([
this.getUserStats(userId),
this.getActivityBreakdown(userId),
this.getUserActivityTimeline(userId, { limit: 20 }),
this.getSecurityAlerts(userId)
]);
return {
stats,
breakdown,
recentActivities: recentActivities.activities,
securityAlerts,
generatedAt: new Date()
};
}
/**
* Get system-wide recent activities (admin only)
* Filters for important activities like form creation, org registration, trainer approval, etc.
*/
static async getSystemRecentActivities(options: {
limit?: number;
skip?: number;
} = {}) {
const { limit = 20, skip = 0 } = options;
// Important actions to track
const importantActions = [
ActivityAction.FORM_CREATE,
ActivityAction.SIGNUP, // New organization/trainer registration
ActivityAction.TRAINER_VERIFY,
ActivityAction.ORG_VERIFY,
ActivityAction.EVENT_CREATE,
ActivityAction.EVENT_CHECKIN, // Training started
];
const query = {
action: { $in: importantActions },
success: true // Only successful activities
};
const [activities, total] = await Promise.all([
UserActivity.find(query)
.sort({ timestamp: -1 })
.limit(limit)
.skip(skip)
.populate('user', 'username email profilePhoto role organization')
.populate('metadata.eventId', 'title code')
.populate('metadata.formId', 'title')
.lean(),
UserActivity.countDocuments(query)
]);
return {
activities,
pagination: {
total,
limit,
skip,
pages: Math.ceil(total / limit)
}
};
}
}
export default ActivityService;