SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
9.7 KB · 212 lines tsx
Raw Blame History
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 &gt; 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