diff --git a/src/pages/api/bot-firewall/block-bot-ip.ts b/src/pages/api/bot-firewall/block-bot-ip.ts index a2bb9bdb..f9079fca 100644 --- a/src/pages/api/bot-firewall/block-bot-ip.ts +++ b/src/pages/api/bot-firewall/block-bot-ip.ts @@ -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)) { @@ -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 { @@ -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); } } @@ -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.' }; } diff --git a/src/pages/bot-firewall/index.tsx b/src/pages/bot-firewall/index.tsx index fc34ec13..2a343375 100644 --- a/src/pages/bot-firewall/index.tsx +++ b/src/pages/bot-firewall/index.tsx @@ -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); @@ -19,18 +20,25 @@ const formatDate = (date: string) => { }; export const BotFirewall: NextPage = ({ 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 => { 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 => { @@ -38,19 +46,36 @@ export const BotFirewall: NextPage = ({ embed }) => { }, }); + // Post request mutation to block/unblock IP addresses const { mutate: blockIp, isLoading: isLoadingBlockIp } = useMutation({ mutationFn: async ({ ip, blocked }: Omit) => { 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  {data.ip}  was  {data.blocked ? 'blocked' : 'unblocked'}  in the + application firewall.{' '} + , + { variant: 'success', autoHideDuration: 3000 }, + ); + }, + onError: (error: Error) => { + enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 3000 }); }, }); @@ -59,7 +84,7 @@ export const BotFirewall: NextPage = ({ embed }) => { }; if (!botVisits) { - return null; + return

Failed to fetch bot visits.

; } return ( @@ -75,10 +100,11 @@ export const BotFirewall: NextPage = ({ embed }) => { refetchBlockedIps(); }} className={styles.reloadButton} + disabled={isLoadingBotVisits} > - Reload + {isLoadingBotVisits ? 'Loading bots visits ⏳' : 'Reload'} - Note: For the purposes of this demo,you can only block/unblock your own IP address ({visitorData?.ip}) + Note: For the purposes of this demo, you can only block/unblock your own IP address ({visitorData?.ip}) @@ -104,7 +130,11 @@ export const BotFirewall: NextPage = ({ 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'} ) : ( <>-
Timestamp