Skip to content

Commit

Permalink
Load WHOIS preview through API route 👀
Browse files Browse the repository at this point in the history
  • Loading branch information
wotschofsky committed Dec 29, 2023
1 parent cf1a7c5 commit 5d6b721
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 79 deletions.
4 changes: 4 additions & 0 deletions app/lookup/[domain]/certs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type CertsData = {
serial_number: string;
}[];

export const runtime = 'edge';
// crt.sh located in GB, always use LHR1 for lowest latency
export const preferredRegion = 'lhr1';

const lookupCerts = async (domain: string): Promise<CertsData> => {
const response = await fetch(
'https://crt.sh?' +
Expand Down
11 changes: 3 additions & 8 deletions app/lookup/[domain]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { ExternalLinkIcon } from 'lucide-react';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { type FC, type ReactNode, Suspense } from 'react';
import { type FC, type ReactNode } from 'react';

import RelatedDomains from '@/components/RelatedDomains';
import ResultsTabs from '@/components/ResultsTabs';
import SearchForm from '@/components/SearchForm';
import WhoisQuickInfo, {
WhoisQuickInfoPlaceholder,
} from '@/components/WhoisQuickInfo';
import WhoisQuickInfo from '@/components/WhoisQuickInfo';
import isValidDomain from '@/utils/isValidDomain';

type LookupLayoutProps = {
Expand Down Expand Up @@ -67,10 +65,7 @@ const LookupLayout: FC<LookupLayoutProps> = ({
</h1>

<RelatedDomains domain={domain} />
<Suspense fallback={<WhoisQuickInfoPlaceholder />}>
{/* TODO Add error boundary */}
<WhoisQuickInfo domain={domain} />
</Suspense>
<WhoisQuickInfo domain={domain} />
<ResultsTabs domain={domain} />

{children}
Expand Down
87 changes: 87 additions & 0 deletions app/lookup/[domain]/whois-summary/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import whoiser, { WhoisSearchResult } from 'whoiser';

import isValidDomain from '@/utils/isValidDomain';

export type WhoisSummaryResponse = {
registrar: string | null;
createdAt: string | null;
dnssec: string | null;
};

export type WhoisSummaryErrorResponse = { error: true; message: string };

const getSummary = async (domain: string): Promise<WhoisSummaryResponse> => {
// TODO Allow resolving for TLDs
if (!isValidDomain(domain)) {
return {
registrar: null,
createdAt: null,
dnssec: null,
};
}

try {
const results = await whoiser(domain, {
timeout: 5000,
});

const resultsKey = Object.keys(results).find(
// @ts-expect-error
(key) => !('error' in results[key])
);
if (!resultsKey) {
throw new Error('No valid results found for domain ' + domain);
}
const firstResult = results[resultsKey] as WhoisSearchResult;

return {
registrar: firstResult['Registrar']?.toString(),
createdAt:
firstResult && 'Created Date' in firstResult
? new Date(firstResult['Created Date'].toString()).toLocaleDateString(
'en-US'
)
: null,
dnssec: firstResult['DNSSEC']?.toString(),
};
} catch (error) {
console.error(error);
return {
registrar: null,
createdAt: null,
dnssec: null,
};
}
};

export async function GET(
_request: Request,
{ params }: { params: { domain: string } }
) {
if (!params.domain) {
return Response.json(
{ error: true, message: 'No domain provided' },
{
status: 400,
headers: {
'Content-Type': 'application/json',
},
}
);
}

try {
const summary = await getSummary(params.domain);
return Response.json(summary);
} catch (error) {
return Response.json(
{ error: true, message: 'Error fetching whois summary' },
{
status: 500,
headers: {
'Content-Type': 'application/json',
},
}
);
}
}
95 changes: 24 additions & 71 deletions components/WhoisQuickInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,49 @@
import { FC } from 'react';
import whoiser, { type WhoisSearchResult } from 'whoiser';
'use client';

import isValidDomain from '@/utils/isValidDomain';
import type { FC } from 'react';
import useSWR from 'swr';

import { WhoisSummaryResponse } from '@/app/lookup/[domain]/whois-summary/route';

import { Skeleton } from './ui/skeleton';

type WhoisQuickInfoProps = {
domain: string;
};

const getSummary = async (
domain: string
): Promise<{ registrar: string; createdAt: string; dnssec: string }> => {
// TODO Allow resolving for TLDs
if (!isValidDomain(domain)) {
return {
registrar: 'Unavailable',
createdAt: 'Unavailable',
dnssec: 'Unavailable',
};
}

try {
const results = await whoiser(domain, {
timeout: 5000,
});

const resultsKey = Object.keys(results).find(
// @ts-expect-error
(key) => !('error' in results[key])
);
if (!resultsKey) {
throw new Error('No valid results found for domain ' + domain);
}
const firstResult = results[resultsKey] as WhoisSearchResult;

return {
registrar: firstResult['Registrar']?.toString() || 'Unavailable',
createdAt:
firstResult && 'Created Date' in firstResult
? new Date(firstResult['Created Date'].toString()).toLocaleDateString(
'en-US'
)
: 'Unavailable',
dnssec: firstResult['DNSSEC']?.toString() || 'Unavailable',
};
} catch (error) {
console.error(error);
return {
registrar: 'Unavailable',
createdAt: 'Unavailable',
dnssec: 'Unavailable',
};
}
};

const WhoisQuickInfo: FC<WhoisQuickInfoProps> = async ({ domain }) => {
const results = await getSummary(domain);
const WhoisQuickInfo: FC<WhoisQuickInfoProps> = ({ domain }) => {
const { data, isLoading } = useSWR<WhoisSummaryResponse>(
`/lookup/${domain}/whois-summary`
);

return (
<div className="my-8 flex gap-8">
<div>
<h3 className="text-xs text-muted-foreground">Registrar</h3>
<p className="text-sm">{results.registrar}</p>
{isLoading ? (
<Skeleton className="mt-1 h-4 w-24 rounded-sm" />
) : (
<p className="text-sm">{data?.registrar || 'Unavailable'}</p>
)}
</div>
<div>
<h3 className="text-xs text-muted-foreground">Creation Date</h3>
<p className="text-sm">{results.createdAt}</p>
{isLoading ? (
<Skeleton className="w-18 mt-1 h-4 rounded-sm" />
) : (
<p className="text-sm">{data?.createdAt || 'Unavailable'}</p>
)}
</div>
<div>
<h3 className="text-xs text-muted-foreground">DNSSEC</h3>
<p className="text-sm">{results.dnssec}</p>
{isLoading ? (
<Skeleton className="mt-1 h-4 w-16 rounded-sm" />
) : (
<p className="text-sm">{data?.dnssec || 'Unavailable'}</p>
)}
</div>
</div>
);
};

export default WhoisQuickInfo;

export const WhoisQuickInfoPlaceholder: FC = () => (
<div className="my-8 flex gap-8">
<div>
<h3 className="text-xs text-muted-foreground">Registrar</h3>
<Skeleton className="mt-1 h-4 w-24 rounded-sm" />
</div>
<div>
<h3 className="text-xs text-muted-foreground">Creation Date</h3>
<Skeleton className="w-18 mt-1 h-4 rounded-sm" />
</div>
<div>
<h3 className="text-xs text-muted-foreground">DNSSEC</h3>
<Skeleton className="mt-1 h-4 w-16 rounded-sm" />
</div>
</div>
);

1 comment on commit 5d6b721

@vercel
Copy link

@vercel vercel bot commented on 5d6b721 Dec 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.