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

@@ -0,0 +1,104 @@
const API_URL = 'http://localhost:8080/api';
class ApiError extends Error {
constructor(message, status, data) {
super(message);
this.name = 'ApiError';
this.status = status;
this.data = data;
}
}
async function fetchAPI(endpoint, options = {}) {
const url = `${API_URL}${endpoint}`;
const config = {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
};
// Add auth token if available
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
try {
const response = await fetch(url, config);
const data = await response.json();
if (!response.ok) {
throw new ApiError(
data.error || 'API request failed',
response.status,
data
);
}
return data;
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError('Network error', 0, { message: error.message });
}
}
// Auth API
export const authAPI = {
async register(username, email, password) {
const data = await fetchAPI('/auth/register', {
method: 'POST',
body: JSON.stringify({ username, email, password }),
});
// Save token
if (data.data.token) {
localStorage.setItem('token', data.data.token);
}
return data.data;
},
async login(email, password) {
const data = await fetchAPI('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
// Save token
if (data.data.token) {
localStorage.setItem('token', data.data.token);
}
return data.data;
},
async getCurrentUser() {
const data = await fetchAPI('/users/me');
return data.data;
},
logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
},
};
// Events API
export const eventsAPI = {
async getAll() {
const data = await fetchAPI('/events');
return data.data;
},
async getById(id) {
const data = await fetchAPI(`/events/${id}`);
return data.data;
},
};
export { ApiError };