/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import React, { useState, useCallback } from 'react'; import { useForm } from 'react-hook-form'; import iconSearch from '../../images/icon-search.svg'; import ErrorAlert from '../ErrorAlert'; import { BlockStatus } from './BlockStatus'; import { BlockStatus as BlockStatusData } from 'fxa-admin-server/src/types'; import { adminApi } from '../../lib/api'; interface RateLimitSearchState { ip: string; email: string; uid: string; } type RateLimitQuery = Partial; const createRateLimitQuery = (data: RateLimitSearchState): RateLimitQuery => { const [ip, email, uid] = [data.ip.trim(), data.email.trim(), data.uid.trim()]; return { ip: ip || undefined, email: email || undefined, uid: uid || undefined, }; }; const validateQuery = (query: RateLimitQuery): boolean => { return Boolean(query.ip || query.email || query.uid); }; const VALIDATION_MESSAGE = 'Please enter at least one of: IP address, email, or UID'; export const PageRateLimiting = () => { const { register, handleSubmit, reset, getValues } = useForm({ defaultValues: { ip: '', email: '', uid: '', }, }); const [showResults, setShowResults] = useState(false); const [clearing, setClearing] = useState(false); const [status, setStatus] = useState<{ isError: boolean; message: string; }>(); const [rateLimitsState, setRateLimitsState] = useState<{ loading: boolean; error?: Error; data?: BlockStatusData[]; }>({ loading: false }); const fetchRateLimits = useCallback(async (variables: RateLimitQuery) => { setRateLimitsState({ loading: true }); try { const data = await adminApi.getRateLimits(variables); setRateLimitsState({ loading: false, data }); } catch (error) { setRateLimitsState({ loading: false, error: error as Error }); } }, []); const onSubmit = (data: RateLimitSearchState) => { const variables = createRateLimitQuery(data); setStatus(undefined); if (!validateQuery(variables)) { setStatus({ isError: true, message: VALIDATION_MESSAGE }); return; } setShowResults(true); fetchRateLimits(variables); // keep form values after submit reset(data); }; const handleClear = async () => { const values = getValues(); const variables = createRateLimitQuery(values); setStatus(undefined); if (!validateQuery(variables)) { setStatus({ isError: true, message: VALIDATION_MESSAGE }); return; } setClearing(true); try { const count = await adminApi.clearRateLimits(variables); setStatus({ isError: false, message: `Successfully cleared ${count} rate limit record(s)`, }); } catch (error) { setStatus({ isError: true, message: `Failed to clear: ${error instanceof Error ? error.message : 'Unknown error'}`, }); return; } finally { setClearing(false); // keep form values after submit reset(values); } setShowResults(true); // Refetch after clear fetchRateLimits(variables); }; const SearchField = ({ id, label, type = 'text', }: { id: keyof RateLimitSearchState; label: string; type?: string; }) => { return (
setStatus(undefined) })} />
); }; return (

Rate Limiting

Search for active rate limiting blocks and bans by IP address, email, and/or UID. Please provide as much fields as possible for more accurate results.

{/* Plain calls, so a status change does not remount the inputs */} {SearchField({ id: 'ip', label: 'IP Address' })} {SearchField({ id: 'email', label: 'Email', type: 'email' })} {SearchField({ id: 'uid', label: 'UID' })}
{status && (

{status.message}

)} {showResults && }
); }; interface RateLimitResultsProps { rateLimitsResult: { loading: boolean; error?: Error; data?: BlockStatusData[]; }; } const RateLimitResults = ({ rateLimitsResult }: RateLimitResultsProps) => { if (rateLimitsResult.loading) { return ( <>

Loading rate limit information...

); } if (rateLimitsResult.error) { return ( <>
); } const allRateLimits = rateLimitsResult.data || []; const blocks = allRateLimits.filter((item) => item.policy === 'block'); const bans = allRateLimits.filter((item) => item.policy === 'ban'); return ( <>

Active Blocks ({blocks.length})

{blocks.length === 0 ? (

No active blocks found.

) : (
{blocks.map((block) => ( ))}
)}

Active Bans ({bans.length})

{bans.length === 0 ? (

No active bans found.

) : (
{bans.map((ban) => ( ))}
)}
); }; export default PageRateLimiting;