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,64 @@
const { verifyToken } = require('../utils/auth');
const { prisma } = require('../utils/db');
// Authentication middleware
async function authenticate(req, res, next) {
try {
// Get token from header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
error: 'Unauthorized',
message: 'No token provided',
});
}
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
// Verify token
const decoded = verifyToken(token);
if (!decoded) {
return res.status(401).json({
success: false,
error: 'Unauthorized',
message: 'Invalid or expired token',
});
}
// Get user from database
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: {
id: true,
username: true,
email: true,
avatar: true,
createdAt: true,
updatedAt: true,
},
});
if (!user) {
return res.status(401).json({
success: false,
error: 'Unauthorized',
message: 'User not found',
});
}
// Attach user to request
req.user = user;
next();
} catch (error) {
console.error('Auth middleware error:', error);
res.status(500).json({
success: false,
error: 'Internal Server Error',
});
}
}
module.exports = { authenticate };

View File

@@ -0,0 +1,52 @@
const { body, validationResult } = require('express-validator');
// Validation error handler
function handleValidationErrors(req, res, next) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: 'Validation Error',
details: errors.array(),
});
}
next();
}
// Register validation rules
const registerValidation = [
body('username')
.trim()
.isLength({ min: 3, max: 50 })
.withMessage('Username must be between 3 and 50 characters')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Username can only contain letters, numbers, and underscores'),
body('email')
.trim()
.isEmail()
.withMessage('Must be a valid email address')
.normalizeEmail(),
body('password')
.isLength({ min: 6 })
.withMessage('Password must be at least 6 characters long'),
handleValidationErrors,
];
// Login validation rules
const loginValidation = [
body('email')
.trim()
.isEmail()
.withMessage('Must be a valid email address')
.normalizeEmail(),
body('password')
.notEmpty()
.withMessage('Password is required'),
handleValidationErrors,
];
module.exports = {
registerValidation,
loginValidation,
handleValidationErrors,
};