import { BadgeCheck, Cloud, Lock, Server, ShieldAlert } from "lucide-react"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; import { removeCloudflareRule, resetAntiddosSettings, saveAntiddosSettings, unbanAntiddosIp, verifyCloudflareConfiguration, } 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 { type CloudflareBlockView, cloudflareEnabled, getLastCloudflareVerify, listCloudflareBlocks, sweepExpiredCloudflareBlocks, } from "@/lib/cloudflare-api"; 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; source: "gate"; }[] = []; 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); const ip = key.replace("antiddos:block:", ""); return { ip, ttlMs: ttlMs > 0 ? ttlMs : 0, count: violationCounts.get(ip) ?? 0, source: "gate" as const, }; }), ); blocks = withTtl .filter((b) => b.ttlMs > 0) .sort((a, b) => a.ttlMs - b.ttlMs); } catch { redisOk = false; } } const stored = persistedRows; const cloudflareConfigured = cloudflareEnabled(); const cloudflareBlocks: CloudflareBlockView[] = []; if (cloudflareConfigured) { await sweepExpiredCloudflareBlocks(); cloudflareBlocks.push(...(await listCloudflareBlocks())); } const lastVerify = await getLastCloudflareVerify(); 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

Requires CLOUDFLARE_API_TOKEN{" "} and CLOUDFLARE_ZONE_ID in the environment. Blocks are only created for traffic that provably transits Cloudflare, and expire together with the host-level block.

{( [ ["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) => { return (
{b.ip} Gate TTL {seconds(b.ttlMs)} · violations {b.count}
); })}
)}
Cloudflare edge blocks
{cloudflareConfigured ? "API configured" : "API not configured"} {!cloudflareConfigured && (

Set CLOUDFLARE_API_TOKEN and{" "} CLOUDFLARE_ZONE_ID to enable automatic edge blocking via the Cloudflare API.

)}
{lastVerify && cloudflareConfigured && (

{lastVerify.ok ? "Reachable" : "Failed"} {lastVerify.ok ? `Zone ${lastVerify.zoneName ?? lastVerify.zoneId ?? ""} — verified ${new Date(lastVerify.at).toLocaleString()}` : lastVerify.message}

)} {cloudflareConfigured && cloudflareBlocks.length === 0 ? (

No automatic Cloudflare blocks are active. When the gate blocks a repeat offender behind Cloudflare, an IP Access Rule is created here automatically.

) : ( cloudflareConfigured && (
{cloudflareBlocks.map((b) => (
{b.ip} {b.category} · {seconds(b.remainingSeconds * 1000)} left
))}

Expired rules are swept automatically every 30s.

) )}
{stored.size === 0 && (

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

)}
); }