Initial commit

This commit is contained in:
2026-04-28 22:51:54 +03:00
commit 3565390a49
76 changed files with 16956 additions and 0 deletions

View 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
};

View 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;

View 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;