update proxy to be in line with convex auth
This commit is contained in:
@@ -4,7 +4,8 @@ import { NextResponse } from 'next/server';
|
|||||||
// In-memory stores for tracking IPs (use Redis in production)
|
// In-memory stores for tracking IPs (use Redis in production)
|
||||||
const ipAttempts = new Map<string, { count: number; lastAttempt: number }>();
|
const ipAttempts = new Map<string, { count: number; lastAttempt: number }>();
|
||||||
const ip404Attempts = new Map<string, { count: number; lastAttempt: number }>();
|
const ip404Attempts = new Map<string, { count: number; lastAttempt: number }>();
|
||||||
const bannedIPs = new Set<string>();
|
// Map of ip -> ban expiry timestamp. Avoids setTimeout closures leaking on hot reload.
|
||||||
|
const bannedIPs = new Map<string, number>();
|
||||||
|
|
||||||
// Suspicious patterns that indicate malicious activity
|
// Suspicious patterns that indicate malicious activity
|
||||||
const MALICIOUS_PATTERNS = [
|
const MALICIOUS_PATTERNS = [
|
||||||
@@ -72,6 +73,36 @@ const BAN_DURATION = 30 * 60 * 1000; // 30 minutes
|
|||||||
const RATE_404_WINDOW = 2 * 60 * 1000; // 2 minutes
|
const RATE_404_WINDOW = 2 * 60 * 1000; // 2 minutes
|
||||||
const MAX_404_ATTEMPTS = 10; // Max 404s before ban
|
const MAX_404_ATTEMPTS = 10; // Max 404s before ban
|
||||||
|
|
||||||
|
let lastCleanup = Date.now();
|
||||||
|
const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
// Lazily purge stale entries so Maps don't grow without bound.
|
||||||
|
// Called on every request but only iterates Maps every CLEANUP_INTERVAL.
|
||||||
|
const cleanupStaleMaps = () => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastCleanup < CLEANUP_INTERVAL) return;
|
||||||
|
lastCleanup = now;
|
||||||
|
for (const [ip, data] of ipAttempts.entries()) {
|
||||||
|
if (now - data.lastAttempt > RATE_LIMIT_WINDOW) ipAttempts.delete(ip);
|
||||||
|
}
|
||||||
|
for (const [ip, data] of ip404Attempts.entries()) {
|
||||||
|
if (now - data.lastAttempt > RATE_404_WINDOW) ip404Attempts.delete(ip);
|
||||||
|
}
|
||||||
|
for (const [ip, expiry] of bannedIPs.entries()) {
|
||||||
|
if (now > expiry) bannedIPs.delete(ip);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isIPBanned = (ip: string): boolean => {
|
||||||
|
const expiry = bannedIPs.get(ip);
|
||||||
|
if (expiry === undefined) return false;
|
||||||
|
if (Date.now() > expiry) {
|
||||||
|
bannedIPs.delete(ip);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const getClientIP = (request: NextRequest): string => {
|
const getClientIP = (request: NextRequest): string => {
|
||||||
const forwarded = request.headers.get('x-forwarded-for');
|
const forwarded = request.headers.get('x-forwarded-for');
|
||||||
const realIP = request.headers.get('x-real-ip');
|
const realIP = request.headers.get('x-real-ip');
|
||||||
@@ -104,13 +135,8 @@ const updateIPAttempts = (ip: string): boolean => {
|
|||||||
attempts.lastAttempt = now;
|
attempts.lastAttempt = now;
|
||||||
|
|
||||||
if (attempts.count > MAX_ATTEMPTS) {
|
if (attempts.count > MAX_ATTEMPTS) {
|
||||||
bannedIPs.add(ip);
|
bannedIPs.set(ip, Date.now() + BAN_DURATION);
|
||||||
ipAttempts.delete(ip);
|
ipAttempts.delete(ip);
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
bannedIPs.delete(ip);
|
|
||||||
}, BAN_DURATION);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,17 +156,13 @@ const update404Attempts = (ip: string): boolean => {
|
|||||||
attempts.lastAttempt = now;
|
attempts.lastAttempt = now;
|
||||||
|
|
||||||
if (attempts.count > MAX_404_ATTEMPTS) {
|
if (attempts.count > MAX_404_ATTEMPTS) {
|
||||||
bannedIPs.add(ip);
|
bannedIPs.set(ip, Date.now() + BAN_DURATION);
|
||||||
ip404Attempts.delete(ip);
|
ip404Attempts.delete(ip);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`🔨 IP ${ip} banned for excessive 404 requests (${attempts.count} in ${RATE_404_WINDOW / 1000}s)`,
|
`🔨 IP ${ip} banned for excessive 404 requests (${attempts.count} in ${RATE_404_WINDOW / 1000}s)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
bannedIPs.delete(ip);
|
|
||||||
}, BAN_DURATION);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,12 +170,14 @@ const update404Attempts = (ip: string): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const banSuspiciousIPs = (request: NextRequest): NextResponse | null => {
|
export const banSuspiciousIPs = (request: NextRequest): NextResponse | null => {
|
||||||
|
cleanupStaleMaps();
|
||||||
|
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
const method = request.method;
|
const method = request.method;
|
||||||
const ip = getClientIP(request);
|
const ip = getClientIP(request);
|
||||||
|
|
||||||
// Check if IP is already banned
|
// Check if IP is already banned
|
||||||
if (bannedIPs.has(ip)) {
|
if (isIPBanned(ip)) {
|
||||||
return new NextResponse('Access denied.', { status: 403 });
|
return new NextResponse('Access denied.', { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +207,7 @@ export const handle404Response = (
|
|||||||
): NextResponse | null => {
|
): NextResponse | null => {
|
||||||
const ip = getClientIP(request);
|
const ip = getClientIP(request);
|
||||||
|
|
||||||
if (bannedIPs.has(ip)) {
|
if (isIPBanned(ip)) {
|
||||||
return new NextResponse('Access denied.', { status: 403 });
|
return new NextResponse('Access denied.', { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,36 @@
|
|||||||
|
import { convexAuthNextjsMiddleware } from "@convex-dev/auth/nextjs/server";
|
||||||
|
|
||||||
|
export default convexAuthNextjsMiddleware();
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
'/((?!_next/static|_next/image|favicon.ico|monitoring-tunnel|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||||
|
'/((?!.*\\..*|_next).*)',
|
||||||
|
'/',
|
||||||
|
'/(api)(.*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
import { banSuspiciousIPs } from '@/lib/proxy/ban-sus-ips';
|
import { banSuspiciousIPs } from '@/lib/proxy/ban-sus-ips';
|
||||||
import {
|
import {
|
||||||
convexAuthNextjsMiddleware,
|
convexAuthNextjsMiddleware,
|
||||||
createRouteMatcher,
|
createRouteMatcher,
|
||||||
nextjsMiddlewareRedirect,
|
nextjsMiddlewareRedirect,
|
||||||
} from '@convex-dev/auth/nextjs/server';
|
} from '@convex-dev/auth/nextjs/server';
|
||||||
|
|
||||||
const isSignInPage = createRouteMatcher(['/sign-in']);
|
const isSignInPage = createRouteMatcher(['/sign-in']);
|
||||||
const isProtectedRoute = createRouteMatcher(['/profile', '/admin']);
|
const isProtectedRoute = createRouteMatcher([
|
||||||
|
'/profile(.*)',
|
||||||
|
'/dashboard(.*)',
|
||||||
|
'/admin-panel(.*)',
|
||||||
|
]);
|
||||||
|
|
||||||
export default convexAuthNextjsMiddleware(
|
export default convexAuthNextjsMiddleware(
|
||||||
async (request, { convexAuth }) => {
|
async (request, { convexAuth }) => {
|
||||||
const banResponse = banSuspiciousIPs(request);
|
const banResponse = banSuspiciousIPs(request);
|
||||||
if (banResponse) return banResponse;
|
if (banResponse) return banResponse;
|
||||||
if (isSignInPage(request) && (await convexAuth.isAuthenticated())) {
|
if (isSignInPage(request) && (await convexAuth.isAuthenticated())) {
|
||||||
return nextjsMiddlewareRedirect(request, '/');
|
return nextjsMiddlewareRedirect(request, '/profile');
|
||||||
}
|
}
|
||||||
if (isProtectedRoute(request) && !(await convexAuth.isAuthenticated())) {
|
if (isProtectedRoute(request) && !(await convexAuth.isAuthenticated())) {
|
||||||
return nextjsMiddlewareRedirect(request, '/sign-in');
|
return nextjsMiddlewareRedirect(request, '/sign-in');
|
||||||
@@ -23,8 +40,6 @@ export default convexAuthNextjsMiddleware(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
// The following matcher runs middleware on all routes
|
|
||||||
// except static assets.
|
|
||||||
matcher: [
|
matcher: [
|
||||||
'/((?!_next/static|_next/image|favicon.ico|monitoring-tunnel|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
'/((?!_next/static|_next/image|favicon.ico|monitoring-tunnel|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||||
'/((?!.*\\..*|_next).*)',
|
'/((?!.*\\..*|_next).*)',
|
||||||
@@ -32,3 +47,4 @@ export const config = {
|
|||||||
'/(api)(.*)',
|
'/(api)(.*)',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
**/
|
||||||
|
|||||||
Reference in New Issue
Block a user