Initial commit
This commit is contained in:
115
backend/src/middleware/auth.js
Normal file
115
backend/src/middleware/auth.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const pool = require('../config/database');
|
||||
const logger = require('../config/logger');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
||||
|
||||
function verifyJwtToken(token) {
|
||||
try {
|
||||
return jwt.verify(token, JWT_SECRET);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getApiKeyInfo(apiKey, touchLastUsed = false) {
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = touchLastUsed
|
||||
? `UPDATE api_keys
|
||||
SET last_used_at = CURRENT_TIMESTAMP
|
||||
WHERE key = $1 AND is_active = true
|
||||
RETURNING id, app_name, rate_limit`
|
||||
: `SELECT id, app_name, rate_limit
|
||||
FROM api_keys
|
||||
WHERE key = $1 AND is_active = true`;
|
||||
|
||||
const result = await pool.query(query, [apiKey]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
// Verify JWT token
|
||||
function authenticateToken(req, res, next) {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
const user = verifyJwtToken(token);
|
||||
|
||||
if (!user) {
|
||||
return res.status(403).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
}
|
||||
|
||||
// Check if user is admin
|
||||
function requireAdmin(req, res, next) {
|
||||
if (!req.user || req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
async function authenticateApiKey(req, res, next) {
|
||||
// Check for JWT token first
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (token) {
|
||||
// If JWT token is present, verify it
|
||||
try {
|
||||
const user = verifyJwtToken(token);
|
||||
if (!user) {
|
||||
throw new Error('Invalid JWT token');
|
||||
}
|
||||
req.user = user;
|
||||
return next();
|
||||
} catch (error) {
|
||||
// JWT token is invalid, continue to API key check
|
||||
logger.warn('Invalid JWT token in request');
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to API key authentication
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(401).json({
|
||||
error: 'Authentication required',
|
||||
message: 'Please provide either Authorization header with JWT token or X-API-Key header'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const apiKeyInfo = await getApiKeyInfo(apiKey, true);
|
||||
|
||||
if (!apiKeyInfo) {
|
||||
logger.warn(`Invalid API key attempt: ${apiKey.substring(0, 8)}...`);
|
||||
return res.status(403).json({
|
||||
error: 'Invalid API key',
|
||||
message: 'The provided API key is invalid or inactive'
|
||||
});
|
||||
}
|
||||
|
||||
req.apiKeyInfo = apiKeyInfo;
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error('Authentication error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authenticateToken,
|
||||
requireAdmin,
|
||||
authenticateApiKey,
|
||||
verifyJwtToken,
|
||||
getApiKeyInfo
|
||||
};
|
||||
66
backend/src/middleware/rateLimiter.js
Normal file
66
backend/src/middleware/rateLimiter.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const redisClient = require('../config/redis');
|
||||
|
||||
// In-memory fallback store if Redis is unavailable
|
||||
const memoryStore = new Map();
|
||||
|
||||
async function getRateLimitStore() {
|
||||
// Custom Redis store
|
||||
return {
|
||||
async increment(key) {
|
||||
try {
|
||||
const current = await redisClient.get(key);
|
||||
const count = current ? parseInt(current) + 1 : 1;
|
||||
await redisClient.setEx(key, 3600, count.toString()); // 1 hour TTL
|
||||
return { totalHits: count, resetTime: null };
|
||||
} catch (error) {
|
||||
// Fallback to memory store
|
||||
const count = (memoryStore.get(key) || 0) + 1;
|
||||
memoryStore.set(key, count);
|
||||
setTimeout(() => memoryStore.delete(key), 3600000); // 1 hour
|
||||
return { totalHits: count, resetTime: null };
|
||||
}
|
||||
},
|
||||
async decrement(key) {
|
||||
try {
|
||||
await redisClient.decr(key);
|
||||
} catch (error) {
|
||||
const count = memoryStore.get(key) || 0;
|
||||
memoryStore.set(key, Math.max(0, count - 1));
|
||||
}
|
||||
},
|
||||
async resetKey(key) {
|
||||
try {
|
||||
await redisClient.del(key);
|
||||
} catch (error) {
|
||||
memoryStore.delete(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const createRateLimiter = (maxRequests = 1000, windowMs = 3600000) => {
|
||||
return rateLimit({
|
||||
windowMs, // 1 hour default
|
||||
max: maxRequests,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
keyGenerator: (req) => {
|
||||
// Use API key if available, otherwise IP
|
||||
return req.apiKeyInfo?.id || req.ip;
|
||||
},
|
||||
handler: (req, res) => {
|
||||
res.status(429).json({
|
||||
error: 'Too many requests',
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
retryAfter: Math.ceil(windowMs / 1000)
|
||||
});
|
||||
},
|
||||
skip: (req) => {
|
||||
// Skip rate limiting for health checks
|
||||
return req.path === '/health';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = createRateLimiter;
|
||||
47
backend/src/middleware/upload.js
Normal file
47
backend/src/middleware/upload.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Ensure upload directories exist
|
||||
const uploadDir = path.join(__dirname, '../../public/uploads/avatars');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Configure storage
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// Generate unique filename: userId-timestamp.ext
|
||||
const userId = req.user.id;
|
||||
const timestamp = Date.now();
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `user-${userId}-${timestamp}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
// File filter - only images
|
||||
const fileFilter = (req, file, cb) => {
|
||||
const allowedTypes = /jpeg|jpg|png|gif|webp/;
|
||||
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
|
||||
const mimetype = allowedTypes.test(file.mimetype);
|
||||
|
||||
if (extname && mimetype) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only image files are allowed (jpeg, jpg, png, gif, webp)'));
|
||||
}
|
||||
};
|
||||
|
||||
// Configure multer
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024 // 5MB limit
|
||||
},
|
||||
fileFilter: fileFilter
|
||||
});
|
||||
|
||||
module.exports = upload;
|
||||
Reference in New Issue
Block a user