Prashikshak / API /src /controller /user.controller.ts
Abhisingh-18's picture
Initial commit: Prashikshak - disaster management training platform
9a92a42
Raw
History Blame Contribute Delete
35.7 kB
import { Request, Response } from 'express';
import User, { IUser } from '../model/user.model';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import sendMail from '../util/mailer.util';
import { JWT_CONFIG, PASSWORD_RESET_CONFIG } from '../config/env.config';
import cloudinary from '../util/cloudinary.util';
import { Types } from 'mongoose';
import { oauth2Client } from '../util/googleClient.util';
import axios from 'axios';
import { FRONTEND_URL } from '../config/env.config';
import speakeasy from 'speakeasy';
import QRCode from 'qrcode';
import { getRedisClient } from '../util/redis.util';
interface IUserRequest extends Request {
userId?: string;
body: {
username?: string;
email?: string;
password?: string;
currentPassword?: string;
newPassword?: string;
role?: string;
traineeCategory?: string;
organization?: string;
workDesignation?: string;
organizationType?: string;
govtIdCard?: string;
[key: string]: any;
};
}
// ==================== GOOGLE OAUTH ====================
export const authWithGoogle = async (req: Request, res: Response) => {
const code = req.query.code;
const role = req.query.state as string; // We'll pass role as state parameter
if (!code) {
return res.json({ error: "No code provided" });
}
try {
const googleRes = await oauth2Client.getToken(code as string);
oauth2Client.setCredentials(googleRes.tokens);
const userRes: any = await axios.get(
`https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=${googleRes.tokens.access_token}`
);
const { email, name, picture } = userRes.data;
// Clean username
let cleanUsername = name.replace(/\s+/g, "").toLowerCase();
let finalUsername = cleanUsername;
let usernameExists = await User.findOne({ username: finalUsername });
while (usernameExists) {
const randomSuffix = Math.floor(1000 + Math.random() * 9000);
finalUsername = `${cleanUsername}${randomSuffix}`;
usernameExists = await User.findOne({ username: finalUsername });
}
let user: IUser | null = await User.findOne({ email });
// New user signup
if (!user) {
const userRole = role && ['trainee', 'trainer'].includes(role) ? role : 'trainee';
user = new User({
username: finalUsername,
email,
profilePhoto: picture,
usedOAuth: true,
role: userRole,
isActive: true
});
await user.save();
}
// Check if user needs to upload documents (for trainers and organizations)
const requiresDocumentUpload = (user.role === 'trainer' || user.role === 'organization') && !user.documentsUploaded;
const token = jwt.sign(
{ userId: user._id?.toString() },
JWT_CONFIG.secret,
{ expiresIn: JWT_CONFIG.expiresIn } as jwt.SignOptions
);
// Create session in Redis
const redis = await getRedisClient();
const sessionKey = `session:${user._id}:${token.substring(0, 10)}`;
await redis.set(sessionKey, 'valid', { EX: 7 * 24 * 60 * 60 }); // 7 days
res.status(200).json({
message: "success",
token,
requires2FASetup: !user.isTwoFactorEnabled,
requiresDocumentUpload,
user: {
_id: user._id,
username: user.username,
email: user.email,
role: user.role,
profilePhoto: user.profilePhoto,
organizationVerificationStatus: user.organizationVerificationStatus,
verificationStatus: user.verificationStatus,
documentsUploaded: user.documentsUploaded,
rejectionReason: user.rejectionReason,
createdAt: user.createdAt,
lastActive: user.lastActive,
isActive: user.isActive,
}
});
} catch (err) {
console.log("OAuth Error:", err);
res.status(500).json({
message: "OAuth authentication failed. Please try again."
});
}
};
// ==================== TRAINEE SIGNUP ====================
export const signupTrainee = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
let { username, email, password, traineeCategory } = req.body;
if (!username || !email || !password) {
return res.status(400).json({ message: 'Username, email, and password are required.' });
}
username = username?.trim();
email = email?.trim().toLowerCase();
if (!username || !email || !password) {
return res.status(400).json({ message: 'All fields are required.' });
}
const existingEmail = await User.findOne({ email });
if (existingEmail) {
return res.status(409).json({ message: 'Email already exists. Try logging in instead.' });
}
const existingUsername = await User.findOne({ username });
if (existingUsername) {
return res.status(409).json({ message: 'Username already taken. Try a different one.' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = new User({
username,
email,
password: hashedPassword,
usedOAuth: false,
role: 'trainee',
traineeCategory: traineeCategory || 'other',
isActive: true
});
await user.save();
return res.status(201).json({
message: 'Trainee account created successfully. You can now login.'
});
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== TRAINER SIGNUP ====================
export const signupTrainer = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
let { username, email, password, organization, workDesignation, govtIdCard } = req.body;
if (!username || !email || !password || !organization || !workDesignation) {
return res.status(400).json({
message: 'Username, email, password, organization, and work designation are required.'
});
}
username = username?.trim();
email = email?.trim().toLowerCase();
const existingEmail = await User.findOne({ email });
if (existingEmail) {
return res.status(409).json({ message: 'Email already exists. Try logging in instead.' });
}
const existingUsername = await User.findOne({ username });
if (existingUsername) {
return res.status(409).json({ message: 'Username already taken. Try a different one.' });
}
// Verify organization exists
const orgExists = await User.findOne({
_id: organization,
role: 'organization'
});
if (!orgExists) {
return res.status(404).json({ message: 'Organization not found.' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = new User({
username,
email,
password: hashedPassword,
usedOAuth: false,
role: 'trainer',
organization,
workDesignation,
govtIdCard,
organizationVerificationStatus: 'pending',
isActive: true
});
await user.save();
// Notify organization about new trainer request
await sendNotificationToOrganization(organization, user);
return res.status(201).json({
message: 'Trainer account created successfully. Waiting for organization verification.'
});
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== ORGANIZATION SIGNUP ====================
export const signupOrganization = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
let { username, email, password, organizationType } = req.body;
if (!username || !email || !password || !organizationType) {
return res.status(400).json({
message: 'Username, email, password, and organization type are required.'
});
}
username = username?.trim();
email = email?.trim().toLowerCase();
const existingEmail = await User.findOne({ email });
if (existingEmail) {
return res.status(409).json({ message: 'Email already exists. Try logging in instead.' });
}
const existingUsername = await User.findOne({ username });
if (existingUsername) {
return res.status(409).json({ message: 'Username already taken. Try a different one.' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = new User({
username,
email,
password: hashedPassword,
usedOAuth: false,
role: 'organization',
organizationType,
verificationStatus: 'pending',
isActive: true
});
await user.save();
// Notify admins about new organization registration
await notifyAdminsAboutNewOrganization(user);
return res.status(201).json({
message: 'Organization registered successfully. Waiting for admin verification.'
});
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== UPLOAD DOCUMENTS (ORGANIZATION & TRAINER) ====================
export const uploadOrganizationDocuments = async (req: any, res: Response): Promise<Response> => {
try {
const user: IUser | null = await User.findById(req.userId);
if (!user) return res.status(404).json({ message: 'User not found.' });
if (user.role !== 'organization' && user.role !== 'trainer') {
return res.status(403).json({ message: 'Only organizations and trainers can upload documents.' });
}
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).json({ message: 'No files uploaded.' });
}
const uploadedDocs: any = {};
let govtIdUrl: string | undefined;
// Handle trainer government ID upload
if (user.role === 'trainer' && req.files.govtIdCard) {
govtIdUrl = await uploadToCloudinary(
req.files.govtIdCard[0],
`trainer_docs/${user._id}/govt_id`
);
user.govtIdCard = govtIdUrl;
}
// Handle organization document uploads
if (user.role === 'organization') {
// Upload registration certificate
if (req.files.registrationCertificate) {
const regCert = await uploadToCloudinary(
req.files.registrationCertificate[0],
`org_docs/${user._id}/registration`
);
uploadedDocs.registrationCertificate = regCert;
}
// Upload GST certificate
if (req.files.gstCertificate) {
const gstCert = await uploadToCloudinary(
req.files.gstCertificate[0],
`org_docs/${user._id}/gst`
);
uploadedDocs.gstCertificate = gstCert;
}
// Upload authorization letter
if (req.files.authorizationLetter) {
const authLetter = await uploadToCloudinary(
req.files.authorizationLetter[0],
`org_docs/${user._id}/authorization`
);
uploadedDocs.authorizationLetter = authLetter;
}
// Upload additional documents
if (req.files.additionalDocs) {
const additionalUrls: string[] = [];
for (let i = 0; i < req.files.additionalDocs.length; i++) {
const url = await uploadToCloudinary(
req.files.additionalDocs[i],
`org_docs/${user._id}/additional_${i}`
);
additionalUrls.push(url);
}
uploadedDocs.additionalDocs = additionalUrls;
}
user.documents = { ...user.documents, ...uploadedDocs };
}
// Mark documents as uploaded
user.documentsUploaded = true;
await user.save();
return res.status(200).json({
message: 'Documents uploaded successfully.',
documents: user.documents,
govtIdCard: govtIdUrl
});
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== LOGIN ====================
export const login = async (req: IUserRequest, res: Response): Promise<Response> => {
console.log("login req came");
try {
let { email, password } = req.body;
email = email?.trim().toLowerCase();
if (!email || !password) {
return res.status(400).json({ message: "All fields are required." });
}
const user: IUser | null = await User.findOne({ email }).populate('organization', 'username email');
if (!user) {
return res.status(401).json({ message: "User not found!" });
}
if (user.usedOAuth) {
return res.status(401).json({
message: "This account uses Google OAuth. Please login using Google."
});
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ message: "Invalid credentials." });
}
// 2FA Check
if (user.isTwoFactorEnabled) {
const { otp } = req.body;
if (!otp) {
return res.status(403).json({
message: "2FA is enabled. Please provide OTP.",
code: "2fa_required"
});
}
const verified = speakeasy.totp.verify({
secret: user.twoFactorSecret!,
encoding: 'base32',
token: otp
});
if (!verified) {
return res.status(401).json({ message: "Invalid OTP." });
}
} else {
// 2FA NOT ENABLED -> Mandatory Setup
// We allow login, but the frontend will redirect to setup page
// And middleware will block other actions
}
// Check if user needs to upload documents (for trainers and organizations)
const requiresDocumentUpload = (user.role === 'trainer' || user.role === 'organization') && !user.documentsUploaded;
const token = jwt.sign(
{ userId: user._id?.toString() },
JWT_CONFIG.secret,
{ expiresIn: JWT_CONFIG.expiresIn } as jwt.SignOptions
);
// Create session in Redis
const redis = await getRedisClient();
const sessionKey = `session:${user._id}:${token.substring(0, 10)}`;
await redis.set(sessionKey, 'valid', { EX: 7 * 24 * 60 * 60 }); // 7 days
user.lastActive = new Date();
await user.save();
return res.status(200).json({
token,
requires2FASetup: !user.isTwoFactorEnabled,
requiresDocumentUpload,
user: {
_id: user._id,
username: user.username,
email: user.email,
role: user.role,
profilePhoto: user.profilePhoto,
traineeCategory: user.traineeCategory,
organization: user.organization,
workDesignation: user.workDesignation,
organizationType: user.organizationType,
organizationVerificationStatus: user.organizationVerificationStatus,
verificationStatus: user.verificationStatus,
documentsUploaded: user.documentsUploaded,
rejectionReason: user.rejectionReason,
createdAt: user.createdAt,
lastActive: user.lastActive,
isActive: user.isActive,
},
});
} catch (err: any) {
return res.status(500).json({ message: "Server error", error: err.message });
}
};
// ==================== ORGANIZATION VERIFY TRAINER ====================
export const verifyTrainer = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const organizationId = req.userId;
const { trainerId, status, rejectionReason } = req.body; // status: 'verified' or 'rejected'
if (!trainerId || !status || !['verified', 'rejected'].includes(status)) {
return res.status(400).json({
message: 'Trainer ID and valid status (verified/rejected) are required.'
});
}
if (status === 'rejected' && !rejectionReason) {
return res.status(400).json({
message: 'Rejection reason is required when rejecting a trainer.'
});
}
const organization = await User.findById(organizationId);
if (!organization || organization.role !== 'organization') {
return res.status(403).json({ message: 'Only organizations can verify trainers.' });
}
const trainer = await User.findOne({
_id: trainerId,
role: 'trainer',
organization: organizationId
});
if (!trainer) {
return res.status(404).json({ message: 'Trainer not found or not associated with your organization.' });
}
trainer.organizationVerificationStatus = status;
if (status === 'rejected') {
trainer.rejectionReason = rejectionReason;
} else {
trainer.rejectionReason = undefined; // Clear rejection reason if verified
}
await trainer.save();
// Send email notification to trainer
const emailSubject = `Trainer Verification ${status === 'verified' ? 'Approved' : 'Rejected'}`;
let emailText = `Your trainer account has been ${status} by ${organization.username}.`;
let emailHtml = `<p>Your trainer account has been <strong>${status}</strong> by ${organization.username}.</p>`;
if (status === 'rejected') {
emailText += ` Reason: ${rejectionReason}`;
emailHtml += `<p><strong>Reason:</strong> ${rejectionReason}</p>`;
}
await sendMail({
to: trainer.email,
subject: emailSubject,
text: emailText,
html: emailHtml
});
return res.status(200).json({
message: `Trainer ${status} successfully.`,
trainer: {
_id: trainer._id,
username: trainer.username,
email: trainer.email,
organizationVerificationStatus: trainer.organizationVerificationStatus
}
});
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== ADMIN VERIFY ORGANIZATION ====================
export const verifyOrganization = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const adminId = req.userId;
const { organizationId, status, rejectionReason } = req.body; // status: 'approved' or 'rejected'
if (!organizationId || !status || !['approved', 'rejected'].includes(status)) {
return res.status(400).json({
message: 'Organization ID and valid status (approved/rejected) are required.'
});
}
if (status === 'rejected' && !rejectionReason) {
return res.status(400).json({
message: 'Rejection reason is required when rejecting an organization.'
});
}
const admin = await User.findById(adminId);
if (!admin || admin.role !== 'admin') {
return res.status(403).json({ message: 'Only admins can verify organizations.' });
}
const organization = await User.findOne({
_id: organizationId,
role: 'organization'
});
if (!organization) {
return res.status(404).json({ message: 'Organization not found.' });
}
organization.verificationStatus = status;
organization.verifiedBy = new Types.ObjectId(adminId);
if (status === 'rejected') {
organization.rejectionReason = rejectionReason;
} else {
organization.rejectionReason = undefined;
}
await organization.save();
// Send email notification to organization
const emailSubject = `Organization Verification ${status === 'approved' ? 'Approved' : 'Rejected'}`;
let emailText = `Your organization has been ${status} by the admin.`;
let emailHtml = `<p>Your organization has been <strong>${status}</strong> by the admin.</p>`;
if (status === 'rejected') {
emailText += ` Reason: ${rejectionReason}`;
emailHtml += `<p><strong>Reason:</strong> ${rejectionReason}</p>`;
}
await sendMail({
to: organization.email,
subject: emailSubject,
text: emailText,
html: emailHtml
});
return res.status(200).json({
message: `Organization ${status} successfully.`,
organization: {
_id: organization._id,
username: organization.username,
email: organization.email,
verificationStatus: organization.verificationStatus
}
});
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET PENDING TRAINERS (FOR ORGANIZATION) ====================
export const getPendingTrainers = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const organizationId = req.userId;
const pendingTrainers = await User.find({
role: 'trainer',
organization: organizationId,
organizationVerificationStatus: 'pending'
}).select('username email workDesignation govtIdCard createdAt');
return res.status(200).json({
count: pendingTrainers.length,
trainers: pendingTrainers
});
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET PENDING ORGANIZATIONS (FOR ADMIN) ====================
export const getPendingOrganizations = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const adminId = req.userId;
const admin = await User.findById(adminId);
if (!admin || admin.role !== 'admin') {
return res.status(403).json({ message: 'Only admins can view pending organizations.' });
}
const pendingOrgs = await User.find({
role: 'organization',
verificationStatus: 'pending'
}).select('username email organizationType documents createdAt');
return res.status(200).json({
count: pendingOrgs.length,
organizations: pendingOrgs
});
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== REAPPLY FOR VERIFICATION ====================
export const reapplyForVerification = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const userId = req.userId;
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ message: 'User not found.' });
}
if (user.role === 'trainer') {
if (user.organizationVerificationStatus !== 'rejected') {
return res.status(400).json({ message: 'You can only reapply if your application was rejected.' });
}
user.organizationVerificationStatus = 'pending';
user.rejectionReason = undefined;
} else if (user.role === 'organization') {
if (user.verificationStatus !== 'rejected') {
return res.status(400).json({ message: 'You can only reapply if your application was rejected.' });
}
user.verificationStatus = 'pending';
user.rejectionReason = undefined;
} else {
return res.status(400).json({ message: 'Invalid role for verification reapplication.' });
}
await user.save();
return res.status(200).json({
message: 'Reapplication successful. Your status is now pending.',
user: {
_id: user._id,
verificationStatus: user.verificationStatus,
organizationVerificationStatus: user.organizationVerificationStatus
}
});
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET ALL ORGANIZATIONS ====================
export const getAllOrganizations = async (req: Request, res: Response): Promise<Response> => {
try {
const organizations = await User.find({
role: 'organization',
verificationStatus: 'approved'
}).select('_id username email organizationType');
return res.status(200).json({
count: organizations.length,
organizations
});
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET CURRENT USER ====================
export const getUser = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const user: IUser | null = await User.findById(req.userId)
.select('-password -resetPasswordToken -resetPasswordExpires')
.populate('organization', 'username email organizationType');
if (!user) return res.status(404).json({ message: 'User not found.' });
return res.status(200).json(user);
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== UPDATE USER ====================
export const updateUser = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const updates = req.body;
const userId = req.userId;
// Don't allow password update here
if (updates.password) delete updates.password;
// Don't allow role changes
if (updates.role) delete updates.role;
// Don't allow verification status changes
if (updates.verificationStatus) delete updates.verificationStatus;
if (updates.organizationVerificationStatus) delete updates.organizationVerificationStatus;
const currentUser: IUser | null = await User.findById(userId);
if (!currentUser) {
return res.status(404).json({ message: "User not found." });
}
// Block username/email edits for OAuth accounts
if (currentUser.usedOAuth && updates.email) {
return res.status(403).json({
message: "Cannot edit email for Google OAuth accounts.",
});
}
// Check username uniqueness
if (updates.username && updates.username !== currentUser.username) {
const existingUsername = await User.findOne({
username: updates.username,
_id: { $ne: userId },
});
if (existingUsername) {
return res.status(409).json({
message: "Username already taken. Try a different one.",
});
}
}
const updatedUser: IUser | null = await User.findByIdAndUpdate(
userId,
updates,
{ new: true }
).select("-password");
if (!updatedUser) {
return res.status(404).json({ message: "User not found." });
}
return res.status(200).json(updatedUser);
} catch (err: any) {
return res.status(500).json({ message: "Server error", error: err.message });
}
};
// ==================== CHANGE PASSWORD ====================
export const changePassword = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
return res.status(400).json({
message: 'Current password and new password are required.'
});
}
const user: IUser | null = await User.findById(req.userId);
if (!user) return res.status(404).json({ message: 'User not found.' });
const isMatch = await bcrypt.compare(currentPassword, user.password);
if (!isMatch) {
return res.status(401).json({ message: 'Current password is incorrect.' });
}
const hashedPassword = await bcrypt.hash(newPassword, 10);
user.password = hashedPassword;
await user.save();
return res.status(200).json({ message: 'Password changed successfully.' });
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== UPLOAD PROFILE PHOTO ====================
export const uploadProfilePhoto = async (req: any, res: Response): Promise<Response> => {
try {
const user: IUser | null = await User.findById(req.userId);
if (!user) return res.status(404).json({ message: 'User not found.' });
if (!req.file) return res.status(400).json({ message: 'No file uploaded.' });
return new Promise((resolve) => {
const uploadStream = cloudinary.uploader.upload_stream(
{
folder: 'profile_photos',
public_id: user._id?.toString(),
overwrite: true
},
async (error, result) => {
if (error || !result) {
return resolve(
res.status(500).json({
message: 'Cloudinary upload failed.',
error
})
);
}
user.profilePhoto = result.secure_url;
await user.save();
return resolve(
res.status(200).json({
message: 'Profile photo uploaded successfully.',
profilePhoto: result.secure_url,
})
);
}
);
uploadStream.end(req.file.buffer);
});
} catch (err: any) {
return res.status(500).json({
message: 'Server error',
error: err.message
});
}
};
// ==================== PASSWORD RESET REQUEST ====================
export const requestPasswordReset = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const { email } = req.body;
if (!email) return res.status(400).json({ message: 'Email is required.' });
const user: IUser | null = await User.findOne({ email });
if (!user) return res.status(404).json({ message: 'User not found.' });
const resetToken = crypto.randomBytes(PASSWORD_RESET_CONFIG.tokenLength).toString('hex');
const resetTokenExpiry = Date.now() + PASSWORD_RESET_CONFIG.tokenExpiry;
user.resetPasswordToken = resetToken;
user.resetPasswordExpires = resetTokenExpiry;
await user.save();
// const resetLink = `${req.protocol}://${req.get('host')}/api/users/reset-password/${resetToken}`;
const resetLink = `${FRONTEND_URL}/reset-password?token=${resetToken}`; //Make call to FE bc
await sendMail({
to: user.email,
subject: 'Password Reset Request',
text: `Reset your password using this link: ${resetLink}`,
html: `<p>You requested a password reset.</p><p><a href="${resetLink}">Click here to reset your password</a></p><p>If you did not request this, please ignore this email.</p>`,
});
return res.status(200).json({ message: 'Password reset link sent to your email.' });
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== RESET PASSWORD ====================
export const resetPassword = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const { token } = req.params;
const { password } = req.body;
if (!password) return res.status(400).json({ message: 'Password is required.' });
const user: IUser | null = await User.findOne({
resetPasswordToken: token,
resetPasswordExpires: { $gt: Date.now() },
});
if (!user) return res.status(400).json({ message: 'Invalid or expired token.' });
user.password = await bcrypt.hash(password, 10);
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
await user.save();
return res.status(200).json({ message: 'Password reset successful.' });
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== DELETE USER ====================
export const deleteUser = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const user: IUser | null = await User.findByIdAndDelete(req.userId);
if (!user) return res.status(404).json({ message: 'User not found.' });
return res.status(200).json({ message: 'User deleted successfully.' });
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== HELPER FUNCTIONS ====================
async function uploadToCloudinary(file: any, path: string): Promise<string> {
return new Promise((resolve, reject) => {
const uploadStream = cloudinary.uploader.upload_stream(
{ folder: path },
(error, result) => {
if (error || !result) reject(error);
else resolve(result.secure_url);
}
);
uploadStream.end(file.buffer);
});
}
async function sendNotificationToOrganization(orgId: string, trainer: IUser) {
const org = await User.findById(orgId);
if (org && org.email) {
await sendMail({
to: org.email,
subject: 'New Trainer Registration Request',
text: `A new trainer ${trainer.username} has requested to join your organization.`,
html: `<p>A new trainer <strong>${trainer.username}</strong> (${trainer.email}) has requested to join your organization.</p><p>Please review and verify their account.</p>`
});
}
}
async function notifyAdminsAboutNewOrganization(org: IUser) {
const admins = await User.find({ role: 'admin' });
for (const admin of admins) {
await sendMail({
to: admin.email,
subject: 'New Organization Registration',
text: `A new organization ${org.username} has registered.`,
html: `<p>A new organization <strong>${org.username}</strong> (${org.email}) of type <strong>${org.organizationType}</strong> has registered.</p><p>Please review and verify their account.</p>`
});
}
}
// ==================== 2FA SETUP ====================
export const setupTwoFactor = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const user: IUser | null = await User.findById(req.userId);
if (!user) return res.status(404).json({ message: 'User not found.' });
const secret = speakeasy.generateSecret({
name: `Prashikshak (${user.email})`
});
// Save secret to user but don't enable it yet
user.twoFactorSecret = secret.base32;
await user.save();
// Generate QR Code
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url!);
return res.status(200).json({
secret: secret.base32,
qrCode: qrCodeUrl
});
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== 2FA VERIFY ====================
export const verifyTwoFactor = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const { token } = req.body;
const user: IUser | null = await User.findById(req.userId);
if (!user) return res.status(404).json({ message: 'User not found.' });
if (!user.twoFactorSecret) return res.status(400).json({ message: '2FA setup not initiated.' });
const verified = speakeasy.totp.verify({
secret: user.twoFactorSecret,
encoding: 'base32',
token
});
if (verified) {
user.isTwoFactorEnabled = true;
await user.save();
return res.status(200).json({ message: '2FA enabled successfully.' });
} else {
return res.status(400).json({ message: 'Invalid OTP.' });
}
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET ALL USERS (ADMIN) ====================
export const getAllUsers = async (req: IUserRequest, res: Response): Promise<Response> => {
try {
const adminId = req.userId;
const admin = await User.findById(adminId);
if (!admin || admin.role !== 'admin') {
return res.status(403).json({ message: 'Only admins can view all users.' });
}
const { role, search, limit = 50, skip = 0, userId } = req.query;
const query: any = {};
if (userId) {
query._id = userId;
}
// Role filter
if (role) {
query.role = role;
}
// Search filter
if (search) {
const searchRegex = new RegExp(search as string, 'i');
query.$or = [
{ username: searchRegex },
{ email: searchRegex },
{ 'location.coordinates': { $exists: true } } // Just to keep structure valid
];
// Remove the dummy location query, just searching name/email
query.$or.pop();
}
const [users, total] = await Promise.all([
User.find(query)
.select('-password -twoFactorSecret -resetPasswordToken')
.sort({ createdAt: -1 })
.limit(Number(limit))
.skip(Number(skip))
.populate('organization', 'username email'),
User.countDocuments(query)
]);
return res.status(200).json({
count: users.length,
total,
users
});
} catch (err: any) {
return res.status(500).json({ message: 'Server error', error: err.message });
}
};