import { Request, Response } from "express"; import prisma from "../../prisma/client"; import * as argon2 from "argon2"; import { sendEmail } from "../../middlewares/email"; import { generateOTP } from "../../middlewares/generateOTP"; import jwt from "jsonwebtoken"; import dotenv from "dotenv"; import redis from "../../utils/redis"; import { Difficulty, Role } from "@prisma/client"; dotenv.config(); const JWT_SECRET: string = process.env.JWT_SECRET || "default_secret"; export const signUp = async (req: Request, res: Response) => { const { firstName, lastName, skillLevel, email, password, role } = req.body; try { const existingUser = await prisma.user.findUnique({ where: { email } }); if (existingUser) { return res .status(400) .json({ success: false, message: "Email already registered" }); } const hashedPassword = await argon2.hash(password); const user = await prisma.user.create({ data: { firstName, lastName, email, skillLevel: (skillLevel as Difficulty) || "BEGINNER", password: hashedPassword, role: (role as Role) || "STUDENT", }, }); res.json({ success: true, message: "User registered successfully", data: { id: user.id }, }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const login = async (req: Request, res: Response) => { const { email, password } = req.body; try { const user = await prisma.user.findUnique({ where: { email } }); if (!user || !user.password) { return res .status(401) .json({ success: false, message: "Invalid Email or Password" }); } const verifyPassword = await argon2.verify(user.password, password); if (!verifyPassword) { return res .status(401) .json({ success: false, message: "Invalid Email or Password" }); } const token = jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, { expiresIn: "7d", }); res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days }); res.json({ success: true, message: "Login successful", data: { id: user.id, firstName: user.firstName, lastName: user.lastName, email: user.email, role: user.role, token, }, }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const logout = async (req: Request, res: Response) => { res.clearCookie("token"); res.json({ success: true, message: "Logged out successfully" }); }; export const me = async (req: Request, res: Response) => { const userId = (req as any).user?.id; if (!userId) { return res.status(401).json({ success: false, message: "Unauthorized" }); } try { const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true, firstName: true, lastName: true, skillLevel: true, bio: true, portfolioLinks: true, xp: true, streak: true, role: true, avatarUrl: true, createdAt: true, projects: { include: { project: true, }, }, achievements: { include: { achievement: true, }, }, }, }); if (!user) { return res .status(404) .json({ success: false, message: "User not found" }); } res.json({ success: true, data: user }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const forgetPassword = async (req: Request, res: Response) => { const { email } = req.body; const appName = "Dev Drill"; try { const user = await prisma.user.findUnique({ where: { email } }); if (!user) { return res .status(404) .json({ success: false, message: "Account is not registered" }); } const { otp, otpDate } = generateOTP(); // Store OTP in Redis with 1 hour expiry await redis.setex(`otp:${email}`, 3600, otp.toString()); const htmlMessage = `

${appName}

Hello ${user.lastName},

We received a request to reset your password for your ${appName} account.

${otp}

This verification code will expire in 1 hour.
If you didn't request this password reset, please ignore this email.

© ${new Date().getFullYear()} ${appName}. All rights reserved.

`; const textMessage = `${appName} Password Reset\n\n` + `Hello ${user.lastName},\n\n` + `Use this OTP to reset your password: ${otp}\n` + `This code expires in 1 hour.`; sendEmail( user.email, `Password Reset Request - ${appName}`, htmlMessage, textMessage ); res.json({ success: true, message: `An OTP has been sent to your mail. Check your mail for your verification code`, }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const verifyOTP = async (req: Request, res: Response) => { const { email, otp: userOtp } = req.body; try { const storedOtp = await redis.get(`otp:${email}`); if (!storedOtp) { return res .status(400) .json({ success: false, message: "OTP expired or not found" }); } if (storedOtp !== userOtp.toString()) { return res .status(400) .json({ success: false, message: "Invalid Verification code!" }); } // OTP verified, can remove it now await redis.del(`otp:${email}`); // Optionally set a flag in Redis that OTP was verified for this email to allow password reset await redis.setex(`otp_verified:${email}`, 600, "true"); res.json({ success: true, message: "Verification successful" }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const resetPassword = async (req: Request, res: Response) => { const { email, newPassword } = req.body; try { const isVerified = await redis.get(`otp_verified:${email}`); if (!isVerified) { return res .status(400) .json({ success: false, message: "OTP not verified" }); } const user = await prisma.user.findUnique({ where: { email } }); if (!user) return res .status(404) .json({ success: false, message: "User not found" }); if (user.password) { const verifyPassword = await argon2.verify(user.password, newPassword); if (verifyPassword) { return res .status(400) .json({ success: false, message: "You entered your old password" }); } } const newPass = await argon2.hash(newPassword); await prisma.user.update({ where: { email }, data: { password: newPass }, }); await redis.del(`otp_verified:${email}`); res.json({ success: true, message: "Password successfully reset" }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const getUser = async (req: Request, res: Response) => { const { id } = req.params; try { const user = await prisma.user.findUnique({ where: { id }, select: { id: true, email: true, firstName: true, lastName: true, skillLevel: true, bio: true, portfolioLinks: true, xp: true, streak: true, role: true, avatarUrl: true, createdAt: true, projects: { include: { project: true, }, }, achievements: { include: { achievement: true, }, }, }, }); if (!user) return res .status(404) .json({ success: false, message: "User not found" }); res.json({ success: true, data: user }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const updateUser = async (req: Request, res: Response) => { const { id } = req.params; const authenticatedUserId = (req as any).user?.id; if (authenticatedUserId !== id) { return res.status(403).json({ success: false, message: "Forbidden" }); } let { firstName, lastName, email, password, bio, skillLevel, portfolioLinks, avatarUrl, } = req.body; try { const user = await prisma.user.findUnique({ where: { id } }); if (!user) { return res .status(404) .json({ success: false, message: "User not found" }); } const data: any = { firstName, lastName, bio, skillLevel, portfolioLinks, avatarUrl, }; if (email && email !== user.email) { const existingUser = await prisma.user.findUnique({ where: { email } }); if (existingUser) { return res .status(400) .json({ success: false, message: "Email already in use" }); } data.email = email; } if (password) { data.password = await argon2.hash(password); } const updatedUser = await prisma.user.update({ where: { id }, data, select: { id: true, email: true, firstName: true, lastName: true, skillLevel: true, bio: true, portfolioLinks: true, xp: true, streak: true, role: true, avatarUrl: true, createdAt: true, projects: { include: { project: true, }, }, achievements: { include: { achievement: true, }, }, }, }); res.json({ success: true, data: updatedUser, }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const googleAuthCallback = async (req: Request, res: Response) => { try { const user = req.user as any; if (!user) { return res.redirect( `${process.env.CLIENT_URL}/auth/signin?error=auth_failed` ); } const token = jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, { expiresIn: "7d", }); res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", maxAge: 7 * 24 * 60 * 60 * 1000, }); res.redirect(`${process.env.CLIENT_URL}/dashboard`); } catch (error: any) { res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=server_error`); } }; export const githubAuthCallback = async (req: Request, res: Response) => { try { const user = req.user as any; if (!user) { return res.redirect( `${process.env.CLIENT_URL}/auth/signin?error=auth_failed` ); } const token = jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, { expiresIn: "7d", }); res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", maxAge: 7 * 24 * 60 * 60 * 1000, }); res.redirect(`${process.env.CLIENT_URL}/dashboard`); } catch (error: any) { res.redirect(`${process.env.CLIENT_URL}/auth/signin?error=server_error`); } };