import { BadgeCheck, Cloud, Lock, Server, ShieldAlert } from "lucide-react"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; import { resetAntiddosSettings, saveAntiddosSettings, unbanAntiddosIp, } from "@/actions/admin-antiddos"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { antiddosDefaultsFromEnv, getAntiddosConfig, } from "@/lib/antiddos-config"; import { resolveClientIp } from "@/lib/client-ip"; import { isCloudflareProxied, preferredClientIpHeader } from "@/lib/cloudflare"; import { db, WebsiteSetting } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { redis } from "@/lib/redis"; function seconds(ttlMs: number): string { const s = Math.floor(ttlMs / 1000); if (s <= 0) return "–"; if (s < 60) return `${s}s`; if (s < 3600) return `${Math.floor(s / 60)}m${s % 60 ? ` ${s % 60}s` : ""}`; return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`; } export default async function AdminAntiDdosPage() { const { session, permissions } = await getAdminContext(); if (!canAccess(permissions, PERMS.SETTINGS_VIEW, session.user.rank)) { redirect("/admin"); } const [effective, defaults, requestHeaders, persistedRows] = await Promise.all([ getAntiddosConfig(), antiddosDefaultsFromEnv(), headers(), db .select({ key: WebsiteSetting.key, value: WebsiteSetting.value }) .from(WebsiteSetting) .then((rows) => new Map(rows.map((r) => [r.key, r.value]))) .catch(() => new Map() as Map), ]); const cloudflare = isCloudflareProxied(requestHeaders); const sourceHeader = preferredClientIpHeader(requestHeaders); const viewerIp = resolveClientIp(requestHeaders); let blocks: { ip: string; ttlMs: number; count: number }[] = []; let redisOk = false; const rateStore = redis; if (rateStore) { redisOk = true; try { const blockKeys = await rateStore.keys("antiddos:block:*"); const violationKeys = await rateStore.keys("antiddos:v:*"); const violationCounts = new Map(); for (const key of violationKeys.slice(0, 200)) { // incr is used on writes; for display we just read the raw value. const raw = await rateStore.get(key); const n = Number(raw); violationCounts.set( key.replace(`antiddos:v:`, ""), Number.isFinite(n) ? n : 0, ); } const withTtl = await Promise.all( blockKeys.slice(0, 100).map(async (key) => { const ttlMs = await rateStore.pttl(key); return { ip: key.replace("antiddos:block:", ""), ttlMs: ttlMs > 0 ? ttlMs : 0, count: violationCounts.get(key.replace("antiddos:block:", "")) ?? 0, }; }), ); blocks = withTtl .filter((b) => b.ttlMs > 0) .sort((a, b) => a.ttlMs - b.ttlMs); } catch { redisOk = false; } } const stored = persistedRows; return (
Gate {effective.enabled ? "Enabled" : "Disabled"}

Boot default: {defaults.enabled ? "on" : "off"}

Rates
{effective.api.limit} {" "} /min API

Pages {effective.pages.limit} · Auth {effective.auth.limit} · Global {effective.global.limit}

Cloudflare {cloudflare ? "Detected" : "Not detected"}

IP source: {sourceHeader}

Active blocks
{blocks.length}

Temporary DDoS blocks

Redis {redisOk ? "Connected" : "Unavailable"}

Shared rate-limit state

{!cloudflare && ( Cloudflare not detected

This request did not arrive through Cloudflare. When the site DNS is proxied (orange cloud), the CMS trusts the real visitor IP from{" "} CF-Connecting-IP. A direct client can spoof that header — put the origin behind Cloudflare and restrict direct access to the origin ports for full DDoS protection.

Your request is keyed as{" "} {viewerIp} (via{" "} {sourceHeader}).

)} Anti-DDoS settings
{( [ ["pages", "Pages", "pages_limit", "pages_window_sec"], ["api", "API", "api_limit", "api_window_sec"], ["auth", "Auth", "auth_limit", "auth_window_sec"], ] as const ).map(([category, label, limitName, windowName]) => (

{label}

req/min sec window
))}

Boot default:{" "} {defaults.blockTiers .map((t) => `${t.minViolations}:${t.ttlSeconds}`) .join(", ")}

Blocked IPs ({blocks.length}) {blocks.length === 0 ? (

No IPs are currently rate-limited into a temporary block.

) : (
{blocks.map((b) => (
{b.ip} TTL {seconds(b.ttlMs)} · violations {b.count}
))}
)}
{stored.size === 0 && (

Persisted site settings: none yet — the form values above reflect the current effective configuration.

)}
); }