Add DragonflyDB integration for JWT revocation and implement rate limiting for authentication routes
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m14s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m14s
This commit is contained in:
@@ -1,13 +1,49 @@
|
||||
import { Router } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
import { RedisStore } from 'rate-limit-redis';
|
||||
import { getDb } from '../config/database.js';
|
||||
import redis from '../config/redis.js';
|
||||
import { authenticateToken, generateToken } from '../middleware/auth.js';
|
||||
import { isMailerConfigured, sendVerificationEmail } from '../config/mailer.js';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-change-me';
|
||||
|
||||
// ── Rate Limiting ────────────────────────────────────────────────────────────
|
||||
function makeRedisStore(prefix) {
|
||||
try {
|
||||
return new RedisStore({
|
||||
sendCommand: (...args) => redis.call(...args),
|
||||
prefix,
|
||||
});
|
||||
} catch {
|
||||
return undefined; // falls back to in-memory if Redis unavailable
|
||||
}
|
||||
}
|
||||
|
||||
const loginLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 20,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: 'Too many login attempts. Please try again in 15 minutes.' },
|
||||
store: makeRedisStore('rl:login:'),
|
||||
});
|
||||
|
||||
const registerLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 10,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: 'Too many registration attempts. Please try again later.' },
|
||||
store: makeRedisStore('rl:register:'),
|
||||
});
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const uploadsDir = path.join(__dirname, '..', '..', 'uploads', 'avatars');
|
||||
@@ -20,7 +56,7 @@ if (!fs.existsSync(uploadsDir)) {
|
||||
const router = Router();
|
||||
|
||||
// POST /api/auth/register
|
||||
router.post('/register', async (req, res) => {
|
||||
router.post('/register', registerLimiter, async (req, res) => {
|
||||
try {
|
||||
const { username, display_name, email, password } = req.body;
|
||||
|
||||
@@ -194,7 +230,7 @@ router.post('/resend-verification', async (req, res) => {
|
||||
});
|
||||
|
||||
// POST /api/auth/login
|
||||
router.post('/login', async (req, res) => {
|
||||
router.post('/login', loginLimiter, async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
@@ -223,6 +259,31 @@ router.post('/login', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/auth/logout – revoke JWT via DragonflyDB blacklist
|
||||
router.post('/logout', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
const decoded = jwt.decode(token);
|
||||
|
||||
if (decoded?.jti && decoded?.exp) {
|
||||
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
|
||||
if (ttl > 0) {
|
||||
try {
|
||||
await redis.setex(`blacklist:${decoded.jti}`, ttl, '1');
|
||||
} catch (redisErr) {
|
||||
console.warn('Redis blacklist write failed:', redisErr.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
res.status(500).json({ error: 'Logout failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/auth/me
|
||||
router.get('/me', authenticateToken, (req, res) => {
|
||||
res.json({ user: req.user });
|
||||
|
||||
Reference in New Issue
Block a user