import { Request, Response, NextFunction } from 'express'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import { getUserAuthByEmail, getUserAuthByMobile, createUserWithProfile, getUserAuthById, getUserDetailsByUserId, updateUserProfile, } from '@/src/dbService'; import { ApiError } from '@/src/lib/api-error' import catchAsync from '@/src/lib/catch-async' import { jwtSecret } from '@/src/lib/env-exporter'; import uploadHandler from '@/src/lib/upload-handler' import { imageUploadS3, generateSignedUrlFromS3Url } from '@/src/lib/s3-client' interface RegisterRequest { name: string; email: string; mobile: string; password: string; profileImage?: string; } interface UpdateProfileRequest { name?: string; email?: string; mobile?: string; password?: string; bio?: string; dateOfBirth?: string; gender?: string; occupation?: string; } interface AuthResponse { token: string; user: { id: number; name: string | null; email: string | null; mobile: string | null; profileImage?: string | null; bio?: string | null; dateOfBirth?: string | null; gender?: string | null; occupation?: string | null; }; } const generateToken = (userId: number): string => { const secret = jwtSecret; if (!secret) { throw new ApiError('JWT secret not configured', 500); } return jwt.sign({ userId }, secret, { expiresIn: '7d' }); }; export const register = catchAsync(async (req: Request, res: Response, next: NextFunction) => { const { name, email, mobile, password }: RegisterRequest = req.body; // Handle profile image upload let profileImageUrl: string | undefined; if (req.file) { const key = `profile-images/${Date.now()}-${req.file.originalname}`; profileImageUrl = await imageUploadS3(req.file.buffer, req.file.mimetype, key); } if (!name || !email || !mobile || !password) { throw new ApiError('All fields are required', 400); } // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { throw new ApiError('Invalid email format', 400); } // Validate mobile format (Indian mobile numbers) const cleanMobile = mobile.replace(/\D/g, ''); if (cleanMobile.length !== 10 || !/^[6-9]/.test(cleanMobile)) { throw new ApiError('Invalid mobile number', 400); } /* // Old implementation - direct DB queries: import { db } from '@/src/db/db_index' import { users } from '@/src/db/schema' import { eq } from 'drizzle-orm'; const [existingEmail] = await db .select() .from(users) .where(eq(users.email, email.toLowerCase())) .limit(1); */ // Check if email already exists const existingEmail = await getUserAuthByEmail(email.toLowerCase()); if (existingEmail) { throw new ApiError('Email already registered', 409); } /* // Old implementation - direct DB queries: const [existingMobile] = await db .select() .from(users) .where(eq(users.mobile, cleanMobile)) .limit(1); */ // Check if mobile already exists const existingMobile = await getUserAuthByMobile(cleanMobile); if (existingMobile) { throw new ApiError('Mobile number already registered', 409); } // Hash password const hashedPassword = await bcrypt.hash(password, 12); /* // Old implementation - direct DB queries: import { userCreds, userDetails } from '@/src/db/schema' const newUser = await db.transaction(async (tx) => { const [user] = await tx .insert(users) .values({ name: name.trim(), email: email.toLowerCase().trim(), mobile: cleanMobile, }) .returning(); await tx.insert(userCreds).values({ userId: user.id, userPassword: hashedPassword, }); await tx.insert(userDetails).values({ userId: user.id, profileImage: profileImageUrl, }); return user; }); */ // Create user with profile in transaction const newUser = await createUserWithProfile({ name: name.trim(), email: email.toLowerCase().trim(), mobile: cleanMobile, hashedPassword, profileImage: profileImageUrl, }); const token = generateToken(newUser.id); // Generate signed URL for profile image if it was uploaded const profileImageSignedUrl = profileImageUrl ? await generateSignedUrlFromS3Url(profileImageUrl) : null; const response: AuthResponse = { token, user: { id: newUser.id, name: newUser.name, email: newUser.email, mobile: newUser.mobile, profileImage: profileImageSignedUrl, bio: null, dateOfBirth: null, gender: null, occupation: null, }, }; res.status(201).json({ success: true, data: response, }); }); export const updateProfile = catchAsync(async (req: Request, res: Response, next: NextFunction) => { const userId = req.user?.userId; if (!userId) { throw new ApiError('User not authenticated', 401); } const { name, email, mobile, password, bio, dateOfBirth, gender, occupation }: UpdateProfileRequest = req.body; // Handle profile image upload let profileImageUrl: string | undefined; if (req.file) { const key = `profile-images/${Date.now()}-${req.file.originalname}`; profileImageUrl = await imageUploadS3(req.file.buffer, req.file.mimetype, key); } // Validate email format if provided if (email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { throw new ApiError('Invalid email format', 400); } } // Validate mobile format if provided if (mobile) { const cleanMobile = mobile.replace(/\D/g, ''); if (cleanMobile.length !== 10 || !/^[6-9]/.test(cleanMobile)) { throw new ApiError('Invalid mobile number', 400); } } /* // Old implementation - direct DB queries: import { db } from '@/src/db/db_index' import { users, userCreds, userDetails } from '@/src/db/schema' import { eq } from 'drizzle-orm'; if (email) { const [existingEmail] = await db .select() .from(users) .where(eq(users.email, email.toLowerCase())) .limit(1); if (existingEmail && existingEmail.id !== userId) { throw new ApiError('Email already registered', 409); } } */ // Check if email already exists (if changing email) if (email) { const existingEmail = await getUserAuthByEmail(email.toLowerCase()); if (existingEmail && existingEmail.id !== userId) { throw new ApiError('Email already registered', 409); } } /* // Old implementation - direct DB queries: if (mobile) { const cleanMobile = mobile.replace(/\D/g, ''); const [existingMobile] = await db .select() .from(users) .where(eq(users.mobile, cleanMobile)) .limit(1); if (existingMobile && existingMobile.id !== userId) { throw new ApiError('Mobile number already registered', 409); } } */ // Check if mobile already exists (if changing mobile) if (mobile) { const cleanMobile = mobile.replace(/\D/g, ''); const existingMobile = await getUserAuthByMobile(cleanMobile); if (existingMobile && existingMobile.id !== userId) { throw new ApiError('Mobile number already registered', 409); } } // Hash password if provided let hashedPassword: string | undefined; if (password) { hashedPassword = await bcrypt.hash(password, 12); } /* // Old implementation - direct DB queries: const updatedUser = await db.transaction(async (tx) => { // Update user table const updateData: any = {}; if (name) updateData.name = name.trim(); if (email) updateData.email = email.toLowerCase().trim(); if (mobile) updateData.mobile = mobile.replace(/\D/g, ''); if (Object.keys(updateData).length > 0) { await tx.update(users).set(updateData).where(eq(users.id, userId)); } // Update password if provided if (password) { const hashedPassword = await bcrypt.hash(password, 12); await tx.update(userCreds).set({ userPassword: hashedPassword }).where(eq(userCreds.userId, userId)); } // Update or insert user details const userDetailsUpdate: any = {}; if (bio !== undefined) userDetailsUpdate.bio = bio; if (dateOfBirth !== undefined) userDetailsUpdate.dateOfBirth = dateOfBirth ? new Date(dateOfBirth) : null; if (gender !== undefined) userDetailsUpdate.gender = gender; if (occupation !== undefined) userDetailsUpdate.occupation = occupation; if (profileImageUrl) userDetailsUpdate.profileImage = profileImageUrl; userDetailsUpdate.updatedAt = new Date(); const [existingDetails] = await tx.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1); if (existingDetails) { await tx.update(userDetails).set(userDetailsUpdate).where(eq(userDetails.userId, userId)); } else { userDetailsUpdate.userId = userId; userDetailsUpdate.createdAt = new Date(); await tx.insert(userDetails).values(userDetailsUpdate); } const [user] = await tx.select().from(users).where(eq(users.id, userId)).limit(1); return user; }); */ // Update user profile in transaction const updatedUser = await updateUserProfile(userId, { name: name?.trim(), email: email?.toLowerCase().trim(), mobile: mobile?.replace(/\D/g, ''), hashedPassword, profileImage: profileImageUrl, bio, dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : undefined, gender, occupation, }); /* // Old implementation - direct DB queries: const [userDetail] = await db.select().from(userDetails).where(eq(userDetails.userId, userId)).limit(1); */ // Get updated user details for response const userDetail = await getUserDetailsByUserId(userId); // Generate signed URL for profile image if it exists const profileImageSignedUrl = userDetail?.profileImage ? await generateSignedUrlFromS3Url(userDetail.profileImage) : null; const response: AuthResponse = { token: req.headers.authorization?.replace('Bearer ', '') || '', // Keep existing token user: { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email, mobile: updatedUser.mobile, profileImage: profileImageSignedUrl, bio: userDetail?.bio || null, dateOfBirth: userDetail?.dateOfBirth || null, gender: userDetail?.gender || null, occupation: userDetail?.occupation || null, }, }; res.status(200).json({ success: true, data: response, }); }); /* // Old implementation - direct DB queries: import { db } from '@/src/db/db_index' import { users, userCreds, userDetails } from '@/src/db/schema' import { eq } from 'drizzle-orm'; */