Files
rdap/src/pages/index.tsx
Ryan Walters 13fc05226d feat: add TLD validation with real-time warnings
Implement comprehensive TLD validation that checks domain inputs against
the IANA TLD list and RDAP registry to provide context-aware error
messages and real-time UI warnings.

Key changes:
- Add tld-validation service that validates TLDs against IANA list and
  RDAP registry availability
- Show warning icons in LookupInput for invalid TLDs (error) and valid
  TLDs without RDAP support (warning) with explanatory tooltips
- Enhance error messages in getRegistryURL to differentiate between
  invalid TLDs, valid TLDs without RDAP, and malformed domains
- Add NotFoundError class for better 404 handling with context-specific
  messages per target type (domain, IP, ASN, entity, TLD)
- Make getRegistryURL async to support TLD validation in error path
- Add comprehensive test coverage for TLD validation and error messages
- Track TLD warning displays in telemetry

This improves UX by catching common mistakes (typos, non-existent TLDs,
reserved TLDs) before query execution and providing clear guidance.
2025-11-06 15:40:28 -06:00

196 lines
5.5 KiB
TypeScript

import { type NextPage } from "next";
import Head from "next/head";
import { useState, useEffect, useCallback } from "react";
import { useRouter } from "next/router";
import RdapObjectRouter from "@/rdap/components/RdapObjectRouter";
import type { MetaParsedGeneric } from "@/rdap/hooks/useLookup";
import useLookup from "@/rdap/hooks/useLookup";
import LookupInput from "@/rdap/components/LookupInput";
import ErrorCard from "@/components/ErrorCard";
import { ThemeToggle } from "@/components/ThemeToggle";
import { Maybe } from "true-myth";
import { Flex, Container, Section, Text, Link, IconButton } from "@radix-ui/themes";
import { GitHubLogoIcon } from "@radix-ui/react-icons";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import { serializeQueryToUrl, deserializeUrlToQuery, buildShareableUrl } from "@/lib/urls";
import type { TargetType } from "@/rdap/schemas";
const Index: NextPage = () => {
const router = useRouter();
const [response, setResponse] = useState<Maybe<MetaParsedGeneric>>(Maybe.nothing());
const [isLoading, setLoading] = useState<boolean>(false);
// URL update handler for useLookup hook
const handleUrlUpdate = useCallback(
(target: string, manuallySelectedType: TargetType | null) => {
const queryString = serializeQueryToUrl(target, manuallySelectedType);
// Use shallow routing to update URL without page reload
router.push(queryString, undefined, { shallow: true });
},
[router]
);
const {
error,
target,
setTarget,
setTargetType,
submit,
currentType,
manualType,
tldValidation,
} = useLookup(undefined, handleUrlUpdate);
// Parse URL parameters on mount and auto-execute query if present
useEffect(() => {
// Only run once on mount, when router is ready
if (!router.isReady) return;
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
const queryState = deserializeUrlToQuery(searchParams);
if (queryState) {
// Set the target and type from URL
setTarget(queryState.query);
if (queryState.type) {
setTargetType(queryState.type);
}
// Auto-execute the query
setLoading(true);
submit({
target: queryState.query,
requestJSContact: true,
followReferral: true,
})
.then(setResponse)
.catch((e) => {
console.error("Error executing query from URL:", e);
setResponse(Maybe.nothing());
})
.finally(() => setLoading(false));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router.isReady]); // Only run when router becomes ready
return (
<>
<Head>
<title>rdap.xevion.dev</title>
<meta
name="description"
content="A custom, private RDAP lookup client built by Xevion."
/>
<meta property="og:url" content="https://rdap.xevion.dev" />
<meta property="og:title" content="RDAP | by Xevion.dev" />
<meta
property="og:description"
content="A custom, private RDAP lookup client built by Xevion."
/>
<meta property="og:site_name" content="rdap.xevion.dev" />
<meta property="og:type" content="website" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="keywords"
content="xevion, rdap, whois, rdap, domain name, dns, ip address"
/>
</Head>
<OverlayScrollbarsComponent
defer
options={{
scrollbars: {
autoHide: "leave",
autoHideDelay: 1300,
},
}}
style={{ height: "100vh" }}
>
<Flex
asChild
justify="between"
align="center"
px="5"
py="4"
style={{
borderBottom: "1px solid var(--gray-a5)",
}}
>
<nav>
<Text size="5" weight="medium">
<Link href="https://github.com/Xevion/rdap" color="gray" highContrast>
rdap
</Link>
<Link href="https://xevion.dev" color="gray">
.xevion.dev
</Link>
</Text>
<Flex gap="4" align="center">
<IconButton
asChild
size="3"
variant="ghost"
aria-label="View on GitHub"
title="View on GitHub"
>
<a
href="https://github.com/Xevion/rdap"
target="_blank"
rel="noopener noreferrer"
>
<GitHubLogoIcon width="22" height="22" />
</a>
</IconButton>
<ThemeToggle />
</Flex>
</nav>
</Flex>
<Container size="3" px="5">
<Section size="2">
<LookupInput
isLoading={isLoading}
detectedType={currentType}
tldValidation={tldValidation}
shareableUrl={
response.isJust && target && typeof window !== "undefined"
? buildShareableUrl(window.location.origin, target, manualType)
: undefined
}
onChange={({ target, targetType }) => {
setTarget(target);
setTargetType(targetType);
}}
onSubmit={async function (props) {
try {
setLoading(true);
setResponse(await submit(props));
setLoading(false);
} catch (e) {
console.error(e);
setResponse(Maybe.nothing());
setLoading(false);
}
}}
/>
{error != null ? (
<ErrorCard
title="An error occurred while performing a lookup."
description={error}
className="mb-2"
/>
) : null}
{response.isJust ? (
<RdapObjectRouter
url={response.value.url}
data={response.value.data}
queryTimestamp={response.value.completeTime}
/>
) : null}
</Section>
</Container>
</OverlayScrollbarsComponent>
</>
);
};
export default Index;