diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 551b745..a54e663 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -4,342 +4,157 @@ This document breaks the entire BanGUI project into development stages, ordered --- -## Stage 1 — Inactive Jail Discovery and Activation +## Stage: Config Page Cleanup & Improvements -Currently BanGUI only shows jails that are actively running in fail2ban. fail2ban ships with dozens of pre-defined jails in `jail.conf` (sshd, apache-auth, nginx-http-auth, postfix, etc.) that are disabled by default. Users must be able to see these inactive jails and activate them from the web interface. - -### Task 1.1 — Backend: Parse Inactive Jails from Config Files ✅ DONE - -**Goal:** Read all jail definitions from the fail2ban configuration files and identify which ones are not currently enabled. - -**Details:** - -- Create a new service `config_file_service.py` in `app/services/` responsible for reading and writing fail2ban config files on disk. -- Parse `jail.conf` and any `jail.local` / `jail.d/*.conf` / `jail.d/*.local` override files. Use Python's `configparser` (INI format) since fail2ban configs follow that format. Local files override `.conf` files (fail2ban merge order: `jail.conf` → `jail.local` → `jail.d/*.conf` → `jail.d/*.local`). -- For each jail section found, extract at minimum: jail name, `enabled` flag (defaults to `false` if absent), `filter` name (defaults to the jail name if absent), `action` references, `port`, `logpath`, `backend`, `bantime`, `findtime`, `maxretry`. -- Build a list of `InactiveJail` objects for jails where `enabled = false` or that are defined in config but not reported by `fail2ban-client status`. -- The fail2ban config base path should come from the application settings (default: `/etc/fail2ban/`). The path is already available via `config.py` settings or can be derived from the fail2ban socket connection. -- Handle the `[DEFAULT]` section properly — its values provide defaults inherited by every jail section. -- Handle `[INCLUDES]` sections (`before` / `after` directives) so that path includes like `paths-debian.conf` are resolved, providing variables like `%(sshd_log)s`. - -**Files to create/modify:** -- `app/services/config_file_service.py` (new) -- `app/models/config.py` (add `InactiveJail`, `InactiveJailListResponse` models) - -**References:** [Features.md §6](Features.md), [Architekture.md §2](Architekture.md), [Backend-Development.md](Backend-Development.md) +These tasks refine the Configuration page UI. Each task is self-contained and can be completed independently. References point to the specific files that need modification. --- -### Task 1.2 — Backend: API Endpoints for Inactive Jails ✅ DONE +### ✅ Task 1 — Remove the Export tab from the Config page -**Goal:** Expose inactive jail data and an activation endpoint via the REST API. +**Status:** Done — Removed `ExportTab` import, `"export"` from `TabValue`, the `` element, and the conditional render. `DocumentArrowDown24Regular` icon import also cleaned up. -**Details:** +**Goal:** Remove the "Export" tab entirely from the Configuration page. Users should no longer see the Export option in the tab bar, and its rendering logic should be cleaned up. -- Add a `GET /api/config/jails/inactive` endpoint to the config router that returns all inactive jails found in the config files. Each entry should include: `name`, `filter`, `actions` (list), `port`, `logpath`, `bantime`, `findtime`, `maxretry`, and the source file where the jail is defined. -- Add a `POST /api/config/jails/{name}/activate` endpoint that enables an inactive jail. This endpoint should: - 1. Validate the jail name exists in the parsed config. - 2. Write `enabled = true` into the appropriate `.local` override file (`jail.local` or `jail.d/{name}.local`). Never modify `.conf` files directly — always use `.local` overrides following fail2ban convention. - 3. Optionally accept a request body with override values (`bantime`, `findtime`, `maxretry`, `port`, `logpath`) that get written alongside the `enabled = true` line. - 4. Trigger a fail2ban reload so the jail starts immediately. - 5. Return the jail's new status after activation. -- Add a `POST /api/config/jails/{name}/deactivate` endpoint that sets `enabled = false` in the `.local` file and reloads fail2ban to stop the jail. -- All endpoints require authentication. Return 404 if the jail name is not found in any config file. Return 409 if the jail is already active/inactive. +**Files to modify:** +- `frontend/src/pages/ConfigPage.tsx` -**Files to create/modify:** -- `app/routers/config.py` (add endpoints) -- `app/services/config_file_service.py` (add activate/deactivate logic) -- `app/models/config.py` (add request/response models: `ActivateJailRequest`, `JailActivationResponse`) +**What to do:** +1. Remove the `"export"` value from the `TabValue` union type. +2. Remove the `}>Export` element from the ``. +3. Remove the `{tab === "export" && }` conditional render from the tab content area. +4. Remove the `ExportTab` import from the component imports. +5. Remove the `DocumentArrowDown24Regular` icon import if it is no longer used elsewhere. -**References:** [Features.md §6](Features.md), [Backend-Development.md §3](Backend-Development.md) +**Verification:** The Config page should render without the Export tab. No TypeScript errors. All other tabs continue to work. --- -### Task 1.3 — Frontend: Inactive Jails in Configuration View ✅ DONE +### ✅ Task 2 — Remove "Refresh" and "Reload fail2ban" buttons from the Jails tab -**Goal:** Display inactive jails in the Configuration page and let users activate them. +**Status:** Done — Removed `buttonRow` div with both buttons, the `reloadMsg` `MessageBar`, all associated state (`reloading`, `reloadMsg`, `deactivating`) and handlers (`handleRefresh`, `handleReload`). Cleaned up `reloadAll` from `useJailConfigs()` destructure and `ArrowClockwise24Regular` icon import. -**Details:** +**Goal:** Remove the two action buttons ("Refresh" and "Reload fail2ban") that appear at the top of the Jails tab. -- In the **Jails tab** of the Configuration page, extend the existing master/detail list layout. The left pane currently shows active jails — add a section or toggle that also shows inactive jails. Use the **Active/Inactive badge** system described in [Features.md §6](Features.md): active jails sorted to the top with an "Active" badge, inactive jails below with an "Inactive" badge, alphabetical within each group. -- Clicking an inactive jail in the list loads its config details in the right detail pane. Show all parsed fields (filter, actions, port, logpath, bantime, findtime, maxretry) as read-only or editable fields. -- Add an **"Activate Jail"** button (prominent, primary style) in the detail pane for inactive jails. Clicking it opens a confirmation dialog that allows the user to override default values (bantime, findtime, maxretry, port, logpath) before activation. On confirm, call `POST /api/config/jails/{name}/activate` with the overrides. -- After successful activation, refresh the jail list and show the jail under the active group with a success toast notification. -- For active jails, add a **"Deactivate Jail"** button (secondary/danger style) that calls the deactivate endpoint after confirmation. -- Add the API functions `fetchInactiveJails()`, `activateJail(name, overrides)`, `deactivateJail(name)` to `frontend/src/api/config.ts`. -- Add corresponding TypeScript types (`InactiveJail`, `ActivateJailRequest`) to `frontend/src/types/config.ts`. +**Files to modify:** +- `frontend/src/components/config/JailsTab.tsx` -**Files to create/modify:** -- `frontend/src/api/config.ts` (add API calls) -- `frontend/src/types/config.ts` (add types) -- `frontend/src/components/config/JailsTab.tsx` (add inactive jail list + activation UI) -- New component: `frontend/src/components/config/ActivateJailDialog.tsx` +**What to do:** +1. In the `JailsTab` component (the public export, starting around line 584), locate the `
` block that renders the "Refresh" and "Reload fail2ban" buttons (around lines 722–740). +2. Remove the entire `
...
` block containing both buttons. +3. Remove the `reloadMsg` `` block that follows the button row (around lines 741–748), since it only displays messages from the reload action. +4. Clean up the now-unused state variables and handlers: `reloading`, `reloadMsg`, `setReloading`, `setReloadMsg`, `handleRefresh`, and `handleReload`. +5. Remove the `reloadAll` destructured value from the `useJailConfigs()` call if it is no longer used. +6. Remove the `ArrowClockwise24Regular` icon import if it is no longer used elsewhere in this file. +7. Keep `refresh` and `loadInactive` — they are still used by `handleDeactivate` and `handleActivated`. -**References:** [Features.md §6](Features.md), [Web-Design.md](Web-Design.md), [Web-Development.md](Web-Development.md) +**Verification:** The Jails tab no longer shows the two buttons or any reload message bar. Active and inactive jails still load and display correctly. Deactivate and activate flows still work. --- -### Task 1.4 — Frontend: Inactive Jails on Jails Page ✅ DONE +### ✅ Task 3 — Add a "Create Config" button to the Jails tab -**Goal:** Show inactive jails alongside active jails in the Jail Management overview. +**Status:** Done — Created `CreateJailDialog.tsx`, wired `listHeader` prop into `ConfigListDetail` with an `Add24Regular` button, added `createDialogOpen` state. Dialog calls `createJailConfigFile` API and refreshes list on success. Exported from `config/index.ts`. -**Details:** +**Goal:** Add a button at the top of the Jails tab list pane that opens a dialog to create a new jail configuration file by name. This is analogous to the "Create Filter" button already present in the Filters tab. -- On the **Jails Page** (`JailsPage.tsx`), extend the jail overview table to include inactive jails. Add a column or badge showing each jail's activation state. -- Inactive jails should appear in the table with dimmed/muted styling and show "Inactive" as status. They should not have start/stop/idle/reload controls — only an "Activate" action button. -- Add a toggle or filter above the table: "Show inactive jails" (default: on). When off, only active jails are displayed. -- Clicking an inactive jail's name navigates to the Configuration page's jail detail for that jail, pre-selecting it in the list. +**Files to modify:** +- `frontend/src/components/config/JailsTab.tsx` — add button + dialog trigger +- Optionally create a new `frontend/src/components/config/CreateJailDialog.tsx` dialog component -**Files to create/modify:** -- `frontend/src/pages/JailsPage.tsx` -- `frontend/src/hooks/useJails.ts` (extend to optionally fetch inactive jails) +**What to do:** +1. Add a "Create Config" button at the top of the Jails tab. The recommended approach is to use the `listHeader` prop of ``, the same pattern used in `FiltersTab.tsx` (see lines 233–240). Pass a `` as the `listHeader`. +2. Create a `CreateJailDialog` component (or inline the logic). The dialog should: + - Accept a name input from the user (the jail config file name, e.g. `my-custom-jail`). + - On confirm, call the existing `createJailConfigFile` API function from `frontend/src/api/config.ts` with `{ name, content: "# \n" }` as the payload. + - Show a loading state while the request is in flight. + - Display any errors from the API. + - On success, close the dialog and refresh the jail list (call `refresh()` and `loadInactive()`). +3. Wire the button's `onClick` to open the dialog, and handle the `onCreated` callback to refresh data. -**References:** [Features.md §5](Features.md), [Web-Design.md](Web-Design.md) +**Reference:** See `CreateFilterDialog.tsx` for the established dialog pattern, and `JailFilesTab.tsx` lines 98–110 for the `createJailConfigFile` API usage. + +**Verification:** Clicking "Create Config" opens a dialog. Entering a name and confirming creates a new jail config file. The new file appears in the jail list after creation. --- -### Task 1.5 — Tests: Inactive Jail Parsing and Activation ✅ DONE +### ✅ Task 4 — Remove the "Active" badge from the Filter and Action detail panes -**Goal:** Full test coverage for the new inactive-jail functionality. +**Status:** Done — Removed the `
` wrapper with Active/Inactive and "Has local override" badges from `FilterDetail` in `FiltersTab.tsx` and from `ActionDetail` in `ActionsTab.tsx`. Removed unused `Badge` import from both files. -**Details:** +**Goal:** Remove the `` component that shows "Active" or "Inactive" in the detail (right) pane of both the Filters tab and the Actions tab. The badge in the list pane (left side, inside `ConfigListDetail`) should remain — only the one in the detail section needs to be removed. -- **Service tests** (`tests/test_services/test_config_file_service.py`): Test config file parsing with mock jail.conf content containing multiple jails (enabled and disabled). Test the merge order (`.conf` → `.local` → `jail.d/`). Test DEFAULT section inheritance. Test variable interpolation (`%(sshd_log)s`). Test activation writes to `.local` files. Test deactivation. -- **Router tests** (`tests/test_routers/test_config.py`): Add tests for the three new endpoints. Test 404 for unknown jail names. Test 409 for already-active/inactive jails. Test that override values are passed through correctly. -- **Frontend tests**: Add unit tests for the new API functions. Add component tests for the activation dialog and the inactive jail display. +**Files to modify:** +- `frontend/src/components/config/FiltersTab.tsx` +- `frontend/src/components/config/ActionsTab.tsx` -**Files to create/modify:** -- `backend/tests/test_services/test_config_file_service.py` (new) -- `backend/tests/test_routers/test_config.py` (extend) -- `frontend/src/components/config/__tests__/` (new tests) +**What to do in `FiltersTab.tsx`:** +1. In the `FilterDetail` component (around lines 90–112), locate the `
` block that wraps the Active/Inactive `` and the optional "Has local override" ``. +2. Remove this entire `
` block, including both badges inside it. +3. If the `Badge` import from `@fluentui/react-components` is no longer used in this file, remove it. -**References:** [Backend-Development.md](Backend-Development.md), [Web-Development.md](Web-Development.md) +**What to do in `ActionsTab.tsx`:** +1. In the `ActionDetail` component (around lines 115–137), locate the same `
` block with the Active/Inactive badge and the "Has local override" badge. +2. Remove this entire `
` block, including both badges. +3. Clean up unused `Badge` import if applicable. + +**Verification:** Selecting a filter or action in the list still works. The detail pane no longer shows the Active/Inactive badge or the "Has local override" badge. The list pane badges are unaffected. --- -## Stage 2 — Filter Configuration Discovery and Activation - -fail2ban ships with a large collection of filter definitions in `filter.d/` (over 80 files). Users need to see all available filters — both those currently in use by active jails and those available but unused — and assign them to jails. - -### Task 2.1 — Backend: List All Available Filters with Active/Inactive Status ✅ DONE - -**Goal:** Enumerate all filter config files and mark each as active or inactive. - -**Details:** - -- Add a method `list_filters()` to `config_file_service.py` that scans the `filter.d/` directory within the fail2ban config path. -- For each `.conf` file found, parse its `[Definition]` section to extract: `failregex` (list of patterns), `ignoreregex` (list), `datepattern`, `journalmatch`, and any other fields present. Also parse `[Init]` sections for default variable bindings. -- Handle `.local` overrides: if `sshd.local` exists alongside `sshd.conf`, merge the local values on top of the conf values (local wins). -- Determine active/inactive status: a filter is "active" if its name matches the `filter` field of any currently enabled jail (cross-reference with the active jail list from fail2ban and the inactive jail configs). Mark it accordingly. -- Return a list of `FilterConfig` objects with: `name`, `active` (bool), `used_by_jails` (list of jail names using this filter), `failregex` (list), `ignoreregex` (list), `datepattern`, `source_file` (path to the `.conf` file), `has_local_override` (bool). -- Add a `GET /api/config/filters` endpoint to the config router returning all filters. -- Add a `GET /api/config/filters/{name}` endpoint returning the full parsed detail of a single filter. - -**Files to create/modify:** -- `app/services/config_file_service.py` (add `list_filters`, `get_filter`) -- `app/models/config.py` (add `FilterConfig`, `FilterListResponse`, `FilterDetailResponse`) -- `app/routers/config.py` (add endpoints) - -**References:** [Features.md §6](Features.md), [Architekture.md §2](Architekture.md) - ---- - -### Task 2.2 — Backend: Activate and Edit Filters ✅ DONE - -**Implemented:** -- `PUT /api/config/filters/{name}` — writes `failregex`, `ignoreregex`, `datepattern`, `journalmatch` changes to `filter.d/{name}.local`. Validates regex before writing. Supports `?reload=true`. -- `POST /api/config/filters` — creates `filter.d/{name}.local` from `FilterCreateRequest`. Returns 409 if file already exists. -- `DELETE /api/config/filters/{name}` — deletes `.local` only; refuses with 409 if filter is conf-only (readonly). -- `POST /api/config/jails/{name}/filter` — assigns a filter to a jail by writing `filter = {name}` to `jail.d/{jail}.local`. Supports `?reload=true`. -- All regex patterns validated via `re.compile()` before writing; invalid patterns return 422. -- New models: `FilterUpdateRequest`, `FilterCreateRequest`, `AssignFilterRequest`. -- Resolved routing conflict: `file_config.py` raw-write routes renamed to `PUT /filters/{name}/raw` and `POST /filters/raw` (consistent with existing `GET /filters/{name}/raw`). -- Full service + router tests added; all 930 tests pass. - -**Files modified:** `app/models/config.py`, `app/services/config_file_service.py`, `app/routers/config.py`, `app/routers/file_config.py`, `tests/test_services/test_config_file_service.py`, `tests/test_routers/test_config.py`, `tests/test_routers/test_file_config.py` - ---- - -### Task 2.3 — Frontend: Filters Tab with Active/Inactive Display and Activation ✅ DONE - -**Goal:** Enhance the Filters tab in the Configuration page to show all filters with their active/inactive status and allow editing. - -**Details:** - -- Redesign the **Filters tab** to use the master/detail list layout described in [Features.md §6](Features.md). -- The **left pane** lists all filter names with an Active or Inactive badge. Active filters (those used by at least one enabled jail) are sorted to the top. Within each group, sort alphabetically. -- The badge should also show which jails use the filter (e.g., "Active — used by sshd, apache-auth"). -- Clicking a filter loads its detail in the **right pane**: `failregex` patterns (editable list), `ignoreregex` patterns (editable list), `datepattern` (editable), source file info, and whether a `.local` override exists. -- Add a **"Save Changes"** button that calls `PUT /api/config/filters/{name}` to persist edits to the `.local` override. Show save-state feedback (idle → saving → saved → error). -- Add an **"Assign to Jail"** button that opens a dialog where the user selects a jail and assigns this filter to it. This calls the assign-filter endpoint. -- Add a **"Create Filter"** button at the top of the list pane that opens a dialog for entering a new filter name and regex patterns. -- On narrow screens (< 900 px), collapse the list pane into a dropdown as specified in [Features.md §6](Features.md). -- Include the existing **Raw Configuration** collapsible section at the bottom of the detail pane for direct file editing. - -**Files to create/modify:** -- `frontend/src/components/config/FiltersTab.tsx` (rewrite with master/detail layout) -- `frontend/src/components/config/FilterForm.tsx` (update for editable fields) -- `frontend/src/api/config.ts` (add `fetchFilters`, `fetchFilter`, `updateFilter`, `createFilter`, `deleteFilter`, `assignFilterToJail`) -- `frontend/src/types/config.ts` (add types) -- New component: `frontend/src/components/config/AssignFilterDialog.tsx` - -**References:** [Features.md §6](Features.md), [Web-Design.md](Web-Design.md), [Web-Development.md](Web-Development.md) - ---- - -### Task 2.4 — Tests: Filter Discovery and Management ✅ DONE - -**Goal:** Test coverage for filter listing, editing, creation, and assignment. - -**Details:** - -- **Service tests**: Mock the `filter.d/` directory with sample `.conf` and `.local` files. Test parsing of `[Definition]` sections. Test merge of `.local` over `.conf`. Test active/inactive detection by cross-referencing with mock jail data. Test write operations create correct `.local` content. Test regex validation rejects bad patterns. -- **Router tests**: Test all new filter endpoints (list, detail, update, create, delete, assign). Test auth required. Test 404/409/422 responses. -- **Frontend tests**: Test the filter list rendering with mixed active/inactive items. Test the form submission in the detail pane. Test the assign dialog. - -**Files to create/modify:** -- `backend/tests/test_services/test_config_file_service.py` (extend) -- `backend/tests/test_routers/test_config.py` (extend) -- `frontend/src/components/config/__tests__/` (add filter tests) - ---- - -## Stage 3 — Action Configuration Discovery and Activation - -fail2ban ships with many action definitions in `action.d/` (iptables, firewalld, cloudflare, sendmail, etc.). Users need to see all available actions, understand which are in use, and assign them to jails. - -### Task 3.1 — Backend: List All Available Actions with Active/Inactive Status ✅ DONE - -**Goal:** Enumerate all action config files and mark each as active or inactive based on jail usage. - -**Details:** - -- Add a method `list_actions()` to `config_file_service.py` that scans the `action.d/` directory. -- For each `.conf` file, parse the `[Definition]` section to extract: `actionstart`, `actionstop`, `actioncheck`, `actionban`, `actionunban` commands. Also parse `[Init]` for default variable bindings (port, protocol, chain, etc.). -- Handle `.local` overrides the same way as filters. -- Determine active/inactive status: an action is "active" if its name appears in the `action` field of any currently enabled jail. Cross-reference against both running jails (from fail2ban) and the config files. -- Return `ActionConfig` objects with: `name`, `active` (bool), `used_by_jails` (list), `actionban` (command template), `actionunban` (command template), `actionstart`, `actionstop`, `init_params` (dict of Init variables), `source_file`, `has_local_override`. -- Add `GET /api/config/actions` endpoint returning all actions. -- Add `GET /api/config/actions/{name}` endpoint returning the full parsed detail of one action. - -**Files to create/modify:** -- `app/services/config_file_service.py` (add `list_actions`, `get_action`) -- `app/models/config.py` (add `ActionConfig`, `ActionListResponse`, `ActionDetailResponse`) -- `app/routers/config.py` (add endpoints) - -**References:** [Features.md §6](Features.md), [Architekture.md §2](Architekture.md) - ---- - -### Task 3.2 — Backend: Activate and Edit Actions ✅ DONE - -**Goal:** Allow users to assign actions to jails, edit action definitions, and create new actions. - -**Details:** - -- Add a `PUT /api/config/actions/{name}` endpoint that writes changes to an action's `.local` override file. Accepts updated `actionban`, `actionunban`, `actionstart`, `actionstop`, `actioncheck` values and any `[Init]` parameters. -- Add a `POST /api/config/jails/{jail_name}/action` endpoint that adds an action to a jail's action list. This writes the action reference into the jail's `.local` config file. Multiple actions per jail are supported (fail2ban allows comma-separated action lists). Include optional parameters for the action (e.g., port, protocol). -- Add a `DELETE /api/config/jails/{jail_name}/action/{action_name}` endpoint that removes an action from a jail's configuration. -- Add a `POST /api/config/actions` endpoint to create a brand-new action definition (`.local` file). -- Add a `DELETE /api/config/actions/{name}` endpoint to delete a custom action's `.local` file. Same safety rules as filters — refuse to delete shipped `.conf` files. -- After any write, optionally reload fail2ban (`?reload=true`). - -**Files to create/modify:** -- `app/services/config_file_service.py` (add action write/create/delete/assign methods) -- `app/routers/config.py` (add endpoints) -- `app/models/config.py` (add `ActionUpdateRequest`, `ActionCreateRequest`, `AssignActionRequest`) - -**References:** [Features.md §6](Features.md), [Backend-Development.md](Backend-Development.md) - ---- - -### Task 3.3 — Frontend: Actions Tab with Active/Inactive Display and Activation ✅ DONE - -**Goal:** Enhance the Actions tab in the Configuration page to show all actions with active/inactive status and allow editing and assignment. - -**Details:** - -- Redesign the **Actions tab** to use the same master/detail list layout as Filters. -- The **left pane** lists all action names with Active/Inactive badges. Active actions (used by at least one enabled jail) sorted to the top. -- Badge shows which jails reference the action (e.g., "Active — used by sshd, postfix"). -- Clicking an action loads its detail in the **right pane**: `actionban`, `actionunban`, `actionstart`, `actionstop`, `actioncheck` (each in a code/textarea editor), `[Init]` parameters as key-value fields. -- Add a **"Save Changes"** button for persisting edits to the `.local` override. -- Add an **"Assign to Jail"** button that opens a dialog for selecting a jail and providing action parameters (port, protocol, chain). Calls the assign-action endpoint. -- Add a **"Remove from Jail"** option in the detail pane — shows a list of jails currently using the action with a remove button next to each. -- Add a **"Create Action"** button at the top of the list pane. -- Raw Configuration collapsible section at the bottom. -- Responsive collapse on narrow screens. - -**Files to create/modify:** -- `frontend/src/components/config/ActionsTab.tsx` (rewrite with master/detail layout) -- `frontend/src/components/config/ActionForm.tsx` (update for editable fields) -- `frontend/src/api/config.ts` (add `fetchActions`, `fetchAction`, `updateAction`, `createAction`, `deleteAction`, `assignActionToJail`, `removeActionFromJail`) -- `frontend/src/types/config.ts` (add types) -- New component: `frontend/src/components/config/AssignActionDialog.tsx` - -**References:** [Features.md §6](Features.md), [Web-Design.md](Web-Design.md), [Web-Development.md](Web-Development.md) - ---- - -### Task 3.4 — Tests: Action Discovery and Management ✅ DONE - -**Goal:** Test coverage for action listing, editing, creation, and assignment. - -**Details:** - -- **Service tests**: Mock `action.d/` with sample configs. Test parsing of `[Definition]` and `[Init]` sections. Test active/inactive detection. Test write and create operations. Test delete safety (refuse `.conf` deletion). -- **Router tests**: Test all new action endpoints. Test auth, 404, 409, 422 responses. -- **Frontend tests**: Test action list rendering, form editing, assign dialog, remove-from-jail flow. - -**Files to create/modify:** -- `backend/tests/test_services/test_config_file_service.py` (extend) -- `backend/tests/test_routers/test_config.py` (extend) -- `frontend/src/components/config/__tests__/` (add action tests) - ---- - -## Stage 4 — Unified Config File Service and Shared Utilities - -### Task 4.1 — Config File Parser Utility ✅ DONE - -**Goal:** Build a robust, reusable parser for fail2ban INI-style config files that all config-related features share. - -**Details:** - -- Create `app/utils/config_parser.py` with a `Fail2BanConfigParser` class that wraps Python's `configparser.ConfigParser` with fail2ban-specific behaviour: - - **Merge order**: `.conf` file first, then `.local` overlay, then `*.d/` directory overrides. - - **Variable interpolation**: Support `%(variable)s` syntax, resolving against `[DEFAULT]` and `[Init]` sections. - - **Include directives**: Process `before = filename` and `after = filename` in `[INCLUDES]` sections, resolving paths relative to the config directory. - - **Multi-line values**: Handle backslash-continuation and multi-line regex lists. - - **Comments**: Strip `#` and `;` line/inline comments correctly. -- The parser should return structured `dict` data that the `config_file_service` methods consume. -- This is a pure utility — no I/O aside from reading files. Everything is synchronous, callable from async context via `run_in_executor`. -- Write comprehensive unit tests in `tests/test_utils/test_config_parser.py` covering all edge cases: empty sections, missing files, circular includes, invalid interpolation, multi-line regex, override merging. - -**Files to create/modify:** -- `app/utils/config_parser.py` (new) -- `tests/test_utils/test_config_parser.py` (new) - -**References:** [Backend-Development.md](Backend-Development.md) - ---- - -### Task 4.2 — Config File Writer Utility ✅ DONE - -**Goal:** Build a safe writer utility for creating and updating `.local` override files. - -**Details:** - -- Create `app/utils/config_writer.py` with functions for: - - `write_local_override(base_path, section, key_values)` — Writes or updates a `.local` file. If the file exists, update only the specified keys under the given section. If it does not exist, create it with a header comment explaining it's a BanGUI-managed override. - - `remove_local_key(base_path, section, key)` — Removes a specific key from a `.local` file. - - `delete_local_file(path)` — Deletes a `.local` file, but only if no corresponding `.conf` exists or the caller explicitly allows orphan deletion. -- All write operations must be atomic: write to a temporary file first, then rename into place (`os.replace`). This prevents corruption on crash. -- Add a file-level lock (per config file) to prevent concurrent writes from racing. -- Never write to `.conf` files — assert this in every write function. - -**Files to create/modify:** -- `app/utils/config_writer.py` (new) -- `tests/test_utils/test_config_writer.py` (new) - -**References:** [Backend-Development.md](Backend-Development.md) +### ✅ Task 5 — Fix Jail log paths: remove path text spans, populate editable fields + +**Status:** Done — Replaced the read-only `` elements for existing log paths with editable `` fields, each with an `onChange` handler that updates the corresponding index in `logPaths` state. Delete button preserved. + +**Goal:** In the active jail detail pane, the "Log Paths" field currently renders each existing log path as a read-only `` text element with a delete button. The actual `` field below is always empty and is only used for adding new paths. The user wants to remove the read-only text spans and instead make each log path an editable field. + +**Files to modify:** +- `frontend/src/components/config/JailsTab.tsx` + +**What to do:** +1. In the `JailConfigDetail` component, locate the `` block (around lines 292–350). +2. Remove the read-only span list that renders existing log paths: + ```tsx + {logPaths.length === 0 ? ( + (none) + ) : ( + logPaths.map((p) => ( +
+ {p} +
+ )) + )} + ``` +3. Replace it with editable `` fields for each existing log path. Each row should have: + - An `` pre-filled with the path value (`value={p}`), editable. + - An `onChange` handler that updates the corresponding entry in the `logPaths` state array. + - The same delete button (reuse the existing `handleDeleteLogPath` callback). +4. Keep the "Add new log path" form at the bottom unchanged (the `` with placeholder, `` for tail/head, and "Add" button). +5. Ensure the `logPaths` state is properly initialized from `jail.log_paths` (this is already the case at line 87). + +**Implementation hint:** Map over `logPaths` with an index, render an `` for each, and on change update the array at that index. Example: +```tsx +logPaths.map((p, i) => ( +
+ { + setLogPaths((prev) => prev.map((v, j) => (j === i ? d.value : v))); + }} + /> +
+)) +``` + +**Verification:** When selecting an active jail that has log paths configured, the paths appear as editable input fields (not plain text). The input values can be modified. The delete button still works. The "Add" form at the bottom still works for adding new paths. diff --git a/frontend/src/components/config/ActionsTab.tsx b/frontend/src/components/config/ActionsTab.tsx index 9c349e9..bbb46ac 100644 --- a/frontend/src/components/config/ActionsTab.tsx +++ b/frontend/src/components/config/ActionsTab.tsx @@ -12,7 +12,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { - Badge, Button, Field, Input, @@ -121,19 +120,6 @@ function ActionDetail({ size="small" />
-
- - {action.active ? "Active" : "Inactive"} - - {action.has_local_override && ( - - Has local override - - )} -
{/* Structured editor */} diff --git a/frontend/src/components/config/CreateJailDialog.tsx b/frontend/src/components/config/CreateJailDialog.tsx new file mode 100644 index 0000000..4849b37 --- /dev/null +++ b/frontend/src/components/config/CreateJailDialog.tsx @@ -0,0 +1,164 @@ +/** + * CreateJailDialog — dialog for creating a new jail configuration file. + * + * Asks for a config file name and calls ``POST /api/config/jail-files`` on + * confirmation, seeding the file with a minimal comment header. + */ + +import { useCallback, useEffect, useState } from "react"; +import { + Button, + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogSurface, + DialogTitle, + Field, + Input, + MessageBar, + MessageBarBody, + Spinner, + Text, + tokens, +} from "@fluentui/react-components"; +import { createJailConfigFile } from "../../api/config"; +import type { ConfFileCreateRequest } from "../../types/config"; +import { ApiError } from "../../api/client"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface CreateJailDialogProps { + /** Whether the dialog is currently open. */ + open: boolean; + /** Called when the dialog should close without taking action. */ + onClose: () => void; + /** Called after the jail config file has been successfully created. */ + onCreated: () => void; +} + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +/** + * Dialog for creating a new jail configuration file at + * ``jail.d/{name}.conf``. + * + * The name field accepts a plain base name (e.g. ``my-custom-jail``); the + * backend appends the ``.conf`` extension. + * + * @param props - Component props. + * @returns JSX element. + */ +export function CreateJailDialog({ + open, + onClose, + onCreated, +}: CreateJailDialogProps): React.JSX.Element { + const [name, setName] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + // Reset form when the dialog opens. + useEffect(() => { + if (open) { + setName(""); + setError(null); + } + }, [open]); + + const handleClose = useCallback((): void => { + if (submitting) return; + onClose(); + }, [submitting, onClose]); + + const handleConfirm = useCallback((): void => { + const trimmedName = name.trim(); + if (!trimmedName || submitting) return; + + const req: ConfFileCreateRequest = { + name: trimmedName, + content: `# ${trimmedName}\n`, + }; + + setSubmitting(true); + setError(null); + + createJailConfigFile(req) + .then(() => { + onCreated(); + }) + .catch((err: unknown) => { + setError( + err instanceof ApiError ? err.message : "Failed to create jail config.", + ); + }) + .finally(() => { + setSubmitting(false); + }); + }, [name, submitting, onCreated]); + + const canConfirm = name.trim() !== "" && !submitting; + + return ( + { if (!data.open) handleClose(); }}> + + + Create Config + + + Creates a new jail configuration file at{" "} + jail.d/<name>.conf. + + + {error !== null && ( + + {error} + + )} + + + { setName(d.value); }} + placeholder="my-custom-jail" + disabled={submitting} + /> + + + + + + + + + + ); +} diff --git a/frontend/src/components/config/FiltersTab.tsx b/frontend/src/components/config/FiltersTab.tsx index fae3a75..923d533 100644 --- a/frontend/src/components/config/FiltersTab.tsx +++ b/frontend/src/components/config/FiltersTab.tsx @@ -12,7 +12,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { - Badge, Button, Field, Input, @@ -95,19 +94,6 @@ function FilterDetail({ size="small" /> -
- - {filter.active ? "Active" : "Inactive"} - - {filter.has_local_override && ( - - Has local override - - )} -
{/* Structured editor */} diff --git a/frontend/src/components/config/JailsTab.tsx b/frontend/src/components/config/JailsTab.tsx index 81d9f4d..1dc3f76 100644 --- a/frontend/src/components/config/JailsTab.tsx +++ b/frontend/src/components/config/JailsTab.tsx @@ -22,7 +22,7 @@ import { tokens, } from "@fluentui/react-components"; import { - ArrowClockwise24Regular, + Add24Regular, Dismiss24Regular, LockClosed24Regular, LockOpen24Regular, @@ -50,6 +50,7 @@ import { useJailConfigs } from "../../hooks/useConfig"; import { ActivateJailDialog } from "./ActivateJailDialog"; import { AutoSaveIndicator } from "./AutoSaveIndicator"; import { ConfigListDetail } from "./ConfigListDetail"; +import { CreateJailDialog } from "./CreateJailDialog"; import { RawConfigSection } from "./RawConfigSection"; import { RegexList } from "./RegexList"; import { useConfigStyles } from "./configStyles"; @@ -295,11 +296,16 @@ function JailConfigDetail({ (none) ) : ( - logPaths.map((p) => ( -
- - {p} - + logPaths.map((p, i) => ( +
+ { + setLogPaths((prev) => prev.map((v, j) => (j === i ? d.value : v))); + }} + /> + ); + return (
-
- - -
- {reloadMsg && ( - - {reloadMsg} - - )} -
{selectedActiveJail !== undefined ? ( { setActivateTarget(null); }} onActivated={handleActivated} /> + + { setCreateDialogOpen(false); }} + onCreated={() => { + setCreateDialogOpen(false); + refresh(); + loadInactive(); + }} + />
); } diff --git a/frontend/src/components/config/index.ts b/frontend/src/components/config/index.ts index 584bf0b..1f8fe8a 100644 --- a/frontend/src/components/config/index.ts +++ b/frontend/src/components/config/index.ts @@ -24,6 +24,8 @@ export { CreateActionDialog } from "./CreateActionDialog"; export type { CreateActionDialogProps } from "./CreateActionDialog"; export { CreateFilterDialog } from "./CreateFilterDialog"; export type { CreateFilterDialogProps } from "./CreateFilterDialog"; +export { CreateJailDialog } from "./CreateJailDialog"; +export type { CreateJailDialogProps } from "./CreateJailDialog"; export { ExportTab } from "./ExportTab"; export { FilterForm } from "./FilterForm"; export type { FilterFormProps } from "./FilterForm"; diff --git a/frontend/src/pages/ConfigPage.tsx b/frontend/src/pages/ConfigPage.tsx index 7a6248d..c322bce 100644 --- a/frontend/src/pages/ConfigPage.tsx +++ b/frontend/src/pages/ConfigPage.tsx @@ -17,10 +17,8 @@ import { useState } from "react"; import { Tab, TabList, Text, makeStyles, tokens } from "@fluentui/react-components"; -import { DocumentArrowDown24Regular } from "@fluentui/react-icons"; import { ActionsTab, - ExportTab, FiltersTab, GlobalTab, JailsTab, @@ -62,8 +60,7 @@ type TabValue = | "global" | "server" | "map" - | "regex" - | "export"; + | "regex"; export function ConfigPage(): React.JSX.Element { const styles = useStyles(); @@ -94,7 +91,6 @@ export function ConfigPage(): React.JSX.Element { Server Map Regex Tester - }>Export
@@ -105,7 +101,6 @@ export function ConfigPage(): React.JSX.Element { {tab === "server" && } {tab === "map" && } {tab === "regex" && } - {tab === "export" && }
);