Implement authentication functionality with login server action

- 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
This commit is contained in:
2025-12-02 00:36:03 +00:00
parent 70121982a0
commit 211ed50503
3 changed files with 148 additions and 2 deletions

View File

@@ -14,7 +14,8 @@ vi.mock('@/auth', () => ({
}))
// Import after mocking
import { registerUser } from '../auth-actions'
import { registerUser, loginUser } from '../auth-actions'
import { signIn } from '@/auth'
describe('registerUser', () => {
// Clean up test users after each test
@@ -125,3 +126,97 @@ describe('registerUser', () => {
expect(user?.name).toBeNull()
})
})
describe('loginUser', () => {
// Clean up test users after each test
afterEach(async () => {
await prisma.user.deleteMany({
where: {
email: {
contains: 'test-login-',
},
},
})
vi.clearAllMocks()
})
it('should successfully login with valid credentials', async () => {
const email = `test-login-${Date.now()}@example.com`
const password = 'password123'
// Create a test user first
await registerUser(email, password, 'Test User')
// Mock signIn to succeed
vi.mocked(signIn).mockResolvedValueOnce(undefined as any)
// Attempt login
const result = await loginUser(email, password)
expect(result.success).toBe(true)
expect(signIn).toHaveBeenCalledWith('credentials', {
email,
password,
redirect: false,
})
})
it('should reject login with invalid credentials', async () => {
const email = `test-login-${Date.now()}@example.com`
const password = 'password123'
// Create a test user first
await registerUser(email, password, 'Test User')
// Mock signIn to throw error (invalid credentials)
vi.mocked(signIn).mockRejectedValueOnce(new Error('Invalid credentials'))
// Attempt login with wrong password
const result = await loginUser(email, 'wrongpassword')
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error).toContain('Invalid email or password')
}
})
it('should reject login with non-existent email', async () => {
const email = `test-login-nonexistent-${Date.now()}@example.com`
const password = 'password123'
// Mock signIn to throw error (user not found)
vi.mocked(signIn).mockRejectedValueOnce(new Error('User not found'))
// Attempt login with non-existent email
const result = await loginUser(email, password)
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error).toContain('Invalid email or password')
}
})
it('should reject login with invalid email format', async () => {
const email = 'invalid-email'
const password = 'password123'
const result = await loginUser(email, password)
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error).toContain('Invalid email')
}
})
it('should reject login with empty password', async () => {
const email = `test-login-${Date.now()}@example.com`
const password = ''
const result = await loginUser(email, password)
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error).toContain('Password is required')
}
})
})

View File

@@ -12,10 +12,19 @@ const registerSchema = z.object({
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
@@ -81,3 +90,45 @@ export async function registerUser(
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' }
}
}