| 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; |
| }; |
| } |
|
|
| |
| export const authWithGoogle = async (req: Request, res: Response) => { |
| const code = req.query.code; |
| const role = req.query.state as string; |
|
|
| 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; |
|
|
| |
| 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 }); |
|
|
| |
| 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(); |
| } |
|
|
| |
| 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 |
| ); |
|
|
| |
| const redis = await getRedisClient(); |
| const sessionKey = `session:${user._id}:${token.substring(0, 10)}`; |
| await redis.set(sessionKey, 'valid', { EX: 7 * 24 * 60 * 60 }); |
|
|
| 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." |
| }); |
| } |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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.' }); |
| } |
|
|
| |
| 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(); |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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(); |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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; |
|
|
| |
| if (user.role === 'trainer' && req.files.govtIdCard) { |
| govtIdUrl = await uploadToCloudinary( |
| req.files.govtIdCard[0], |
| `trainer_docs/${user._id}/govt_id` |
| ); |
| user.govtIdCard = govtIdUrl; |
| } |
|
|
| |
| if (user.role === 'organization') { |
| |
| if (req.files.registrationCertificate) { |
| const regCert = await uploadToCloudinary( |
| req.files.registrationCertificate[0], |
| `org_docs/${user._id}/registration` |
| ); |
| uploadedDocs.registrationCertificate = regCert; |
| } |
|
|
| |
| if (req.files.gstCertificate) { |
| const gstCert = await uploadToCloudinary( |
| req.files.gstCertificate[0], |
| `org_docs/${user._id}/gst` |
| ); |
| uploadedDocs.gstCertificate = gstCert; |
| } |
|
|
| |
| if (req.files.authorizationLetter) { |
| const authLetter = await uploadToCloudinary( |
| req.files.authorizationLetter[0], |
| `org_docs/${user._id}/authorization` |
| ); |
| uploadedDocs.authorizationLetter = authLetter; |
| } |
|
|
| |
| 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 }; |
| } |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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." }); |
| } |
|
|
| |
| 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 { |
| |
| |
| |
| } |
|
|
| |
| 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 |
| ); |
|
|
| |
| const redis = await getRedisClient(); |
| const sessionKey = `session:${user._id}:${token.substring(0, 10)}`; |
| await redis.set(sessionKey, 'valid', { EX: 7 * 24 * 60 * 60 }); |
|
|
| 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 }); |
| } |
| }; |
|
|
| |
| export const verifyTrainer = async (req: IUserRequest, res: Response): Promise<Response> => { |
| try { |
| const organizationId = req.userId; |
| const { trainerId, status, rejectionReason } = req.body; |
|
|
| 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; |
| } |
| await trainer.save(); |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| export const verifyOrganization = async (req: IUserRequest, res: Response): Promise<Response> => { |
| try { |
| const adminId = req.userId; |
| const { organizationId, status, rejectionReason } = req.body; |
|
|
| 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(); |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| export const updateUser = async (req: IUserRequest, res: Response): Promise<Response> => { |
| try { |
| const updates = req.body; |
| const userId = req.userId; |
|
|
| |
| if (updates.password) delete updates.password; |
| |
| if (updates.role) delete updates.role; |
| |
| 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." }); |
| } |
|
|
| |
| if (currentUser.usedOAuth && updates.email) { |
| return res.status(403).json({ |
| message: "Cannot edit email for Google OAuth accounts.", |
| }); |
| } |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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 |
| }); |
| } |
| }; |
|
|
| |
| 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 = `${FRONTEND_URL}/reset-password?token=${resetToken}`; |
|
|
| 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 }); |
| } |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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>` |
| }); |
| } |
| } |
| |
| 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})` |
| }); |
|
|
| |
| user.twoFactorSecret = secret.base32; |
| await user.save(); |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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; |
| } |
|
|
| |
| if (role) { |
| query.role = role; |
| } |
|
|
| |
| if (search) { |
| const searchRegex = new RegExp(search as string, 'i'); |
| query.$or = [ |
| { username: searchRegex }, |
| { email: searchRegex }, |
| { 'location.coordinates': { $exists: true } } |
| ]; |
| |
| 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 }); |
| } |
| }; |
|
|