- Created loginUser server action with credential validation - Implemented password verification using bcrypt (via NextAuth authorize) - Session creation on successful login handled by NextAuth - Invalid credentials handled with appropriate error messages - Added comprehensive unit tests for login functionality - All tests passing (11/11) Requirements: 2.1, 2.2
135 lines
4.1 KiB
TypeScript
135 lines
4.1 KiB
TypeScript
'use server'
|
|
|
|
import { prisma } from './db'
|
|
import bcrypt from 'bcryptjs'
|
|
import { z } from 'zod'
|
|
import { signIn } from '@/auth'
|
|
|
|
// 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
|
|
*/
|
|
export async function registerUser(
|
|
email: string,
|
|
password: string,
|
|
name?: string
|
|
): Promise<RegisterResult> {
|
|
try {
|
|
// 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
|
|
*/
|
|
export async function loginUser(
|
|
email: string,
|
|
password: string
|
|
): Promise<LoginResult> {
|
|
try {
|
|
// 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
|
|
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' }
|
|
}
|
|
}
|