TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { ShieldAlert, ShieldCheck } from "lucide-react";3import { requireAdmin } from "@/lib/session";4import { getSuspiciousActivity, listAbuseEvents, str } from "@/lib/queries/admin";5import { formatNumber, formatPercent } from "@/lib/format";6import { PageHeader } from "@/components/ui/page-header";7import { Badge } from "@/components/ui/badge";8import { Button } from "@/components/ui/button";9import { EmptyState } from "@/components/ui/empty-state";10import { Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow } from "@/components/ui/table";11import { DateCell, Mono, Panel, PlanBadge, numCell } from "@/components/admin/primitives";12import { RecordAbuseDialog, ResolveAbuseButton } from "@/components/admin/abuse-actions";13import { cn } from "@/lib/utils";1415export const dynamic = "force-dynamic";1617function SeverityBadge({ s }: { s: string }) {18 const v = s === "critical" ? "danger" : s === "high" ? "danger" : s === "medium" ? "warning" : "default";19 return <Badge variant={v}>{s}</Badge>;20}2122export default async function AdminAbusePage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {23 await requireAdmin();24 const sp = await searchParams;25 const view = (["all", "open", "resolved"] as const).find((v) => v === str(sp.view)) ?? "open";26 const [events, sus] = await Promise.all([listAbuseEvents({ resolved: view }), getSuspiciousActivity()]);27 const signals = sus.ssrf.length + sus.limited.length + sus.unverified.length;2829 return (30 <>31 <PageHeader eyebrow="Trust & safety" title="Abuse" description="Recorded abuse events plus live heuristics computed from the last 24 hours of traffic." actions={<RecordAbuseDialog />} />3233 <Panel34 title={35 <span className="flex items-center gap-2">36 Suspicious activity (24h)37 {signals ? <Badge variant="warning" dot>{signals} signals</Badge> : <Badge variant="success" dot>clear</Badge>}38 </span>39 }40 description="Heuristics, not verdicts. Investigate from the organization page before acting."41 bodyClassName="p-0"42 >43 <div className="grid divide-y divide-border xl:grid-cols-3 xl:divide-x xl:divide-y-0">44 <div>45 <h3 className="px-4 pt-3 text-[12px] font-semibold uppercase tracking-wide text-fg-subtle">SSRF / disallowed URLs</h3>46 <p className="px-4 pb-2 text-[12px] text-fg-muted">Orgs with > 30 % `URL_NOT_ALLOWED` (min. 5 requests).</p>47 <Table>48 <TableHeader>49 <TableRow>50 <TableHead>Organization</TableHead>51 <TableHead className="text-right">Blocked</TableHead>52 <TableHead className="text-right">Ratio</TableHead>53 </TableRow>54 </TableHeader>55 <TableBody>56 {sus.ssrf.length === 0 ? (57 <TableEmpty colSpan={3}>None.</TableEmpty>58 ) : (59 sus.ssrf.map((r) => (60 <TableRow key={r.organizationId}>61 <TableCell>62 <Link href={`/admin/organizations/${r.organizationId}`} className="hover:underline">63 {r.orgName}64 </Link>{" "}65 <PlanBadge plan={r.plan} />66 </TableCell>67 <TableCell className={numCell}>68 {r.notAllowed} / {r.total}69 </TableCell>70 <TableCell className={cn(numCell, "text-danger")}>{formatPercent(r.ratio)}</TableCell>71 </TableRow>72 ))73 )}74 </TableBody>75 </Table>76 </div>77 <div>78 <h3 className="px-4 pt-3 text-[12px] font-semibold uppercase tracking-wide text-fg-subtle">Rate / concurrency limits</h3>79 <p className="px-4 pb-2 text-[12px] text-fg-muted">Orgs hitting `RATE_LIMITED` or `CONCURRENCY_LIMIT` most.</p>80 <Table>81 <TableHeader>82 <TableRow>83 <TableHead>Organization</TableHead>84 <TableHead className="text-right">Rate</TableHead>85 <TableHead className="text-right">Concurrency</TableHead>86 </TableRow>87 </TableHeader>88 <TableBody>89 {sus.limited.length === 0 ? (90 <TableEmpty colSpan={3}>None.</TableEmpty>91 ) : (92 sus.limited.map((r) => (93 <TableRow key={r.organizationId}>94 <TableCell>95 <Link href={`/admin/organizations/${r.organizationId}`} className="hover:underline">96 {r.orgName}97 </Link>{" "}98 <PlanBadge plan={r.plan} />99 </TableCell>100 <TableCell className={numCell}>{formatNumber(r.rateLimited)}</TableCell>101 <TableCell className={numCell}>{formatNumber(r.concurrency)}</TableCell>102 </TableRow>103 ))104 )}105 </TableBody>106 </Table>107 </div>108 <div>109 <h3 className="px-4 pt-3 text-[12px] font-semibold uppercase tracking-wide text-fg-subtle">Unverified heavy playground use</h3>110 <p className="px-4 pb-2 text-[12px] text-fg-muted">Unverified owners with ≥ 20 playground requests.</p>111 <Table>112 <TableHeader>113 <TableRow>114 <TableHead>User</TableHead>115 <TableHead className="text-right">Requests</TableHead>116 <TableHead className="text-right">Signed up</TableHead>117 </TableRow>118 </TableHeader>119 <TableBody>120 {sus.unverified.length === 0 ? (121 <TableEmpty colSpan={3}>None.</TableEmpty>122 ) : (123 sus.unverified.map((r) => (124 <TableRow key={r.userId}>125 <TableCell>126 <Link href={`/admin/users/${r.userId}`} className="hover:underline">127 {r.email}128 </Link>129 </TableCell>130 <TableCell className={numCell}>{formatNumber(r.playground)}</TableCell>131 <TableCell className="text-right">132 <DateCell value={r.createdAt} />133 </TableCell>134 </TableRow>135 ))136 )}137 </TableBody>138 </Table>139 </div>140 </div>141 </Panel>142143 <Panel144 title="Abuse events"145 right={146 <div className="inline-flex h-7 items-center gap-0.5 rounded-md bg-bg-muted p-0.5 text-[12px]">147 {(["open", "resolved", "all"] as const).map((v) => (148 <Button key={v} asChild variant={view === v ? "outline" : "ghost"} size="xs" className={cn("h-6 capitalize", view === v && "bg-bg")}>149 <Link href={`/admin/abuse?view=${v}`}>{v}</Link>150 </Button>151 ))}152 </div>153 }154 flush155 className="mt-6"156 >157 {events.length === 0 ? (158 <div className="p-4">159 <EmptyState icon={view === "open" ? ShieldCheck : ShieldAlert} title={view === "open" ? "No open abuse events" : "No abuse events"} description="Automatic detections (SSRF attempts, prohibited targets, rate abuse) and manual records appear here." compact />160 </div>161 ) : (162 <Table>163 <TableHeader>164 <TableRow>165 <TableHead>When</TableHead>166 <TableHead>Kind</TableHead>167 <TableHead>Severity</TableHead>168 <TableHead>Organization</TableHead>169 <TableHead>Project / request</TableHead>170 <TableHead>Detail</TableHead>171 <TableHead>Status</TableHead>172 <TableHead></TableHead>173 </TableRow>174 </TableHeader>175 <TableBody>176 {events.map((e) => (177 <TableRow key={e.id} className={e.resolved ? "opacity-70" : undefined}>178 <TableCell>179 <DateCell value={e.createdAt} />180 </TableCell>181 <TableCell>182 <Mono>{e.kind}</Mono>183 </TableCell>184 <TableCell>185 <SeverityBadge s={e.severity} />186 </TableCell>187 <TableCell>{e.organizationId ? <Link href={`/admin/organizations/${e.organizationId}`} className="hover:underline">{e.orgName ?? e.organizationId}</Link> : <span className="text-fg-subtle">—</span>}</TableCell>188 <TableCell>189 <div className="font-mono text-[11.5px] text-fg-muted">{e.projectId ?? "—"}</div>190 {e.requestId ? (191 <Link href={`/admin/requests/${e.requestId}`} className="font-mono text-[11.5px] hover:underline">192 {e.requestId}193 </Link>194 ) : null}195 </TableCell>196 <TableCell className="max-w-[360px] truncate text-[12.5px]" title={e.detail ?? undefined}>197 {e.detail ?? "—"}198 </TableCell>199 <TableCell>{e.resolved ? <Badge variant="success" dot>resolved</Badge> : <Badge variant="warning" dot>open</Badge>}</TableCell>200 <TableCell className="text-right">201 <ResolveAbuseButton id={e.id} resolved={e.resolved} />202 </TableCell>203 </TableRow>204 ))}205 </TableBody>206 </Table>207 )}208 </Panel>209 </>210 );211}212