More changes that I would want my example to have I think
This commit is contained in:
369
src/lib/metadata.ts
Normal file
369
src/lib/metadata.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
import type { Metadata } from 'next';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
export const generateMetadata = (): Metadata => {
|
||||
return {
|
||||
title: {
|
||||
template: '%s | Tech Tracker',
|
||||
default: 'Tech Tracker',
|
||||
},
|
||||
description:
|
||||
'App used by COG IT employees to \
|
||||
update their status throughout the day.',
|
||||
applicationName: 'Tech Tracker',
|
||||
keywords:
|
||||
'Tech Tracker, City of Gulfport, Information Technology, T3 Template, ' +
|
||||
'Next.js, Supabase, Tailwind, TypeScript, React, T3, Gib',
|
||||
authors: [{ name: 'Gib', url: 'https://gbrown.org' }],
|
||||
creator: 'Gib Brown',
|
||||
publisher: 'Gib Brown',
|
||||
formatDetection: {
|
||||
email: false,
|
||||
address: false,
|
||||
telephone: false,
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
nocache: false,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
noimageindex: false,
|
||||
'max-video-preview': -1,
|
||||
'max-image-preview': 'large',
|
||||
'max-snippet': -1,
|
||||
},
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/favicon.ico', type: 'image/x-icon', sizes: 'any' },
|
||||
{
|
||||
url: '/favicon-16.png',
|
||||
type: 'image/png',
|
||||
sizes: '16x16',
|
||||
},
|
||||
{
|
||||
url: '/favicon-32.png',
|
||||
type: 'image/png',
|
||||
sizes: '32x32',
|
||||
},
|
||||
{ url: '/favicon.png', type: 'image/png', sizes: '96x96' },
|
||||
{
|
||||
url: '/favicon.ico',
|
||||
type: 'image/x-icon',
|
||||
sizes: 'any',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/favicon-16.png',
|
||||
type: 'image/png',
|
||||
sizes: '16x16',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/favicon-32.png',
|
||||
type: 'image/png',
|
||||
sizes: '32x32',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/favicon.png',
|
||||
type: 'image/png',
|
||||
sizes: '96x96',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-36.png',
|
||||
type: 'image/png',
|
||||
sizes: '36x36',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-48.png',
|
||||
type: 'image/png',
|
||||
sizes: '48x48',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-72.png',
|
||||
type: 'image/png',
|
||||
sizes: '72x72',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-96.png',
|
||||
type: 'image/png',
|
||||
sizes: '96x96',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-144.png',
|
||||
type: 'image/png',
|
||||
sizes: '144x144',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon.png',
|
||||
type: 'image/png',
|
||||
sizes: '192x192',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-36.png',
|
||||
type: 'image/png',
|
||||
sizes: '36x36',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-48.png',
|
||||
type: 'image/png',
|
||||
sizes: '48x48',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-72.png',
|
||||
type: 'image/png',
|
||||
sizes: '72x72',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-96.png',
|
||||
type: 'image/png',
|
||||
sizes: '96x96',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-144.png',
|
||||
type: 'image/png',
|
||||
sizes: '144x144',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon.png',
|
||||
type: 'image/png',
|
||||
sizes: '192x192',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
],
|
||||
shortcut: [
|
||||
{
|
||||
url: '/appicon/icon-36.png',
|
||||
type: 'image/png',
|
||||
sizes: '36x36',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-48.png',
|
||||
type: 'image/png',
|
||||
sizes: '48x48',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-72.png',
|
||||
type: 'image/png',
|
||||
sizes: '72x72',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-96.png',
|
||||
type: 'image/png',
|
||||
sizes: '96x96',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-144.png',
|
||||
type: 'image/png',
|
||||
sizes: '144x144',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon.png',
|
||||
type: 'image/png',
|
||||
sizes: '192x192',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-36.png',
|
||||
type: 'image/png',
|
||||
sizes: '36x36',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-48.png',
|
||||
type: 'image/png',
|
||||
sizes: '48x48',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-72.png',
|
||||
type: 'image/png',
|
||||
sizes: '72x72',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-96.png',
|
||||
type: 'image/png',
|
||||
sizes: '96x96',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon-144.png',
|
||||
type: 'image/png',
|
||||
sizes: '144x144',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: '/appicon/icon.png',
|
||||
type: 'image/png',
|
||||
sizes: '192x192',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
],
|
||||
apple: [
|
||||
{
|
||||
url: 'appicon/icon-57.png',
|
||||
type: 'image/png',
|
||||
sizes: '57x57',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-60.png',
|
||||
type: 'image/png',
|
||||
sizes: '60x60',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-72.png',
|
||||
type: 'image/png',
|
||||
sizes: '72x72',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-76.png',
|
||||
type: 'image/png',
|
||||
sizes: '76x76',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-114.png',
|
||||
type: 'image/png',
|
||||
sizes: '114x114',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-120.png',
|
||||
type: 'image/png',
|
||||
sizes: '120x120',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-144.png',
|
||||
type: 'image/png',
|
||||
sizes: '144x144',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-152.png',
|
||||
type: 'image/png',
|
||||
sizes: '152x152',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-180.png',
|
||||
type: 'image/png',
|
||||
sizes: '180x180',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon.png',
|
||||
type: 'image/png',
|
||||
sizes: '192x192',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-57.png',
|
||||
type: 'image/png',
|
||||
sizes: '57x57',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-60.png',
|
||||
type: 'image/png',
|
||||
sizes: '60x60',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-72.png',
|
||||
type: 'image/png',
|
||||
sizes: '72x72',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-76.png',
|
||||
type: 'image/png',
|
||||
sizes: '76x76',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-114.png',
|
||||
type: 'image/png',
|
||||
sizes: '114x114',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-120.png',
|
||||
type: 'image/png',
|
||||
sizes: '120x120',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-144.png',
|
||||
type: 'image/png',
|
||||
sizes: '144x144',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-152.png',
|
||||
type: 'image/png',
|
||||
sizes: '152x152',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon-180.png',
|
||||
type: 'image/png',
|
||||
sizes: '180x180',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
{
|
||||
url: 'appicon/icon.png',
|
||||
type: 'image/png',
|
||||
sizes: '192x192',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
],
|
||||
other: [
|
||||
{
|
||||
rel: 'apple-touch-icon-precomposed',
|
||||
url: '/appicon/icon-precomposed.png',
|
||||
type: 'image/png',
|
||||
sizes: '180x180',
|
||||
},
|
||||
],
|
||||
},
|
||||
other: {
|
||||
...Sentry.getTraceData(),
|
||||
},
|
||||
appleWebApp: {
|
||||
title: 'Tech Tracker',
|
||||
statusBarStyle: 'black-translucent',
|
||||
startupImage: [
|
||||
'/icons/apple/splash-768x1004.png',
|
||||
{
|
||||
url: '/icons/apple/splash-1536x2008.png',
|
||||
media: '(device-width: 768px) and (device-height: 1024px)',
|
||||
},
|
||||
],
|
||||
},
|
||||
verification: {
|
||||
google: 'google',
|
||||
yandex: 'yandex',
|
||||
yahoo: 'yahoo',
|
||||
},
|
||||
category: 'technology',
|
||||
/*
|
||||
appLinks: {
|
||||
ios: {
|
||||
url: 'https://techtracker.gbrown.org/ios',
|
||||
app_store_id: 'com.gbrown.techtracker',
|
||||
},
|
||||
android: {
|
||||
package: 'https://techtracker.gbrown.org/android',
|
||||
app_name: 'app_t3_template',
|
||||
},
|
||||
web: {
|
||||
url: 'https://techtracker.gbrown.org',
|
||||
should_fallback: true,
|
||||
},
|
||||
},
|
||||
*/
|
||||
};
|
||||
};
|
201
src/lib/middleware/ban-suspicious-ips.ts
Normal file
201
src/lib/middleware/ban-suspicious-ips.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
// In-memory stores for tracking IPs (use Redis in production)
|
||||
const ipAttempts = new Map<string, { count: number; lastAttempt: number }>();
|
||||
const ip404Attempts = new Map<string, { count: number; lastAttempt: number }>();
|
||||
const bannedIPs = new Set<string>();
|
||||
|
||||
// Ban Arctic Wolf Explicitly
|
||||
bannedIPs.add('::ffff:10.0.1.49');
|
||||
|
||||
// Suspicious patterns that indicate malicious activity
|
||||
const MALICIOUS_PATTERNS = [
|
||||
// Your existing patterns
|
||||
/web-inf/i,
|
||||
/\.jsp/i,
|
||||
/\.php/i,
|
||||
/puttest/i,
|
||||
/WEB-INF/i,
|
||||
/\.xml$/i,
|
||||
/perl/i,
|
||||
/xampp/i,
|
||||
/phpwebgallery/i,
|
||||
/FileManager/i,
|
||||
/standalonemanager/i,
|
||||
/h2console/i,
|
||||
/WebAdmin/i,
|
||||
/login_form\.php/i,
|
||||
/%2e/i,
|
||||
/%u002e/i,
|
||||
/\.%00/i,
|
||||
/\.\./,
|
||||
/lcgi/i,
|
||||
|
||||
// New patterns from your logs
|
||||
/\/appliance\//i,
|
||||
/bomgar/i,
|
||||
/netburner-logo/i,
|
||||
/\/ui\/images\//i,
|
||||
/logon_merge/i,
|
||||
/logon_t\.gif/i,
|
||||
/login_top\.gif/i,
|
||||
/theme1\/images/i,
|
||||
/\.well-known\/acme-challenge\/.*\.jpg$/i,
|
||||
/\.well-known\/pki-validation\/.*\.jpg$/i,
|
||||
|
||||
// Path traversal and system file access patterns
|
||||
/\/etc\/passwd/i,
|
||||
/\/etc%2fpasswd/i,
|
||||
/\/etc%5cpasswd/i,
|
||||
/\/\/+etc/i,
|
||||
/\\\\+.*etc/i,
|
||||
/%2f%2f/i,
|
||||
/%5c%5c/i,
|
||||
/\/\/+/,
|
||||
/\\\\+/,
|
||||
/%00/i,
|
||||
/%23/i,
|
||||
|
||||
// Encoded path traversal attempts
|
||||
/%2e%2e/i,
|
||||
/%252e/i,
|
||||
/%c0%ae/i,
|
||||
/%c1%9c/i,
|
||||
];
|
||||
|
||||
// Suspicious HTTP methods
|
||||
const SUSPICIOUS_METHODS = ['TRACE', 'PUT', 'DELETE', 'PATCH'];
|
||||
|
||||
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
|
||||
const MAX_ATTEMPTS = 10; // Max suspicious requests per window
|
||||
const BAN_DURATION = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
// 404 rate limiting settings
|
||||
const RATE_404_WINDOW = 2 * 60 * 1000; // 2 minutes
|
||||
const MAX_404_ATTEMPTS = 10; // Max 404s before ban
|
||||
|
||||
const getClientIP = (request: NextRequest): string => {
|
||||
const forwarded = request.headers.get('x-forwarded-for');
|
||||
const realIP = request.headers.get('x-real-ip');
|
||||
const cfConnectingIP = request.headers.get('cf-connecting-ip');
|
||||
|
||||
if (forwarded) return (forwarded.split(',')[0] ?? '').trim();
|
||||
if (realIP) return realIP;
|
||||
if (cfConnectingIP) return cfConnectingIP;
|
||||
return request.headers.get('host') ?? 'unknown';
|
||||
};
|
||||
|
||||
const isPathSuspicious = (pathname: string): boolean => {
|
||||
return MALICIOUS_PATTERNS.some((pattern) => pattern.test(pathname));
|
||||
};
|
||||
|
||||
const isMethodSuspicious = (method: string): boolean => {
|
||||
return SUSPICIOUS_METHODS.includes(method);
|
||||
};
|
||||
|
||||
const updateIPAttempts = (ip: string): boolean => {
|
||||
const now = Date.now();
|
||||
const attempts = ipAttempts.get(ip);
|
||||
|
||||
if (!attempts || now - attempts.lastAttempt > RATE_LIMIT_WINDOW) {
|
||||
ipAttempts.set(ip, { count: 1, lastAttempt: now });
|
||||
return false;
|
||||
}
|
||||
|
||||
attempts.count++;
|
||||
attempts.lastAttempt = now;
|
||||
|
||||
if (attempts.count > MAX_ATTEMPTS) {
|
||||
bannedIPs.add(ip);
|
||||
ipAttempts.delete(ip);
|
||||
|
||||
setTimeout(() => {
|
||||
bannedIPs.delete(ip);
|
||||
}, BAN_DURATION);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const update404Attempts = (ip: string): boolean => {
|
||||
const now = Date.now();
|
||||
const attempts = ip404Attempts.get(ip);
|
||||
|
||||
if (!attempts || now - attempts.lastAttempt > RATE_404_WINDOW) {
|
||||
ip404Attempts.set(ip, { count: 1, lastAttempt: now });
|
||||
return false;
|
||||
}
|
||||
|
||||
attempts.count++;
|
||||
attempts.lastAttempt = now;
|
||||
|
||||
if (attempts.count > MAX_404_ATTEMPTS) {
|
||||
bannedIPs.add(ip);
|
||||
ip404Attempts.delete(ip);
|
||||
|
||||
console.log(
|
||||
`🔨 IP ${ip} banned for excessive 404 requests (${attempts.count} in ${RATE_404_WINDOW / 1000}s)`,
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
bannedIPs.delete(ip);
|
||||
}, BAN_DURATION);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const banSuspiciousIPs = (request: NextRequest): NextResponse | null => {
|
||||
const { pathname } = request.nextUrl;
|
||||
const method = request.method;
|
||||
const ip = getClientIP(request);
|
||||
|
||||
// Check if IP is already banned
|
||||
if (bannedIPs.has(ip)) {
|
||||
return new NextResponse('Access denied.', { status: 403 });
|
||||
}
|
||||
|
||||
const isSuspiciousPath = isPathSuspicious(pathname);
|
||||
const isSuspiciousMethod = isMethodSuspicious(method);
|
||||
|
||||
// Handle suspicious activity
|
||||
if (isSuspiciousPath || isSuspiciousMethod) {
|
||||
const shouldBan = updateIPAttempts(ip);
|
||||
|
||||
if (shouldBan) {
|
||||
console.log(`🔨 IP ${ip} has been banned for suspicious activity`);
|
||||
return new NextResponse('Access denied - IP banned. Please fuck off.', {
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
|
||||
return new NextResponse('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Call this function when you detect a 404 response
|
||||
export const handle404Response = (
|
||||
request: NextRequest,
|
||||
): NextResponse | null => {
|
||||
const ip = getClientIP(request);
|
||||
|
||||
if (bannedIPs.has(ip)) {
|
||||
return new NextResponse('Access denied.', { status: 403 });
|
||||
}
|
||||
|
||||
const shouldBan = update404Attempts(ip);
|
||||
|
||||
if (shouldBan) {
|
||||
return new NextResponse('Access denied - IP banned for excessive 404s.', {
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
20
src/lib/utils.ts
Normal file
20
src/lib/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
};
|
||||
|
||||
export const ccn = ({
|
||||
context,
|
||||
className,
|
||||
on = '',
|
||||
off = '',
|
||||
}: {
|
||||
context: boolean;
|
||||
className: string;
|
||||
on: string;
|
||||
off: string;
|
||||
}) => {
|
||||
return twMerge(className, context ? on : off);
|
||||
};
|
Reference in New Issue
Block a user