diff --git a/src/App.tsx b/src/App.tsx index 1d8660c..53432b9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { Route, Routes } from 'react-router-dom'; import Styled from 'styled-components'; import Home from 'pages/Home'; import Results from 'pages/Results'; +import About from 'pages/About'; import colors from 'styles/colors'; const Container = Styled.main` @@ -18,6 +19,7 @@ function App() { } /> } /> + } /> ); diff --git a/src/components/Form/Heading.tsx b/src/components/Form/Heading.tsx index ddc3d35..a584d29 100644 --- a/src/components/Form/Heading.tsx +++ b/src/components/Form/Heading.tsx @@ -9,6 +9,7 @@ interface HeadingProps { size?: 'xSmall' | 'small' | 'medium' | 'large'; inline?: boolean; children: React.ReactNode; + id?: string; }; const StyledHeading = styled.h1` @@ -46,9 +47,9 @@ const StyledHeading = styled.h1` `; const Heading = (props: HeadingProps): JSX.Element => { - const { children, as, size, align, color, inline } = props; + const { children, as, size, align, color, inline, id } = props; return ( - + {children} ); diff --git a/src/components/Form/Modal.tsx b/src/components/Form/Modal.tsx index 8f9f29b..6c9bbfd 100644 --- a/src/components/Form/Modal.tsx +++ b/src/components/Form/Modal.tsx @@ -43,6 +43,9 @@ const ModalWindow = styled.div` 0% {opacity: 0; transform: scale(0.9);} 100% {opacity: 1; transform: scale(1);} } + pre { + white-space: break-spaces; + } `; const Modal: React.FC = ({ children, isOpen, closeModal }) => { diff --git a/src/components/Form/Nav.tsx b/src/components/Form/Nav.tsx new file mode 100644 index 0000000..c518503 --- /dev/null +++ b/src/components/Form/Nav.tsx @@ -0,0 +1,30 @@ +import styled from 'styled-components'; + +import { StyledCard } from 'components/Form/Card'; +import Heading from 'components/Form/Heading'; +import colors from 'styles/colors'; +import { ReactNode } from 'react'; + +const Header = styled(StyledCard)` + margin: 1rem; + display: flex; + flex-wrap: wrap; + align-items: baseline; + justify-content: space-between; + padding: 0.5rem 1rem; + align-items: center; +`; + +const Nav = (props: { children?: ReactNode}) => { + return ( +
+ + Web Check Icon + Web Check + + {props.children && props.children} +
+ ); +}; + +export default Nav; diff --git a/src/components/Results/DnsRecords.tsx b/src/components/Results/DnsRecords.tsx index 6961325..b752a5c 100644 --- a/src/components/Results/DnsRecords.tsx +++ b/src/components/Results/DnsRecords.tsx @@ -2,8 +2,10 @@ import { Card } from 'components/Form/Card'; import Row, { ListRow } from 'components/Form/Row'; const styles = ` + grid-row: span 2; .content { - max-height: 32rem; + max-height: 50rem; + overflow-x: hidden; overflow-y: auto; } `; diff --git a/src/components/Results/Screenshot.tsx b/src/components/Results/Screenshot.tsx index cff4fef..585c134 100644 --- a/src/components/Results/Screenshot.tsx +++ b/src/components/Results/Screenshot.tsx @@ -2,7 +2,8 @@ import { Card } from 'components/Form/Card'; const cardStyles = ` overflow: auto; - max-height: 32rem; + max-height: 40rem; + grid-row: span 2; img { border-radius: 6px; width: 100%; diff --git a/src/components/Results/ServerLocation.tsx b/src/components/Results/ServerLocation.tsx index 6d1e9d2..a3e0e58 100644 --- a/src/components/Results/ServerLocation.tsx +++ b/src/components/Results/ServerLocation.tsx @@ -7,7 +7,7 @@ import Flag from 'components/misc/Flag'; import { TextSizes } from 'styles/typography'; import Row, { StyledRow } from 'components/Form/Row'; -const cardStyles = 'grid-row: span 2'; +const cardStyles = ''; const SmallText = styled.span` opacity: 0.5; diff --git a/src/components/Results/SiteFeatures.tsx b/src/components/Results/SiteFeatures.tsx index 5cce10b..b69474e 100644 --- a/src/components/Results/SiteFeatures.tsx +++ b/src/components/Results/SiteFeatures.tsx @@ -1,6 +1,6 @@ import { Card } from 'components/Form/Card'; import colors from 'styles/colors'; -import Row, { ListRow } from 'components/Form/Row'; +import Row from 'components/Form/Row'; import Heading from 'components/Form/Heading'; const styles = ` @@ -17,7 +17,12 @@ const styles = ` `; const formatDate = (timestamp: number): string => { + if (isNaN(timestamp) || timestamp <= 0) return 'No Date'; + const date = new Date(timestamp * 1000); + + if (isNaN(date.getTime())) return 'Unknown'; + const formatter = new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', @@ -26,16 +31,18 @@ const formatDate = (timestamp: number): string => { minute: '2-digit', hour12: true }); + return formatter.format(date); } + const SiteFeaturesCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => { const features = props.data; return (
- { features.groups.filter((group: any) => group.categories.length > 0).map((group: any, index: number) => ( + { (features?.groups || []).filter((group: any) => group.categories.length > 0).map((group: any, index: number) => (
{group.name} { group.categories.map((category: any, subIndex: number) => ( diff --git a/src/components/misc/ProgressBar.tsx b/src/components/misc/ProgressBar.tsx index 7730a34..5a15a5b 100644 --- a/src/components/misc/ProgressBar.tsx +++ b/src/components/misc/ProgressBar.tsx @@ -83,6 +83,10 @@ const StatusInfoWrapper = styled.div` } `; +const AboutPageLink = styled.a` + color: ${colors.primary}; +`; + const SummaryContainer = styled.div` margin: 0.5rem 0; b { @@ -178,24 +182,24 @@ export interface LoadingJob { const jobNames = [ 'get-ip', + 'location', 'ssl', 'dns', - 'cookies', - 'robots-txt', - 'headers', - 'lighthouse', - 'location', + 'whois', 'hosts', + 'lighthouse', + 'cookies', + 'trace-route', + 'server-info', 'redirects', - 'txt-records', + 'robots-txt', + 'dnssec', 'status', 'ports', - 'trace-route', - 'carbon', - 'server-info', - 'whois', + 'txt-records', 'features', - 'dnssec', + 'carbon', + 'headers', ] as const; export const initialJobs = jobNames.map((job: string) => { @@ -283,7 +287,7 @@ const SummaryText = (props: { state: LoadingJob[], count: number }): JSX.Element if (loadingTasksCount > 0) { return ( - Loading {loadingTasksCount} / {totalJobs} Jobs + Loading {totalJobs - loadingTasksCount} / {totalJobs} Jobs {skippedInfo} ); @@ -409,6 +413,7 @@ const ProgressLoader = (props: { loadStatus: LoadingJob[], showModal: (err: Reac It's normal for some jobs to fail, either because the host doesn't return the required info, or restrictions in the lambda function, or hitting an API limit.

} + Learn More about Web-Check setHideLoader(true)}>Dismiss diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index ad9130a..746216e 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -117,7 +117,7 @@ const Home = (): JSX.Element => { disabled={inputDisabled} handleChange={inputChange} /> - Or, find my IP + {/* Or, find my IP */} { errorMsg && {errorMsg}} diff --git a/src/pages/Results.tsx b/src/pages/Results.tsx index 73e385b..23b534a 100644 --- a/src/pages/Results.tsx +++ b/src/pages/Results.tsx @@ -8,6 +8,7 @@ import Heading from 'components/Form/Heading'; import Card from 'components/Form/Card'; import Modal from 'components/Form/Modal'; import Footer from 'components/misc/Footer'; +import Nav from 'components/Form/Nav'; import { RowProps } from 'components/Form/Row'; import ErrorBoundary from 'components/misc/ErrorBoundary'; import docs from 'utils/docs'; @@ -67,15 +68,6 @@ const ResultsContent = styled.section` padding-bottom: 1rem; `; -const Header = styled(Card)` - margin: 1rem; - display: flex; - flex-wrap: wrap; - align-items: baseline; - justify-content: space-between; - padding: 0.5rem 1rem; -`; - const JobDocsContainer = styled.div` p.doc-desc, p.doc-uses, ul { margin: 0.25rem auto 1.5rem auto; @@ -148,6 +140,37 @@ const Results = (): JSX.Element => { }); }, []); + const parseJson = (response: Response): Promise => { + return new Promise((resolve) => { + if (response.ok) { + response.json() + .then(data => resolve(data)) + .catch(error => resolve( + { error: `Failed to process response, likely due to Netlify's 10-sec limit on lambda functions. Error: ${error}`} + )); + } else { + resolve( + { error: `Response returned with status: ${response.status} ${response.statusText}.` + + `This is likely due to an incompatibility with the lambda function.` } + ); + } + }); + }; + + + + // const parseJson = (response: Response): Promise => { + // if (response.status >= 400) { + // return new Promise((resolve) => resolve({ error: `Failed to fetch data: ${response.statusText}` })); + // } + // return new Promise((resolve) => { + // if (!response) { resolve({ error: 'No response from server' }); } + // response.json() + // .catch(error => resolve({ error: `Failed to process response, likely due to Netlify's 10-sec limit on lambda functions. Error: ${error}`})); + // }); + // }; + + useEffect(() => { if (!addressType || addressType === 'empt') { setAddressType(determineAddressType(address || '')); @@ -165,7 +188,7 @@ const Results = (): JSX.Element => { updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/find-url-ip?address=${address}`) - .then(res => res.json()) + .then(res => parseJson(res)) .then(res => res.ip), }); @@ -174,7 +197,7 @@ const Results = (): JSX.Element => { jobId: 'ssl', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`/ssl-check?url=${address}`).then((res) => res.json()), + fetchRequest: () => fetch(`/ssl-check?url=${address}`).then((res) => parseJson(res)), }); // Fetch and parse cookies info @@ -183,7 +206,7 @@ const Results = (): JSX.Element => { updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/get-cookies?url=${address}`) - .then(res => res.json()) + .then(res => parseJson(res)) .then(res => parseCookies(res.cookies)), }); @@ -202,7 +225,7 @@ const Results = (): JSX.Element => { jobId: 'headers', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`/get-headers?url=${address}`).then(res => res.json()), + fetchRequest: () => fetch(`/get-headers?url=${address}`).then(res => parseJson(res)), }); // Fetch and parse DNS records @@ -210,7 +233,7 @@ const Results = (): JSX.Element => { jobId: 'dns', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`/get-dns?url=${address}`).then(res => res.json()), + fetchRequest: () => fetch(`/get-dns?url=${address}`).then(res => parseJson(res)), }); // Fetch and parse Lighthouse performance data @@ -219,8 +242,8 @@ const Results = (): JSX.Element => { updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`/lighthouse-report?url=${address}`) - .then(res => res.json()) - .then(res => res.lighthouseResult), + .then(res => parseJson(res)) + .then(res => res?.lighthouseResult || { error: 'No Data'}), }); // Get IP address location info @@ -229,7 +252,7 @@ const Results = (): JSX.Element => { updateLoadingJobs, addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, fetchRequest: () => fetch(`https://ipapi.co/${ipAddress}/json/`) - .then(res => res.json()) + .then(res => parseJson(res)) .then(res => getLocation(res)), }); @@ -240,7 +263,7 @@ const Results = (): JSX.Element => { updateLoadingJobs, addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, fetchRequest: () => fetch(`https://api.shodan.io/shodan/host/${ipAddress}?key=${keys.shodan}`) - .then(res => res.json()) + .then(res => parseJson(res)) .then(res => parseShodanResults(res)), }); @@ -251,7 +274,7 @@ const Results = (): JSX.Element => { updateLoadingJobs, addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, fetchRequest: () => fetch(`/check-ports?url=${ipAddress}`) - .then(res => res.json()), + .then(res => parseJson(res)), }); // Fetch and parse domain whois results @@ -260,7 +283,7 @@ const Results = (): JSX.Element => { updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, fetchRequest: () => fetch(`https://api.whoapi.com/?domain=${address}&r=whois&apikey=${keys.whoApi}`) - .then(res => res.json()) + .then(res => parseJson(res)) .then(res => applyWhoIsResults(res)), }); @@ -270,7 +293,7 @@ const Results = (): JSX.Element => { // updateLoadingJobs, // addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, // fetchRequest: () => fetch(`https://api.builtwith.com/v21/api.json?KEY=${keys.builtWith}&LOOKUP=${address}`) - // .then(res => res.json()) + // .then(res => parseJson(res)) // .then(res => makeTechnologies(res)), // }); @@ -279,7 +302,7 @@ const Results = (): JSX.Element => { jobId: 'txt-records', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`/get-txt?url=${address}`).then(res => res.json()), + fetchRequest: () => fetch(`/get-txt?url=${address}`).then(res => parseJson(res)), }); // Fetches URL redirects @@ -287,7 +310,7 @@ const Results = (): JSX.Element => { jobId: 'redirects', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`/follow-redirects?url=${address}`).then(res => res.json()), + fetchRequest: () => fetch(`/follow-redirects?url=${address}`).then(res => parseJson(res)), }); // Get current status and response time of server @@ -295,7 +318,7 @@ const Results = (): JSX.Element => { jobId: 'status', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`/server-status?url=${address}`).then(res => res.json()), + fetchRequest: () => fetch(`/server-status?url=${address}`).then(res => parseJson(res)), }); // Get trace route for a given hostname @@ -303,7 +326,7 @@ const Results = (): JSX.Element => { jobId: 'trace-route', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`/trace-route?url=${address}`).then(res => res.json()), + fetchRequest: () => fetch(`/trace-route?url=${address}`).then(res => parseJson(res)), }); // Fetch carbon footprint data for a given site @@ -311,7 +334,7 @@ const Results = (): JSX.Element => { jobId: 'carbon', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`/get-carbon?url=${address}`).then(res => res.json()), + fetchRequest: () => fetch(`/get-carbon?url=${address}`).then(res => parseJson(res)), }); // Get site features from BuiltWith @@ -319,7 +342,14 @@ const Results = (): JSX.Element => { jobId: 'features', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`/site-features?url=${address}`).then(res => res.json()), + fetchRequest: () => fetch(`/site-features?url=${address}`) + .then(res => parseJson(res)) + .then(res => { + if (res.Errors && res.Errors.length > 0) { + return { error: `No data returned, because ${res.Errors[0].Message || 'API lookup failed'}` }; + } + return res; + }), }); // Get DNSSEC info @@ -327,7 +357,7 @@ const Results = (): JSX.Element => { jobId: 'dnssec', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`/dns-sec?url=${address}`).then(res => res.json()), + fetchRequest: () => fetch(`/dns-sec?url=${address}`).then(res => parseJson(res)), }); /* Cancel remaining jobs after 10 second timeout */ @@ -357,24 +387,23 @@ const Results = (): JSX.Element => { const resultCardData = [ { id: 'location', title: 'Server Location', result: locationResults, Component: ServerLocationCard, refresh: updateLocationResults }, { id: 'ssl', title: 'SSL Info', result: sslResults, Component: SslCertCard, refresh: updateSslResults }, - { id: 'dns', title: 'Headers', result: headersResults, Component: HeadersCard, refresh: updateHeadersResults }, - { id: 'hosts', title: 'Host Names', result: shoadnResults?.hostnames, Component: HostNamesCard, refresh: updateShodanResults }, + { id: 'headers', title: 'Headers', result: headersResults, Component: HeadersCard, refresh: updateHeadersResults }, { id: 'whois', title: 'Domain Info', result: whoIsResults, Component: WhoIsCard, refresh: updateWhoIsResults }, { id: 'dns', title: 'DNS Records', result: dnsResults, Component: DnsRecordsCard, refresh: updateDnsResults }, + { id: 'hosts', title: 'Host Names', result: shoadnResults?.hostnames, Component: HostNamesCard, refresh: updateShodanResults }, { id: 'lighthouse', title: 'Performance', result: lighthouseResults, Component: LighthouseCard, refresh: updateLighthouseResults }, { id: 'cookies', title: 'Cookies', result: cookieResults, Component: CookiesCard, refresh: updateCookieResults }, { id: 'trace-route', title: 'Trace Route', result: traceRouteResults, Component: TraceRouteCard, refresh: updateTraceRouteResults }, - { id: '', title: 'Screenshot', result: lighthouseResults?.fullPageScreenshot?.screenshot, Component: ScreenshotCard, refresh: updateLighthouseResults }, - // { title: 'Technologies', result: technologyResults, Component: BuiltWithCard, refresh: updateTechnologyResults }, - { id: 'robots-txt', title: 'Crawl Rules', result: robotsTxtResults, Component: RobotsTxtCard, refresh: updateRobotsTxtResults }, { id: 'server-info', title: 'Server Info', result: shoadnResults?.serverInfo, Component: ServerInfoCard, refresh: updateShodanResults }, { id: 'redirects', title: 'Redirects', result: redirectResults, Component: RedirectsCard, refresh: updateRedirectResults }, - { id: 'txt-records', title: 'TXT Records', result: txtRecordResults, Component: TxtRecordCard, refresh: updateTxtRecordResults }, + { id: 'robots-txt', title: 'Crawl Rules', result: robotsTxtResults, Component: RobotsTxtCard, refresh: updateRobotsTxtResults }, + { id: 'dnssec', title: 'DNSSEC', result: dnsSecResults, Component: DnsSecCard, refresh: updateDnsSecResults }, { id: 'status', title: 'Server Status', result: serverStatusResults, Component: ServerStatusCard, refresh: updateServerStatusResults }, { id: 'ports', title: 'Open Ports', result: portsResults, Component: OpenPortsCard, refresh: updatePortsResults }, - { id: 'carbon', title: 'Carbon Footprint', result: carbonResults, Component: CarbonFootprintCard, refresh: updateCarbonResults }, + { id: 'screenshot', title: 'Screenshot', result: lighthouseResults?.fullPageScreenshot?.screenshot, Component: ScreenshotCard, refresh: updateLighthouseResults }, + { id: 'txt-records', title: 'TXT Records', result: txtRecordResults, Component: TxtRecordCard, refresh: updateTxtRecordResults }, { id: 'features', title: 'Site Features', result: siteFeaturesResults, Component: SiteFeaturesCard, refresh: updateSiteFeaturesResults }, - { id: 'dnssec', title: 'DNSSEC', result: dnsSecResults, Component: DnsSecCard, refresh: updateDnsSecResults }, + { id: 'carbon', title: 'Carbon Footprint', result: carbonResults, Component: CarbonFootprintCard, refresh: updateCarbonResults }, ]; const MakeActionButtons = (title: string, refresh: () => void, showInfo: (id: string) => void): ReactNode => { @@ -418,18 +447,14 @@ const Results = (): JSX.Element => { return ( -
- - Web Check Icon - Web Check - - { address && +
+