const { prisma } = require('../utils/db'); const { hashPassword, comparePassword, generateToken, generateVerificationToken, generateVerificationCode, getTokenExpiry } = require('../utils/auth'); const { sendVerificationEmail, sendWelcomeEmail, sendPasswordResetEmail } = require('../utils/email'); // Register new user (Phase 1.5 - with WSDC support and email verification) async function register(req, res, next) { try { const { username, email, password, firstName, lastName, wsdcId } = req.body; // Check if user already exists const existingUser = await prisma.user.findFirst({ where: { OR: [ { email }, { username }, ...(wsdcId ? [{ wsdcId }] : []), ], }, }); if (existingUser) { if (existingUser.email === email) { return res.status(400).json({ success: false, error: 'Email already registered', }); } if (existingUser.username === username) { return res.status(400).json({ success: false, error: 'Username already taken', }); } if (wsdcId && existingUser.wsdcId === wsdcId) { return res.status(400).json({ success: false, error: 'WSDC ID already registered', }); } } // Hash password const passwordHash = await hashPassword(password); // Generate verification token and code const verificationToken = generateVerificationToken(); const verificationCode = generateVerificationCode(); const verificationTokenExpiry = getTokenExpiry(24); // 24 hours // Create display name for avatar const displayName = firstName && lastName ? `${firstName} ${lastName}` : username; // Create user const user = await prisma.user.create({ data: { username, email, passwordHash, firstName, lastName, wsdcId, verificationToken, verificationCode, verificationTokenExpiry, emailVerified: false, avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent(displayName)}&background=6366f1&color=fff`, }, select: { id: true, username: true, email: true, firstName: true, lastName: true, wsdcId: true, emailVerified: true, avatar: true, createdAt: true, }, }); // Send verification email try { await sendVerificationEmail( user.email, user.firstName || user.username, verificationToken, verificationCode ); } catch (emailError) { console.error('Failed to send verification email:', emailError); // Continue even if email fails - user can request resend } // Generate JWT token const token = generateToken({ userId: user.id }); res.status(201).json({ success: true, message: 'User registered successfully. Please check your email to verify your account.', data: { user, token, }, }); } catch (error) { next(error); } } // Login user async function login(req, res, next) { try { const { email, password } = req.body; // Find user by email const user = await prisma.user.findUnique({ where: { email }, }); if (!user) { return res.status(401).json({ success: false, error: 'Invalid credentials', }); } // Compare password const isPasswordValid = await comparePassword(password, user.passwordHash); if (!isPasswordValid) { return res.status(401).json({ success: false, error: 'Invalid credentials', }); } // Generate token const token = generateToken({ userId: user.id }); // Return user without password const { passwordHash, ...userWithoutPassword } = user; res.json({ success: true, message: 'Login successful', data: { user: userWithoutPassword, token, }, }); } catch (error) { next(error); } } // Verify email by token (link in email) async function verifyEmailByToken(req, res, next) { try { const { token } = req.query; if (!token) { return res.status(400).json({ success: false, error: 'Verification token is required', }); } // Find user by verification token const user = await prisma.user.findUnique({ where: { verificationToken: token }, }); if (!user) { return res.status(404).json({ success: false, error: 'Invalid or expired verification token', }); } // Check if already verified if (user.emailVerified) { return res.status(200).json({ success: true, message: 'Email already verified', }); } // Check if token expired if (user.verificationTokenExpiry && new Date() > user.verificationTokenExpiry) { return res.status(400).json({ success: false, error: 'Verification token has expired. Please request a new one.', }); } // Update user - mark as verified and clear tokens await prisma.user.update({ where: { id: user.id }, data: { emailVerified: true, verificationToken: null, verificationCode: null, verificationTokenExpiry: null, }, }); // Send welcome email try { await sendWelcomeEmail(user.email, user.firstName || user.username); } catch (emailError) { console.error('Failed to send welcome email:', emailError); } res.status(200).json({ success: true, message: 'Email verified successfully!', }); } catch (error) { next(error); } } // Verify email by code (6-digit PIN) async function verifyEmailByCode(req, res, next) { try { const { code, email } = req.body; if (!code || !email) { return res.status(400).json({ success: false, error: 'Email and verification code are required', }); } // Find user by email and code const user = await prisma.user.findFirst({ where: { email, verificationCode: code, }, }); if (!user) { return res.status(400).json({ success: false, error: 'Invalid verification code', }); } // Check if already verified if (user.emailVerified) { return res.status(200).json({ success: true, message: 'Email already verified', }); } // Check if token expired if (user.verificationTokenExpiry && new Date() > user.verificationTokenExpiry) { return res.status(400).json({ success: false, error: 'Verification code has expired. Please request a new one.', }); } // Update user - mark as verified and clear tokens await prisma.user.update({ where: { id: user.id }, data: { emailVerified: true, verificationToken: null, verificationCode: null, verificationTokenExpiry: null, }, }); // Send welcome email try { await sendWelcomeEmail(user.email, user.firstName || user.username); } catch (emailError) { console.error('Failed to send welcome email:', emailError); } res.status(200).json({ success: true, message: 'Email verified successfully!', }); } catch (error) { next(error); } } // Resend verification email async function resendVerification(req, res, next) { try { const { email } = req.body; if (!email) { return res.status(400).json({ success: false, error: 'Email is required', }); } // Find user by email const user = await prisma.user.findUnique({ where: { email }, }); if (!user) { return res.status(404).json({ success: false, error: 'User not found', }); } // Check if already verified if (user.emailVerified) { return res.status(400).json({ success: false, error: 'Email is already verified', }); } // Generate new verification token and code const verificationToken = generateVerificationToken(); const verificationCode = generateVerificationCode(); const verificationTokenExpiry = getTokenExpiry(24); // 24 hours // Update user with new tokens await prisma.user.update({ where: { id: user.id }, data: { verificationToken, verificationCode, verificationTokenExpiry, }, }); // Send verification email await sendVerificationEmail( user.email, user.firstName || user.username, verificationToken, verificationCode ); res.status(200).json({ success: true, message: 'Verification email sent successfully', }); } catch (error) { next(error); } } // Request password reset (send email with reset link) async function requestPasswordReset(req, res, next) { try { const { email } = req.body; if (!email) { return res.status(400).json({ success: false, error: 'Email is required', }); } // Find user by email const user = await prisma.user.findUnique({ where: { email }, }); // Always return success to prevent email enumeration if (!user) { return res.status(200).json({ success: true, message: 'If an account with that email exists, a password reset link has been sent.', }); } // Generate reset token const resetToken = generateVerificationToken(); const resetTokenExpiry = getTokenExpiry(1); // 1 hour expiry // Save reset token to database await prisma.user.update({ where: { id: user.id }, data: { resetToken, resetTokenExpiry, }, }); // Send password reset email try { await sendPasswordResetEmail( user.email, user.firstName || user.username, resetToken ); } catch (emailError) { console.error('Failed to send password reset email:', emailError); return res.status(500).json({ success: false, error: 'Failed to send password reset email', }); } res.status(200).json({ success: true, message: 'If an account with that email exists, a password reset link has been sent.', }); } catch (error) { next(error); } } // Reset password using token async function resetPassword(req, res, next) { try { const { token, newPassword } = req.body; if (!token || !newPassword) { return res.status(400).json({ success: false, error: 'Token and new password are required', }); } // Validate password length if (newPassword.length < 8) { return res.status(400).json({ success: false, error: 'Password must be at least 8 characters long', }); } // Find user by reset token const user = await prisma.user.findUnique({ where: { resetToken: token }, }); if (!user) { return res.status(400).json({ success: false, error: 'Invalid or expired reset token', }); } // Check if token expired if (user.resetTokenExpiry && new Date() > user.resetTokenExpiry) { return res.status(400).json({ success: false, error: 'Reset token has expired. Please request a new one.', }); } // Hash new password const passwordHash = await hashPassword(newPassword); // Update user password and clear reset token await prisma.user.update({ where: { id: user.id }, data: { passwordHash, resetToken: null, resetTokenExpiry: null, }, }); res.status(200).json({ success: true, message: 'Password reset successfully', }); } catch (error) { next(error); } } module.exports = { register, login, verifyEmailByToken, verifyEmailByCode, resendVerification, requestPasswordReset, resetPassword, };