Skip to content

Commit

Permalink
feat: error handling, loading states
Browse files Browse the repository at this point in the history
  • Loading branch information
JuroUhlar committed Dec 20, 2023
1 parent 3aa019c commit 3fda0c4
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 13 deletions.
15 changes: 10 additions & 5 deletions src/pages/api/bot-firewall/block-bot-ip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export type BlockIpPayload = {
requestId: string;
};

export type BlockIpResponse = {
result: 'success' | 'error';
message: string;
ip?: string;
blocked?: boolean;
};

export default async function blockIp(req: NextApiRequest, res: NextApiResponse) {
// This API route accepts only POST requests.
if (!ensurePostRequest(req, res)) {
Expand All @@ -24,7 +31,7 @@ export default async function blockIp(req: NextApiRequest, res: NextApiResponse)

const { okay, message } = await validateBlockIpRequest(requestId, ip, req);
if (!okay) {
return res.status(403).json({ severity: 'error', message });
return res.status(403).json({ result: 'error', message } satisfies BlockIpResponse);
}

try {
Expand All @@ -34,10 +41,10 @@ export default async function blockIp(req: NextApiRequest, res: NextApiResponse)
await deleteBlockedIp(ip);
}
await syncCloudflareBotFirewallRule();
return res.status(200).json({ message: 'OK' });
return res.status(200).json({ result: 'success', message: 'OK', ip, blocked } satisfies BlockIpResponse);
} catch (error) {
console.log(error);
return res.status(500).json({ severity: 'error', message: 'Internal server error.' });
return res.status(500).json({ result: 'error', message: 'Internal server error.' } satisfies BlockIpResponse);
}
}

Expand Down Expand Up @@ -85,12 +92,10 @@ const validateBlockIpRequest = async (
return { okay: false, message: "Visitor's IP does not match blocked IP." };
}

// Check if the visit origin matches the request origin
if (!originIsAllowed(identification.url, req)) {
return { okay: false, message: 'Visit origin does not match request origin.' };
}

// Check if the visit timestamp is not old
if (Date.now() - Number(new Date(identification.time)) > ALLOWED_REQUEST_TIMESTAMP_DIFF_MS) {
return { okay: false, message: 'Old visit, potential replay attack.' };
}
Expand Down
46 changes: 38 additions & 8 deletions src/pages/bot-firewall/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { useMutation, useQuery } from 'react-query';
import { BotIp } from '../../server/botd-firewall/saveBotVisit';
import Button from '../../client/components/common/Button/Button';
import styles from './botFirewall.module.scss';
import { BlockIpPayload } from '../api/bot-firewall/block-bot-ip';
import { BlockIpPayload, BlockIpResponse } from '../api/bot-firewall/block-bot-ip';
import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react';
import classnames from 'classnames';
import { enqueueSnackbar } from 'notistack';

const formatDate = (date: string) => {
const d = new Date(date);
Expand All @@ -19,38 +20,62 @@ const formatDate = (date: string) => {
};

export const BotFirewall: NextPage<CustomPageProps> = ({ embed }) => {
// Get visitor data from Fingerprint (just used for the visitor's IP address)
const { getData: getVisitorData, data: visitorData } = useVisitorData({
ignoreCache: true,
extendedResult: true,
});

const { data: botVisits, refetch: refetchBotVisits } = useQuery({
// Get a list of bot visits
const {
data: botVisits,
refetch: refetchBotVisits,
isLoading: isLoadingBotVisits,
} = useQuery({
queryKey: ['botVisits'],
queryFn: (): Promise<BotIp[]> => {
return fetch('/api/bot-firewall/get-bot-visits').then((res) => res.json());
},
});

// Get a list of currently blocked IP addresses
const { data: blockedIps, refetch: refetchBlockedIps } = useQuery({
queryKey: ['blockedIps'],
queryFn: (): Promise<BotIp[]> => {
return fetch('/api/bot-firewall/get-blocked-ips').then((res) => res.json());
},
});

// Post request mutation to block/unblock IP addresses
const { mutate: blockIp, isLoading: isLoadingBlockIp } = useMutation({
mutationFn: async ({ ip, blocked }: Omit<BlockIpPayload, 'requestId'>) => {
const { requestId } = await getVisitorData();
return fetch('/api/bot-firewall/block-bot-ip', {
const response = await fetch('/api/bot-firewall/block-bot-ip', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ip, blocked, requestId } satisfies BlockIpPayload),
});
if (response.ok) {
return await response.json();
} else {
const message = (await response.json()).message;
throw new Error('Failed to update firewall: ' + message ?? response.statusText);
}
},
onSuccess: () => {
onSuccess: async (data: BlockIpResponse) => {
refetchBlockedIps();
enqueueSnackbar(
<>
IP address <b>&nbsp;{data.ip}&nbsp;</b> was <b>&nbsp;{data.blocked ? 'blocked' : 'unblocked'}&nbsp;</b> in the
application firewall.{' '}
</>,
{ variant: 'success', autoHideDuration: 3000 },
);
},
onError: (error: Error) => {
enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 3000 });
},
});

Expand All @@ -59,7 +84,7 @@ export const BotFirewall: NextPage<CustomPageProps> = ({ embed }) => {
};

if (!botVisits) {
return null;
return <h3>Failed to fetch bot visits.</h3>;
}

return (
Expand All @@ -75,10 +100,11 @@ export const BotFirewall: NextPage<CustomPageProps> = ({ embed }) => {
refetchBlockedIps();
}}
className={styles.reloadButton}
disabled={isLoadingBotVisits}
>
Reload
{isLoadingBotVisits ? 'Loading bots visits ⏳' : 'Reload'}
</Button>
<i>Note: For the purposes of this demo,you can only block/unblock your own IP address ({visitorData?.ip})</i>
<i>Note: For the purposes of this demo, you can only block/unblock your own IP address ({visitorData?.ip})</i>
<table className={styles.ipsTable}>
<thead>
<th>Timestamp</th>
Expand All @@ -104,7 +130,11 @@ export const BotFirewall: NextPage<CustomPageProps> = ({ embed }) => {
onClick={() => blockIp({ ip: botVisit?.ip, blocked: !isIpBlocked(botVisit?.ip) })}
disabled={isLoadingBlockIp}
>
{isIpBlocked(botVisit?.ip) ? 'Unblock' : 'Block this IP'}
{isLoadingBlockIp
? 'Working on it ⏳'
: isIpBlocked(botVisit?.ip)
? 'Unblock'
: 'Block this IP'}
</Button>
) : (
<>-</>
Expand Down

0 comments on commit 3fda0c4

Please sign in to comment.