freshyo/apps/backend/src/uv-apis/auth.controller.ts
2026-03-26 12:07:49 +05:30

374 lines
10 KiB
TypeScript

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';
*/