Add mass unban: DELETE /api/bans/all clears all active bans

- Send fail2ban's `unban --all` command via new `unban_all_ips()` service
  function; returns the count of unbanned IPs
- Add `UnbanAllResponse` Pydantic model (message + count)
- Add `DELETE /api/bans/all` router endpoint; handles 502 on socket error
- Frontend: `bansAll` endpoint constant, `unbanAllBans()` API call,
  `UnbanAllResponse` type, `unbanAll` action in `useActiveBans` hook
- JailsPage: "Clear All Bans" button (visible when bans > 0) with a
  Fluent UI confirmation Dialog before executing the operation
- 7 new tests (3 service, 4 router); 440 total pass, 82% coverage
This commit is contained in:
2026-03-07 21:16:49 +01:00
parent 207be94c42
commit 4773ae1c7a
11 changed files with 382 additions and 10 deletions

View File

@@ -320,3 +320,89 @@ When the most recent blocklist import run completed with errors, a warning indic
| Frontend layout | `frontend/src/layouts/MainLayout.tsx` |
| Tests | `backend/tests/test_services/test_blocklist_service.py` |
| Tests | `backend/tests/test_routers/test_blocklist.py` |
---
## Task 6 — Mass Unban: Clear All Currently Banned IPs ✅ DONE
**Completed:** Added `unban_all_ips()` service function using fail2ban's `unban --all` command. Added `DELETE /api/bans/all` endpoint returning `UnbanAllResponse` with count. Frontend: `bansAll` endpoint constant, `unbanAllBans()` API function, `UnbanAllResponse` type in `types/jail.ts`, `unbanAll` action exposed from `useActiveBans` hook. JailsPage `ActiveBansSection` shows a "Clear All Bans" button (only when bans > 0) that opens a Fluent UI confirmation Dialog before executing. Tests: 7 new tests (3 service + 4 router); 440 total pass.
### Problem
[Features.md § 5](Features.md) specifies: *"Option to unban all IPs at once across every jail."*
Currently the Jails page allows unbanning a single IP from a specific jail or from all jails, but there is no mechanism to clear every active ban globally in one operation. fail2ban supports this natively via the `unban --all` socket command, which returns the count of unbanned IPs.
### Goal
Add a **"Clear All Bans"** action that:
1. Sends `unban --all` to fail2ban, removing every active ban across every jail in a single command.
2. Returns a count of how many IPs were unbanned.
3. Is exposed in the UI as a "Clear All Bans" button with a confirmation dialog so users cannot trigger it accidentally.
4. Refreshes the active-bans list automatically after the operation completes.
### Implementation Details
**Backend — service function**
1. **`backend/app/services/jail_service.py`** — Add `unban_all_ips(socket_path: str) -> int`:
- Sends `["unban", "--all"]` via `Fail2BanClient`.
- Returns the integer count reported by fail2ban (`_ok()` extracts it from the `(0, count)` tuple).
- Logs the operation at `info` level with the returned count.
**Backend — new response model**
2. **`backend/app/models/ban.py`** — Add `UnbanAllResponse(BaseModel)` with fields:
- `message: str` — human-readable summary.
- `count: int` — number of IPs that were unbanned.
**Backend — new endpoint**
3. **`backend/app/routers/bans.py`** — Add:
```
DELETE /api/bans/all — unban every currently banned IP across all jails
```
- Returns `UnbanAllResponse` with `count` from the service call.
- No request body required.
- Handles `Fail2BanConnectionError` → 502.
**Frontend — API**
4. **`frontend/src/api/endpoints.ts`** — Add `bansAll: "/bans/all"` to the `ENDPOINTS` map.
5. **`frontend/src/api/jails.ts`** — Add `unbanAllBans(): Promise<UnbanAllResponse>` that calls `del<UnbanAllResponse>(ENDPOINTS.bansAll)`.
6. **`frontend/src/types/jail.ts`** — Add `UnbanAllResponse` interface `{ message: string; count: number }`.
**Frontend — hook**
7. **`frontend/src/hooks/useJails.ts`** — In `useActiveBans`, add `unbanAll: () => Promise<UnbanAllResponse>` action and expose it from the hook return value.
**Frontend — UI**
8. **`frontend/src/pages/JailsPage.tsx`** — In `ActiveBansSection`:
- Add a "Clear All Bans" `Button` (appearance `"outline"`, intent `"danger"`) in the section header next to the existing Refresh button.
- Wrap the confirm action in a Fluent UI `Dialog` with a warning body explaining the operation is irreversible.
- On confirmation, call `unbanAll()`, show a success `MessageBar` with the count, and call `refresh()`.
**Tests**
9. **`backend/tests/test_services/test_jail_service.py`** — Add `TestUnbanAllIps`:
- `test_unban_all_ips_returns_count` — mocks client with `(0, 5)` response, asserts return is `5`.
- `test_unban_all_ips_raises_on_fail2ban_error` — mocks client to raise `Fail2BanConnectionError`, asserts it propagates.
10. **`backend/tests/test_routers/test_bans.py`** — Add `TestUnbanAll`:
- `test_204_clears_all_bans` — patches `unban_all_ips` returning `3`, asserts 200 response with `count == 3`.
- `test_502_when_fail2ban_unreachable` — patches `unban_all_ips` raising `Fail2BanConnectionError`, asserts 502.
- `test_401_when_unauthenticated` — unauthenticated request returns 401.
### Files Touched
| Layer | File |
|-------|------|
| Model | `backend/app/models/ban.py` |
| Service | `backend/app/services/jail_service.py` |
| Router | `backend/app/routers/bans.py` |
| Frontend type | `frontend/src/types/jail.ts` |
| Frontend API | `frontend/src/api/endpoints.ts` |
| Frontend API | `frontend/src/api/jails.ts` |
| Frontend hook | `frontend/src/hooks/useJails.ts` |
| Frontend UI | `frontend/src/pages/JailsPage.tsx` |
| Tests | `backend/tests/test_services/test_jail_service.py` |
| Tests | `backend/tests/test_routers/test_bans.py` |