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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 = `

Your trainer account has been ${status} by ${organization.username}.

`; if (status === 'rejected') { emailText += ` Reason: ${rejectionReason}`; emailHtml += `

Reason: ${rejectionReason}

`; } 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 => { 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 = `

Your organization has been ${status} by the admin.

`; if (status === 'rejected') { emailText += ` Reason: ${rejectionReason}`; emailHtml += `

Reason: ${rejectionReason}

`; } 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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: `

You requested a password reset.

Click here to reset your password

If you did not request this, please ignore this email.

`, }); 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 => { 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 => { 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 { 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: `

A new trainer ${trainer.username} (${trainer.email}) has requested to join your organization.

Please review and verify their account.

` }); } } 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: `

A new organization ${org.username} (${org.email}) of type ${org.organizationType} has registered.

Please review and verify their account.

` }); } } // ==================== 2FA SETUP ==================== export const setupTwoFactor = async (req: IUserRequest, res: Response): Promise => { 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 => { 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 => { 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 }); } };