Add ensure_jail_configs startup check for required jail config files

On startup BanGUI now verifies that the four fail2ban jail config files
required by its two custom jails (manual-Jail and blocklist-import) are
present in `$fail2ban_config_dir/jail.d`.  Any missing file is created
with the correct default content; existing files are never overwritten.

Files managed:
  - manual-Jail.conf        (enabled=false template)
  - manual-Jail.local       (enabled=true override)
  - blocklist-import.conf   (enabled=false template)
  - blocklist-import.local  (enabled=true override)

The check runs in the lifespan hook immediately after logging is
configured, before the database is opened.
This commit is contained in:
2026-03-16 16:26:39 +01:00
parent c41165c294
commit 57cf93b1e5
5 changed files with 299 additions and 27 deletions

View File

@@ -4,43 +4,82 @@ This document breaks the entire BanGUI project into development stages, ordered
---
## Stage 0 — First-Run Bootstrap & Startup Fix
## Task: Ensure Required fail2ban Jail Config Files Exist ✅ DONE
These tasks fix a crash-on-first-boot regression and make the setup/login redirect flow reliable. They must be completed before any other feature work because the application cannot start without them.
**Implemented:** `backend/app/utils/jail_config.py``ensure_jail_configs(jail_d_path)` creates missing `manual-Jail.conf`, `manual-Jail.local`, `blocklist-import.conf`, and `blocklist-import.local` files with correct default content. Called from the lifespan hook in `main.py` using `Path(settings.fail2ban_config_dir) / "jail.d"`. Tests in `backend/tests/test_utils/test_jail_config.py` (6 cases: all missing, all present, only locals missing, directory creation, idempotency, correct content).
The backend must guarantee that two specific jail configuration files are present inside the fail2ban jail directory before the application starts (or on first use). If either file is missing it must be created with the correct default content. The files live inside the directory that fail2ban uses for per-jail drop-in configs (e.g. `jail.d/`). The path to that directory should be read from the application settings (config key `fail2ban_jail_d_path` or equivalent); do not hard-code it.
### Files to create if missing
**`manual-Jail.conf`**
The file must contain a `[manual-Jail]` section with `enabled = false` and all other jail parameters (filter, logpath, backend, maxretry, findtime, bantime, ignoreip) set to the same defaults already documented in `Docker/fail2ban-dev-config/fail2ban/jail.d/manual-Jail.conf`. Only `enabled` must be forced to `false` in this template — it is the `.local` override (see below) that activates the jail.
**`blocklist-import.conf`**
The file must contain a `[blocklist-import]` section with `enabled = false` and all other jail parameters (filter, logpath, backend, maxretry, findtime, bantime, ignoreip) set to the same defaults already documented in `Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf`. Same rule: `enabled = false` here; the `.local` file enables it.
### Local override files
For each `.conf` file above there must also be a corresponding `.local` file checked — and created if missing. The `.local` files must contain **only** the section header and the single `enabled = true` line. Nothing else. fail2ban merges `.local` on top of `.conf` at startup, so all other settings come from the `.conf`.
```
[manual-Jail]
enabled = true
```
```
[blocklist-import]
enabled = true
```
### Implementation notes
- Perform the check in a backend startup routine (e.g. in a `lifespan` hook or a dedicated `ensure_jail_configs()` function called from `main.py`).
- Only create a file if it does **not** already exist. Never overwrite an existing file.
- Log an `INFO` message for each file that is created and a `DEBUG` message when a file already exists.
- Add unit tests that exercise: (a) all four files missing → all four created with correct content, (b) all four files present → nothing is overwritten, (c) only the `.local` files missing → only they are created.
---
### Task 0.1 — Fix: Create the database parent directory before connecting ✅ DONE
## Task: World Map — Country Tooltip on Hover
**File:** `backend/app/main.py`
Currently the world map (`WorldMap.tsx`) shows a ban-count label painted directly onto each country's SVG path and reacts to click events. Add a floating tooltip that appears when the user hovers over a country.
**Implemented:** In `_lifespan`, resolve `settings.database_path` to a `Path`, call `.parent.mkdir(parents=True, exist_ok=True)` before `aiosqlite.connect()`. Added `debug`-level structured log line after mkdir. Tests added in `TestLifespanDatabaseDirectoryCreation`.
### Required behaviour
- When the mouse enters a country geography, display a small floating tooltip near the cursor that shows:
- The country's full name (already available via `country_names` from `useMapData()`).
- The ban count for that country (from the `countries` map; show `0` if the country has no entry).
- The tooltip must follow the mouse while inside the country (or at minimum appear near the cursor when it first enters).
- When the mouse leaves the country the tooltip must disappear.
- Countries with zero bans must also show the tooltip (name + "0 bans").
### Implementation notes
- Store tooltip state (visible, content, x, y) in a `useState` hook local to `WorldMap.tsx`.
- Use `onMouseEnter`, `onMouseMove`, and `onMouseLeave` props on the `<Geography>` element (react-simple-maps already forwards these as standard SVG mouse events).
- Render the tooltip as an absolutely-positioned `<div>` overlaid on the map container. Apply a `pointer-events: none` style so it does not interfere with hover detection on the map itself.
- Reuse the existing Fluent UI design tokens (background, border, shadow, typography) so the tooltip matches the rest of the UI. Do not introduce a new third-party tooltip library.
- Add a Vitest / React Testing Library test that mounts `WorldMap` with mock data, fires a `mouseenter` event on a geography, and asserts the tooltip text is visible.
---
### Task 0.2 — Fix: SetupRedirectMiddleware must treat a missing database as "setup not complete" ✅ DONE
## Task: Main Menu — Tooltips on Navigation Items
**File:** `backend/app/main.py`
The main navigation sidebar (or top bar, whichever is used) currently has no tooltips. Add a tooltip to each navigation item so that users who are unfamiliar with icon-only or collapsed menus can see the destination name without navigating.
**Implemented:** Changed the guard in `SetupRedirectMiddleware.dispatch` so that `db is None` also triggers the redirect to `/api/setup`. The condition is now `if db is None or not await setup_service.is_setup_complete(db)`. The `_setup_complete_cached` flag is only set after a successful `is_setup_complete(db)` call with a live `db`. Tests added in `TestSetupRedirectMiddlewareDbNone`.
---
### Task 0.3 — Fix: SetupGuard must redirect to /setup on API errors, not allow through ✅ DONE
**File:** `frontend/src/components/SetupGuard.tsx`
**Implemented:** Changed the `.catch()` handler to set `status` to `"pending"` instead of `"done"`. Updated the comment to explain the conservative fallback. Tests added in `SetupGuard.test.tsx`.
---
### Task 0.4 — Fix: SetupPage must redirect to /login when setup is already complete ✅ DONE
**File:** `frontend/src/pages/SetupPage.tsx`
**Implemented:**
1. Added `checking` boolean state (initialised to `true`). While `checking` is true, a full-screen `<Spinner>` is rendered instead of the form, preventing the form from flashing.
2. The `useEffect` sets `checking` to `false` in both the `.then()` (when setup is not complete) and the `.catch()` branch. Added a `console.warn` in the catch block. Added a `cancelled` flag and cleanup return to the effect.
Tests added in `SetupPage.test.tsx`.
### Required behaviour
- Each navigation item must show a tooltip containing the item's label (e.g. "Dashboard", "Map", "Blocklist", "Settings") when the user hovers over it.
- Tooltips should appear after a short delay (≈ 300 ms) to avoid flickering during fast cursor movement past the menu.
- The tooltip must be dismissed when the cursor leaves the item.
- If the menu is already showing a full text label next to the icon, the tooltip is still added (it reinforces accessibility); but consider hiding it when the sidebar is expanded and the label is already visible, to avoid redundancy.
### Implementation notes
- Use the Fluent UI `<Tooltip>` component (from `@fluentui/react-components`) which is already a project dependency. Wrap each navigation `<NavLink>` (or equivalent element) with `<Tooltip content="…" relationship="label">`.
- Keep the tooltip content string co-located with the route definition so that if a label changes in one place it changes everywhere.
- Do not introduce any new npm dependencies.
- Add a Vitest / React Testing Library test that renders the navigation component, triggers a hover on each item, and asserts the correct tooltip text is present in the DOM.
---