feat: add JWT authentication with complete test coverage

Phase 1 - Step 3: Authentication API

**Backend Authentication:**
- bcryptjs for password hashing (salt rounds: 10)
- JWT tokens with 24h expiration
- Secure password storage (never expose passwordHash)

**API Endpoints:**
- POST /api/auth/register - User registration
  - Username validation (3-50 chars, alphanumeric + underscore)
  - Email validation and normalization
  - Password validation (min 6 chars)
  - Duplicate email/username detection
  - Auto-generated avatar (ui-avatars.com)

- POST /api/auth/login - User authentication
  - Email + password credentials
  - Returns JWT token + user data
  - Invalid credentials protection

- GET /api/users/me - Get current user (protected)
  - Requires valid JWT token
  - Returns user data + stats (matches, ratings)
  - Token validation via middleware

**Security Features:**
- express-validator for input sanitization
- Auth middleware for protected routes
- Token verification (Bearer token)
- Password never returned in responses
- Proper error messages (no information leakage)

**Frontend Integration:**
- API service layer (frontend/src/services/api.js)
- Updated AuthContext to use real API
- Token storage in localStorage
- Automatic token inclusion in requests
- Error handling for expired/invalid tokens

**Unit Tests (30 tests, 78.26% coverage):**

Auth Endpoints (14 tests):
-  Register: success, duplicate email, duplicate username
-  Register validation: invalid email, short password, short username
-  Login: success, wrong password, non-existent user, invalid format
-  Protected route: valid token, no token, invalid token, malformed header

Auth Utils (9 tests):
-  Password hashing and comparison
-  Different hashes for same password
-  JWT generation and verification
-  Token expiration validation
-  Invalid token handling

All tests passing 
Coverage: 78.26% 
This commit is contained in:
Radosław Gierwiało
2025-11-12 22:16:14 +01:00
parent 0e62b12f5e
commit 3788274f73
14 changed files with 997 additions and 43 deletions

View File

@@ -1,5 +1,5 @@
import { createContext, useContext, useState, useEffect } from 'react';
import { mockCurrentUser } from '../mocks/users';
import { authAPI } from '../services/api';
const AuthContext = createContext(null);
@@ -16,45 +16,53 @@ export const AuthProvider = ({ children }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if user is logged in (from localStorage)
const storedUser = localStorage.getItem('user');
if (storedUser) {
setUser(JSON.parse(storedUser));
}
setLoading(false);
// Check if user is logged in (from token)
const loadUser = async () => {
const token = localStorage.getItem('token');
if (token) {
try {
const userData = await authAPI.getCurrentUser();
setUser(userData);
} catch (error) {
// Token expired or invalid
console.error('Failed to load user:', error);
localStorage.removeItem('token');
localStorage.removeItem('user');
}
}
setLoading(false);
};
loadUser();
}, []);
const login = async (email, password) => {
// Mock login - w przyszłości będzie API call
return new Promise((resolve) => {
setTimeout(() => {
const userData = mockCurrentUser;
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
resolve(userData);
}, 500);
});
try {
const { user: userData } = await authAPI.login(email, password);
setUser(userData);
// Save to localStorage for persistence
localStorage.setItem('user', JSON.stringify(userData));
return userData;
} catch (error) {
throw new Error(error.data?.error || 'Login failed');
}
};
const register = async (username, email, password) => {
// Mock register - w przyszłości będzie API call
return new Promise((resolve) => {
setTimeout(() => {
const userData = {
...mockCurrentUser,
username,
email,
};
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
resolve(userData);
}, 500);
});
try {
const { user: userData } = await authAPI.register(username, email, password);
setUser(userData);
// Save to localStorage for persistence
localStorage.setItem('user', JSON.stringify(userData));
return userData;
} catch (error) {
throw new Error(error.data?.error || 'Registration failed');
}
};
const logout = () => {
authAPI.logout();
setUser(null);
localStorage.removeItem('user');
};
const value = {