Files
t5de-hub/apps/web/app/console/blocks/page.tsx
T
dhkatzandClaude Opus 5.5 ba423f32d8
Pull request / title (pull_request) Successful in 1s
CI / test (pull_request) Successful in 1m8s
CI / image (apps/api/Dockerfile, t5de-hub-api) (pull_request) Skipped
CI / image (apps/web/Dockerfile, t5de-hub-web) (pull_request) Skipped
feat: file blocks
Moderators block a file network-wide by its sha256, from a product's files or the console's new Blocks tab, which
first shows the products using it. Blocking removes those products (their creators are told and may appeal), puts
a `block` change in the changes feed of every catalog hosting it, and refuses uploads with it; moderators see who
tried. Unblocking lets it in again; the products it removed stay removed until restored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 14:32:08 -08:00

89 lines
3.6 KiB
TypeScript

import type { Metadata } from "next";
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import { PageHeader } from "@/components/status";
import { DateTime } from "@/components/time";
import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { api, unwrap } from "@/lib/api";
import { ConsoleNav, requireConsole } from "../console-nav";
export const metadata: Metadata = { title: "Blocks" };
export default async function BlocksPage({ searchParams }: { searchParams: Promise<Record<string, string | string[]>> }) {
const { console: c } = await requireConsole("/console/blocks");
if (!c.sections.includes("blocks")) notFound();
const params = await searchParams;
// Looking a file up goes to its page.
if (typeof params.sha256 === "string") {
const sha256 = params.sha256.trim().toLowerCase();
if (/^[0-9a-f]{64}$/.test(sha256)) redirect(`/console/blocks/${sha256}`);
}
const before = typeof params.before === "string" && !Number.isNaN(Date.parse(params.before)) ? params.before : undefined;
const data = unwrap(await api.blocks.get({ query: before ? { before } : {} }));
return (
<>
<PageHeader title="Blocks">Blocked files: uploads with them are refused, and catalogs delete them.</PageHeader>
<ConsoleNav />
<form action="/console/blocks" className="mb-6 flex flex-wrap gap-2">
<Input
name="sha256"
required
pattern="\s*[0-9a-fA-F]{64}\s*"
defaultValue={typeof params.sha256 === "string" ? params.sha256 : ""}
placeholder="A file's sha256"
aria-label="sha256"
className="max-w-xl font-mono"
/>
<Button type="submit">Look up</Button>
</form>
<Card>
<CardContent>
{data.blocks.length === 0 ? (
<p className="text-sm text-muted-foreground">
Nothing blocked. Block a file from a product&apos;s files, or look it up above.
</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>File</TableHead>
<TableHead>Reason</TableHead>
<TableHead>By</TableHead>
<TableHead>When</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.blocks.map((b) => (
<TableRow key={b.sha256}>
<TableCell className="font-mono text-xs">
<Link href={`/console/blocks/${b.sha256}`} className="underline-offset-4 hover:underline">
{b.sha256.slice(0, 16)}…
</Link>
</TableCell>
<TableCell className="whitespace-normal">{b.reason}</TableCell>
<TableCell className="text-muted-foreground">{b.by ?? ""}</TableCell>
<TableCell className="text-muted-foreground">
<DateTime iso={b.at} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
{data.before && (
<div className="mt-4">
<Link href={`/console/blocks?before=${encodeURIComponent(data.before)}`} className={buttonVariants({ variant: "outline" })}>
Older
</Link>
</div>
)}
</>
);
}