@@ -38,6 +38,58 @@ import { toast } from 'sonner'
3838import { EmptyState , PageHeader , QueryError , ResultsSummary } from '@/components/common'
3939import { useUrlFilters } from '@/hooks'
4040
41+ /**
42+ * Type guard to check if an object is a valid Bouncer.
43+ * This provides runtime validation of the data received from the API.
44+ */
45+ function isBouncer ( obj : unknown ) : obj is Bouncer {
46+ return (
47+ obj !== null &&
48+ typeof obj === 'object' &&
49+ 'name' in obj &&
50+ typeof ( obj as { name : unknown } ) . name === 'string' &&
51+ 'ip_address' in obj &&
52+ typeof ( obj as { ip_address : unknown } ) . ip_address === 'string' &&
53+ 'valid' in obj &&
54+ typeof ( obj as { valid : unknown } ) . valid === 'boolean' &&
55+ 'last_pull' in obj &&
56+ typeof ( obj as { last_pull : unknown } ) . last_pull === 'string' &&
57+ 'type' in obj &&
58+ typeof ( obj as { type : unknown } ) . type === 'string' &&
59+ 'version' in obj &&
60+ typeof ( obj as { version : unknown } ) . version === 'string' &&
61+ ( ! ( 'status' in obj ) || typeof ( obj as { status : unknown } ) . status === 'string' )
62+ )
63+ }
64+
65+ /**
66+ * Extracts an array of valid `Bouncer` objects from API payloads.
67+ *
68+ * Accepts either a raw array of items or an object containing a `bouncers` array and returns
69+ * only entries that conform to the expected `Bouncer` shape.
70+ *
71+ * @param raw - The API response payload to normalize; may be an array or an object with a `bouncers` property
72+ * @returns An array of `Bouncer` objects extracted from `raw`, or an empty array if none were found
73+ */
74+ function normalizeBouncers ( raw : unknown ) : Bouncer [ ] {
75+ if ( Array . isArray ( raw ) ) {
76+ return raw . filter ( isBouncer )
77+ }
78+
79+ if ( raw && typeof raw === 'object' && 'bouncers' in raw && Array . isArray ( ( raw as { bouncers : unknown } ) . bouncers ) ) {
80+ return ( raw as { bouncers : unknown [ ] } ) . bouncers . filter ( isBouncer )
81+ }
82+
83+ return [ ]
84+ }
85+
86+ /**
87+ * Page component that renders the Bouncers management UI for listing, searching, adding, and deleting CrowdSec bouncers.
88+ *
89+ * Renders a searchable table of registered bouncers, provides a dialog to create new bouncers (showing the API key once), and exposes delete actions with confirmation. Fetches and normalizes bouncer data from the Local API and performs add/delete mutations with user-facing success/error toasts.
90+ *
91+ * @returns The page's React element rendering the bouncers management interface.
92+ */
4193export default function Bouncers ( ) {
4294 const queryClient = useQueryClient ( )
4395 const [ urlFilters , setUrlFilter ] = useUrlFilters ( [ 'q' ] , { q : '' } )
@@ -50,18 +102,11 @@ export default function Bouncers() {
50102 queryKey : [ 'bouncers' ] ,
51103 queryFn : async ( ) => {
52104 const response = await api . crowdsec . getBouncers ( )
53- const raw = response . data . data
54- // Adapt to whatever shape the backend returns
55- if ( Array . isArray ( raw ) ) return raw as Bouncer [ ]
56- if ( raw && typeof raw === 'object' ) {
57- // Backend wraps as { bouncers: [...], count: N }
58- const obj = raw as Record < string , unknown >
59- for ( const val of Object . values ( obj ) ) {
60- if ( Array . isArray ( val ) ) return val as Bouncer [ ]
61- }
62- }
63- return [ ] as Bouncer [ ]
105+ return response . data . data
64106 } ,
107+ // The dashboard populates this cache key with the raw { bouncers, count }
108+ // payload shape, so normalize it per observer on the Bouncers page.
109+ select : normalizeBouncers ,
65110 } )
66111
67112 const addBouncerMutation = useMutation ( {
0 commit comments