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:
@@ -41,7 +41,7 @@
|
|||||||
- **Property 5: Password length validation**
|
- **Property 5: Password length validation**
|
||||||
- **Validates: Requirements 1.5**
|
- **Validates: Requirements 1.5**
|
||||||
|
|
||||||
- [ ] 4. Implement authentication functionality
|
- [x] 4. Implement authentication functionality
|
||||||
- Create login server action with credential validation
|
- Create login server action with credential validation
|
||||||
- Implement password verification with bcrypt
|
- Implement password verification with bcrypt
|
||||||
- Create session on successful login
|
- Create session on successful login
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ vi.mock('@/auth', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// Import after mocking
|
// Import after mocking
|
||||||
import { registerUser } from '../auth-actions'
|
import { registerUser, loginUser } from '../auth-actions'
|
||||||
|
import { signIn } from '@/auth'
|
||||||
|
|
||||||
describe('registerUser', () => {
|
describe('registerUser', () => {
|
||||||
// Clean up test users after each test
|
// Clean up test users after each test
|
||||||
@@ -125,3 +126,97 @@ describe('registerUser', () => {
|
|||||||
expect(user?.name).toBeNull()
|
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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -12,10 +12,19 @@ const registerSchema = z.object({
|
|||||||
name: z.string().optional(),
|
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 =
|
export type RegisterResult =
|
||||||
| { success: true; userId: string }
|
| { success: true; userId: string }
|
||||||
| { success: false; error: string }
|
| { success: false; error: string }
|
||||||
|
|
||||||
|
export type LoginResult =
|
||||||
|
| { success: true }
|
||||||
|
| { success: false; error: string }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new user with email and password
|
* Register a new user with email and password
|
||||||
* Requirements: 1.1, 1.2, 1.3, 1.4, 1.5
|
* 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.' }
|
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' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user