'use server' import { prisma } from './db' import bcrypt from 'bcryptjs' import { z } from 'zod' import { signIn, signOut } from '@/auth' import { checkRateLimit, resetRateLimit, LOGIN_RATE_LIMIT, REGISTER_RATE_LIMIT } from './rate-limit' import { headers } from 'next/headers' import { getClientIdentifier } from './rate-limit' // Validation schemas const registerSchema = z.object({ email: z.string().email({ message: 'Invalid email address' }), password: z.string().min(8, { message: 'Password must be at least 8 characters' }), name: z.string().optional(), }) const loginSchema = z.object({ email: z.string().email({ message: 'Invalid email address' }), password: z.string().min(1, { message: 'Password is required' }), }) export type RegisterResult = | { success: true; userId: string } | { success: false; error: string } export type LoginResult = | { success: true } | { success: false; error: string } /** * Register a new user with email and password * Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 9.3 (rate limiting) */ export async function registerUser( email: string, password: string, name?: string ): Promise { try { // Rate limiting - Requirement 9.3 // Only apply rate limiting when headers are available (not in test environment) try { const headersList = await headers() const clientId = getClientIdentifier(headersList) const rateLimitKey = `register:${clientId}` const rateLimit = checkRateLimit(rateLimitKey, REGISTER_RATE_LIMIT) if (!rateLimit.allowed) { const minutesUntilReset = Math.ceil((rateLimit.retryAfter || 0) / 60000) return { success: false, error: `Too many registration attempts. Please try again in ${minutesUntilReset} minutes.` } } } catch (headerError) { // Headers not available (e.g., in test environment) - skip rate limiting console.log('Rate limiting skipped: headers not available') } // Validate input (Requirement 1.5 - password length validation) const validation = registerSchema.safeParse({ email, password, name }) if (!validation.success) { const errors = validation.error.issues.map((issue) => issue.message).join(', ') return { success: false, error: errors } } const { email: validEmail, password: validPassword, name: validName } = validation.data // Check if email already exists (Requirement 1.2 - duplicate email rejection) const existingUser = await prisma.user.findUnique({ where: { email: validEmail }, }) if (existingUser) { return { success: false, error: 'Email already registered' } } // Hash password (Requirement 1.3 - password hashing) const passwordHash = await bcrypt.hash(validPassword, 10) // Create user (Requirement 1.1 - create user with unique identifier) const user = await prisma.user.create({ data: { email: validEmail, passwordHash, name: validName || null, }, }) // Create session after successful registration (Requirement 1.4) try { await signIn('credentials', { email: validEmail, password: validPassword, redirect: false, }) } catch (error) { // If session creation fails, still return success since user was created console.error('Session creation failed after registration:', error) } return { success: true, userId: user.id } } catch (error) { console.error('Registration error:', error) // Handle unique constraint violation (shouldn't happen due to check above, but just in case) if (error instanceof Error && error.message.includes('Unique constraint')) { return { success: false, error: 'Email already registered' } } return { success: false, error: 'Registration failed. Please try again.' } } } /** * Login user with email and password * Requirements: 2.1, 2.2, 9.3 (rate limiting) */ export async function loginUser( email: string, password: string ): Promise { try { // Rate limiting - Requirement 9.3 // Use email as identifier to prevent brute force attacks on specific accounts const rateLimitKey = `login:${email.toLowerCase()}` const rateLimit = checkRateLimit(rateLimitKey, LOGIN_RATE_LIMIT) if (!rateLimit.allowed) { const minutesUntilReset = Math.ceil((rateLimit.retryAfter || 0) / 60000) return { success: false, error: `Too many login attempts. Please try again in ${minutesUntilReset} minutes.` } } // Validate input (Requirement 2.1 - credential validation) const validation = loginSchema.safeParse({ email, password }) if (!validation.success) { const errors = validation.error.issues.map((issue) => issue.message).join(', ') return { success: false, error: errors } } const { email: validEmail, password: validPassword } = validation.data // Attempt to sign in (Requirement 2.1 - authenticate and create session) // The authorize function in auth.ts handles: // - Querying user from database // - Password verification with bcrypt // - Session creation on success await signIn('credentials', { email: validEmail, password: validPassword, redirect: false, }) // If signIn doesn't throw an error, authentication was successful // Reset rate limit on successful login resetRateLimit(rateLimitKey) return { success: true } } catch (error) { // Handle invalid credentials (Requirement 2.2) // NextAuth throws an error for invalid credentials console.error('Login error:', error) // Return generic error message to avoid revealing whether email exists return { success: false, error: 'Invalid email or password' } } } /** * Logout user and terminate session * Requirement: 2.5 - Logout terminates session and clears authentication tokens */ export async function logoutUser(): Promise { try { // Sign out using NextAuth - this will: // 1. Clear the session token // 2. Remove authentication cookies // 3. Terminate the session await signOut({ redirect: false }) } catch (error) { console.error('Logout error:', error) throw new Error('Logout failed') } } /** * Get current session * Requirements: 3.2 - Session validation and persistence */ export async function getCurrentSession() { try { const { auth } = await import('@/auth') const session = await auth() return session } catch (error) { console.error('Session retrieval error:', error) return null } } /** * Validate if user is authenticated * Requirements: 2.4, 3.3 - Session expiration and validation */ export async function isAuthenticated(): Promise { const session = await getCurrentSession() return !!session?.user } // Profile management schemas const updateProfileSchema = z.object({ name: z.string().min(1, { message: 'Name is required' }).max(100, { message: 'Name must be less than 100 characters' }).optional(), email: z.string().email({ message: 'Invalid email address' }).optional(), }) const updatePasswordSchema = z.object({ currentPassword: z.string().min(1, { message: 'Current password is required' }), newPassword: z.string().min(8, { message: 'New password must be at least 8 characters' }), }) export type UpdateProfileResult = | { success: true } | { success: false; error: string } export type UpdatePasswordResult = | { success: true } | { success: false; error: string } /** * Update user profile information * Requirements: 7.1, 7.4, 7.5 */ export async function updateProfile( userId: string, data: { name?: string; email?: string } ): Promise { try { // Validate input const validation = updateProfileSchema.safeParse(data) if (!validation.success) { const errors = validation.error.issues.map((issue) => issue.message).join(', ') return { success: false, error: errors } } const { name, email } = validation.data // Check if there's anything to update if (!name && !email) { return { success: false, error: 'No data to update' } } // If email is being updated, check uniqueness (Requirement 7.4) if (email) { const existingUser = await prisma.user.findUnique({ where: { email }, }) // Check if email is already in use by another user if (existingUser && existingUser.id !== userId) { return { success: false, error: 'Email already in use' } } } // Update user profile (Requirement 7.1) await prisma.user.update({ where: { id: userId }, data: { ...(name !== undefined && { name }), ...(email !== undefined && { email }), }, }) return { success: true } } catch (error) { console.error('Profile update error:', error) // Handle unique constraint violation if (error instanceof Error && error.message.includes('Unique constraint')) { return { success: false, error: 'Email already in use' } } return { success: false, error: 'Profile update failed. Please try again.' } } } /** * Update user password * Requirements: 7.2, 7.3 */ export async function updatePassword( userId: string, currentPassword: string, newPassword: string ): Promise { try { // Validate input const validation = updatePasswordSchema.safeParse({ currentPassword, newPassword }) if (!validation.success) { const errors = validation.error.issues.map((issue) => issue.message).join(', ') return { success: false, error: errors } } // Get user from database const user = await prisma.user.findUnique({ where: { id: userId }, }) if (!user || !user.passwordHash) { return { success: false, error: 'User not found' } } // Verify current password (Requirement 7.2) const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.passwordHash) if (!isCurrentPasswordValid) { return { success: false, error: 'Current password is incorrect' } } // Hash new password (Requirement 7.3) const newPasswordHash = await bcrypt.hash(newPassword, 10) // Update password await prisma.user.update({ where: { id: userId }, data: { passwordHash: newPasswordHash, }, }) return { success: true } } catch (error) { console.error('Password update error:', error) return { success: false, error: 'Password update failed. Please try again.' } } } /** * Get user profile (without sensitive data) * Requirement: 7.5 - Profile data exposure protection */ export async function getUserProfile(userId: string) { try { const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true, name: true, createdAt: true, updatedAt: true, // Explicitly exclude passwordHash (Requirement 7.5) }, }) return user } catch (error) { console.error('Get user profile error:', error) return null } }