From 211ed5050396561e69622c05f06af0bb92664472 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Tue, 2 Dec 2025 00:36:03 +0000 Subject: [PATCH] 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 --- .kiro/specs/user-auth-and-sharing/tasks.md | 2 +- lib/__tests__/auth-actions.test.ts | 97 +++++++++++++++++++++- lib/auth-actions.ts | 51 ++++++++++++ 3 files changed, 148 insertions(+), 2 deletions(-) diff --git a/.kiro/specs/user-auth-and-sharing/tasks.md b/.kiro/specs/user-auth-and-sharing/tasks.md index 421be96..f318f5f 100644 --- a/.kiro/specs/user-auth-and-sharing/tasks.md +++ b/.kiro/specs/user-auth-and-sharing/tasks.md @@ -41,7 +41,7 @@ - **Property 5: Password length validation** - **Validates: Requirements 1.5** -- [ ] 4. Implement authentication functionality +- [x] 4. Implement authentication functionality - Create login server action with credential validation - Implement password verification with bcrypt - Create session on successful login diff --git a/lib/__tests__/auth-actions.test.ts b/lib/__tests__/auth-actions.test.ts index 13027af..f328f33 100644 --- a/lib/__tests__/auth-actions.test.ts +++ b/lib/__tests__/auth-actions.test.ts @@ -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') + } + }) +}) diff --git a/lib/auth-actions.ts b/lib/auth-actions.ts index d98b2b2..af7f78c 100644 --- a/lib/auth-actions.ts +++ b/lib/auth-actions.ts @@ -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 { + 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' } + } +}