Files
BanGUI/frontend/src/hooks/useJailAdmin.ts
Lukas 1c673d600c Standardize API response envelope shapes across all endpoints
This commit standardizes how API responses are wrapped, solving issue #24.

Problem:
- Inconsistent response envelopes (jails vs items vs bans vs no wrapper)
- Frontend required multiple field name variants
- Integration bugs from branching logic
- No clear pattern for different response types

Solution:
- Created response.py with base classes: PaginatedListResponse,
  CollectionResponse, CommandResponse
- Standardized all list/collection responses to use 'items' field
- Domain-specific field names for detail and aggregation responses
- Updated all backends routers and mappers
- Updated frontend types and hooks to match

Changes:
Backend:
- backend/app/models/response.py (new): Base response models
- backend/app/models/ban.py: Updated responses to inherit from bases
- backend/app/models/jail.py: Updated JailListResponse, JailCommandResponse
- backend/app/models/config.py: Updated collection responses
- backend/app/services/jail_service.py: Updated return statements
- backend/app/mappers/ban_mappers.py: Updated 'bans' to 'items'
- backend/tests/test_mappers/test_ban_mappers.py: Updated tests

Frontend:
- frontend/src/types/jail.ts: Updated response interfaces
- frontend/src/types/config.ts: Updated response interfaces
- frontend/src/hooks/useActiveBans.ts: Updated selector
- frontend/src/hooks/useJailList.ts: Updated selector
- frontend/src/hooks/useJailConfigs.ts: Updated selector
- frontend/src/hooks/useConfigActiveStatus.ts: Updated field access
- frontend/src/hooks/useJailAdmin.ts: Updated field access

Documentation:
- Docs/Backend-Development.md: Added § 4.1 API Response Envelope Policy

The policy defines:
1. Paginated lists use PaginatedListResponse (items, total, page, page_size)
2. Non-paginated collections use CollectionResponse (items, total)
3. Detail responses use entity-specific field names (jail, status, settings)
4. Command responses use CommandResponse (message, success, optional target)
5. Aggregations use domain-specific fields (jails, countries, buckets, bans)

All responses now follow one of these patterns, reducing frontend complexity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-28 10:12:55 +02:00

124 lines
3.5 KiB
TypeScript

/**
* React hook for managing inactive jail operations and configuration actions.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import {
activateJail,
deactivateJail,
deleteJailLocalOverride,
fetchInactiveJails,
validateJailConfig,
createJailConfigFile,
} from "../api/config";
import { handleFetchError, createStringErrorAdapter } from "../utils/fetchError";
import type {
ActivateJailRequest,
ConfFileCreateRequest,
InactiveJail,
JailActivationResponse,
JailValidationResult,
} from "../types/config";
export interface UseJailAdminResult {
inactiveJails: InactiveJail[];
inactiveLoading: boolean;
inactiveError: string | null;
refreshInactiveJails: () => void;
deactivateJail: (name: string) => Promise<void>;
deleteJailLocalOverride: (name: string) => Promise<void>;
validateJailConfig: (name: string) => Promise<JailValidationResult>;
activateJail: (name: string, payload: ActivateJailRequest) => Promise<JailActivationResponse>;
createJailConfigFile: (payload: ConfFileCreateRequest) => Promise<void>;
}
/**
* Load inactive fail2ban jails and expose the admin actions used by the
* jail configuration tab.
*/
export function useJailAdmin(): UseJailAdminResult {
const [inactiveJails, setInactiveJails] = useState<InactiveJail[]>([]);
const [inactiveLoading, setInactiveLoading] = useState(false);
const [inactiveError, setInactiveError] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null);
const refreshInactiveJails = useCallback((): void => {
abortRef.current?.abort();
const ctrl = new AbortController();
abortRef.current = ctrl;
setInactiveLoading(true);
setInactiveError(null);
fetchInactiveJails(ctrl.signal)
.then((resp) => {
if (!ctrl.signal.aborted) {
setInactiveJails(resp.items);
}
})
.catch((err: unknown) => {
if (!ctrl.signal.aborted) {
handleFetchError(err, createStringErrorAdapter(setInactiveError), "Failed to load inactive jails");
}
})
.finally(() => {
if (!ctrl.signal.aborted) {
setInactiveLoading(false);
}
});
}, []);
useEffect(() => {
refreshInactiveJails();
return (): void => {
abortRef.current?.abort();
};
}, [refreshInactiveJails]);
const handleDeactivateJail = useCallback(
async (name: string): Promise<void> => {
await deactivateJail(name);
},
[],
);
const handleDeleteLocalOverride = useCallback(
async (name: string): Promise<void> => {
await deleteJailLocalOverride(name);
},
[],
);
const handleValidateJailConfig = useCallback(
async (name: string): Promise<JailValidationResult> => {
return await validateJailConfig(name);
},
[],
);
const handleActivateJail = useCallback(
async (name: string, payload: ActivateJailRequest): Promise<JailActivationResponse> => {
return await activateJail(name, payload);
},
[],
);
const handleCreateJailConfigFile = useCallback(
async (payload: ConfFileCreateRequest): Promise<void> => {
await createJailConfigFile(payload);
},
[],
);
return {
inactiveJails,
inactiveLoading,
inactiveError,
refreshInactiveJails,
deactivateJail: handleDeactivateJail,
deleteJailLocalOverride: handleDeleteLocalOverride,
validateJailConfig: handleValidateJailConfig,
activateJail: handleActivateJail,
createJailConfigFile: handleCreateJailConfigFile,
};
}