refactoring-backend #3
33
.editorconfig
Normal file
33
.editorconfig
Normal file
@@ -0,0 +1,33 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{js,ts,tsx,jsx}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[Dockerfile]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.yaml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
cd frontend && npm run validate:types
|
||||
23
.pre-commit-config.yaml
Normal file
23
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: check-merge-conflict
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
hooks:
|
||||
- id: prettier
|
||||
args: [--check]
|
||||
name: prettier (frontend)
|
||||
files: ^frontend/
|
||||
entry: prettier --check
|
||||
language: system
|
||||
118
CONTRIBUTING.md
Normal file
118
CONTRIBUTING.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Contributing to BanGUI
|
||||
|
||||
Welcome! This guide covers everything you need to know to set up your dev environment, understand the codebase, and submit changes.
|
||||
|
||||
---
|
||||
|
||||
## Dev Setup
|
||||
|
||||
### 1 — Clone and init
|
||||
|
||||
```bash
|
||||
git clone <repo-url>
|
||||
cd BanGUI
|
||||
cp .env.example .env
|
||||
python -c 'import secrets; print(secrets.token_hex(32))'
|
||||
# paste output as BANGUI_SESSION_SECRET in .env
|
||||
```
|
||||
|
||||
### 2 — Start the stack
|
||||
|
||||
```bash
|
||||
make up
|
||||
```
|
||||
|
||||
Backend: http://127.0.0.1:8000 · Frontend (Vite proxy): http://127.0.0.1:5173
|
||||
|
||||
### 3 — Pre-commit hooks
|
||||
|
||||
**Backend** (pre-commit, all languages):
|
||||
|
||||
```bash
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
**Frontend** (husky, TypeScript validation):
|
||||
|
||||
```bash
|
||||
cd frontend && npm install
|
||||
npx husky install
|
||||
```
|
||||
|
||||
Hooks run automatically on every `git commit`. To run manually:
|
||||
|
||||
```bash
|
||||
pre-commit run --all-files # backend hooks
|
||||
cd frontend && npm run validate:types # frontend type check
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
BanGUI/
|
||||
├── backend/ Python FastAPI app
|
||||
│ └── app/
|
||||
│ ├── routers/ HTTP endpoint handlers
|
||||
│ ├── services/ Business logic
|
||||
│ ├── repos/ Data access
|
||||
│ ├── models/ Pydantic request/response/domain models
|
||||
│ └── utils/ Shared helpers
|
||||
├── frontend/ React + TypeScript + Fluent UI v9
|
||||
│ └── src/
|
||||
│ ├── pages/ Route-level page components
|
||||
│ ├── components/ Reusable UI components
|
||||
│ ├── hooks/ Custom React hooks
|
||||
│ └── types/ Shared TypeScript types
|
||||
├── Docs/ Architecture, design, and feature documentation
|
||||
└── Docker/ Container compose files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Quality
|
||||
|
||||
| Tool | Scope | Command |
|
||||
|---|---|---|
|
||||
| `ruff` | Backend linting | `cd backend && ruff check .` |
|
||||
| `ruff-format` | Backend formatting | `cd backend && ruff format .` |
|
||||
| `mypy --strict` | Backend type checking | `cd backend && mypy --strict app` |
|
||||
| `tsc --noEmit` | Frontend type checking | `cd frontend && tsc --noEmit` |
|
||||
| `eslint` | Frontend linting | `cd frontend && eslint src` |
|
||||
| `prettier --check` | Frontend formatting | `cd frontend && prettier --check src` |
|
||||
|
||||
**All checks must pass before committing.** CI runs the same suite.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
cd backend && pytest --cov=app --cov-report=term-missing
|
||||
|
||||
# Frontend — run once
|
||||
cd frontend && npm test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stack
|
||||
|
||||
| Layer | Stack |
|
||||
|---|---|
|
||||
| Backend | Python 3.12+, FastAPI, Pydantic v2, aiosqlite, structlog |
|
||||
| Frontend | TypeScript, React, Fluent UI v9, Vite |
|
||||
| Container | Docker Compose (development + production) |
|
||||
|
||||
---
|
||||
|
||||
## Key Docs
|
||||
|
||||
- [Instructions.md](Docs/Instructions.md) — Agent operating rules
|
||||
- [Backend-Development.md](Docs/Backend-Development.md) — Backend conventions
|
||||
- [Web-Development.md](Docs/Web-Development.md) — Frontend conventions
|
||||
- [Features.md](Docs/Features.md) — Complete feature list
|
||||
- [Architekture.md](Docs/Architekture.md) — System architecture
|
||||
@@ -1,46 +1,3 @@
|
||||
### Issue #27: MEDIUM - Inconsistent Error Handling Patterns
|
||||
|
||||
**Where found**:
|
||||
- `ban_service.py` - Raises exceptions
|
||||
- `server_service.py` - Returns defaults silently
|
||||
- `jail_service.py` - Mixed approach
|
||||
|
||||
**Why this is needed**:
|
||||
Different services have different error handling contracts. Callers don't know what to expect.
|
||||
|
||||
**Goal**:
|
||||
Establish clear error handling contract for all services.
|
||||
|
||||
**What to do**:
|
||||
1. Document error handling patterns:
|
||||
```python
|
||||
class ServiceErrorContract:
|
||||
"""
|
||||
ABORT_ON_ERROR: Raise exception, let router handle
|
||||
RETURN_DEFAULT: Return empty result, log warning
|
||||
PARTIAL_RESULT: Return partial success with error list
|
||||
"""
|
||||
```
|
||||
2. Each service method documents which pattern it follows
|
||||
3. Routers handle errors consistently
|
||||
4. Update service protocols
|
||||
|
||||
**Possible traps and issues**:
|
||||
- Users of service must know pattern
|
||||
- Switching patterns is breaking change
|
||||
|
||||
**Docs changes needed**:
|
||||
- Create service development guide with error patterns
|
||||
|
||||
**Doc references**:
|
||||
- DETAILED_FINDINGS.md - Issue "Inconsistent Error Handling"
|
||||
|
||||
---
|
||||
|
||||
## LOWER PRIORITY ISSUES (LOW-MEDIUM)
|
||||
|
||||
---
|
||||
|
||||
### Issue #28: LOW-MEDIUM - Missing Pre-Commit Hooks
|
||||
|
||||
**Where found**:
|
||||
|
||||
45
Docs/adr/ADR-001-SQLite-Application-Database.md
Normal file
45
Docs/adr/ADR-001-SQLite-Application-Database.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# ADR-001: SQLite as the Application Database
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
BanGUI needs a database to store application state: configuration, session records,
|
||||
blocklist sources, import logs, and ban history archives.
|
||||
|
||||
## Decision
|
||||
Use **SQLite** (via `aiosqlite`) as BanGUI's application database, persisted to a
|
||||
volume mounted from the host or a named Docker volume.
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why SQLite over PostgreSQL?
|
||||
- **Zero-infrastructure:** No separate DB server process, no connection pooling,
|
||||
no credentials to manage. Ships in the Docker container with no additional
|
||||
configuration.
|
||||
- **Fail2ban-compatible:** The fail2ban database itself is SQLite. BanGUI already
|
||||
depends on SQLite; adding a second database engine would increase operational
|
||||
complexity for no benefit.
|
||||
- **Single-instance deployment:** BanGUI runs as a single service with one
|
||||
background scheduler (enforced by `BANGUI_WORKERS=1`). Horizontal scaling is not
|
||||
a design goal.
|
||||
- **Async I/O:** `aiosqlite` provides full async support, avoiding blocking I/O in
|
||||
the FastAPI async request handlers.
|
||||
|
||||
### Why not PostgreSQL?
|
||||
- Requires a separate service or sidecar container.
|
||||
- Adds connection overhead (TCP, connection pools, auth).
|
||||
- Over-engineered for a single-instance web app.
|
||||
|
||||
### Trade-offs
|
||||
- **Not suitable for multi-worker deployments.** SQLite's file-level locking means
|
||||
only one process can write at a time. This is explicitly enforced:
|
||||
`BANGUI_WORKERS=1` is validated at startup, and `scheduler_lock` prevents
|
||||
duplicate schedulers in restarts.
|
||||
- For future multi-instance deployments, BanGUI would need to migrate to
|
||||
PostgreSQL or add a distributed lock layer (Redis + job store).
|
||||
|
||||
## Consequences
|
||||
- Application database is a single file (`bangui.db`) in the container's data volume.
|
||||
- Backup is a file copy. No `pg_dump` equivalent needed.
|
||||
- Schema migrations managed via `app/startup.py` startup DAG.
|
||||
45
Docs/adr/ADR-002-FastAPI-Backend-Framework.md
Normal file
45
Docs/adr/ADR-002-FastAPI-Backend-Framework.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# ADR-002: FastAPI over Django
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The backend requires a Python async web framework with strong typing, validation,
|
||||
and OpenAPI support.
|
||||
|
||||
## Decision
|
||||
Use **FastAPI** as the backend framework.
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why FastAPI over Django?
|
||||
- **Async-first:** FastAPI is built on Starlette with native `async def` route
|
||||
handlers. Django's ORM and request handling are synchronous, requiring thread
|
||||
pools for I/O-bound work.
|
||||
- **Modern Python 3.12+:** FastAPI embraces modern Python idioms — type annotations,
|
||||
structural pattern matching, dataclasses. Django maintains broad Python 3.8+
|
||||
compatibility and shows its age.
|
||||
- **Pydantic v2 integration:** FastAPI natively uses Pydantic for request/response
|
||||
validation. Automatic OpenAPI schema generation from Pydantic models is seamless.
|
||||
- **Dependency injection:** FastAPI's `Depends()` system provides a lightweight,
|
||||
explicit DI pattern without a separate container library.
|
||||
- **Performance:** FastAPI + Uvicorn consistently benchmarks as one of the fastest
|
||||
Python web frameworks, comparable to Node.js and Go for JSON APIs.
|
||||
|
||||
### Why not Django?
|
||||
- Django's synchronous ORM creates thread-pool bottlenecks with SQLite.
|
||||
- Django's "batteries-included" philosophy is overkill for BanGUI's scope.
|
||||
We need REST endpoints and background tasks, not a full CMS.
|
||||
- Less flexible dependency injection — Django's middleware and view system is
|
||||
less composable than FastAPI's routing layers.
|
||||
|
||||
### Trade-offs
|
||||
- **Smaller ecosystem:** Django has decades of third-party packages. FastAPI's
|
||||
ecosystem is younger but covers BanGUI's needs (structlog, aiosqlite, APScheduler,
|
||||
aiohttp) completely.
|
||||
- **No built-in admin UI:** BanGUI is its own admin UI; Django's admin is irrelevant.
|
||||
|
||||
## Consequences
|
||||
- FastAPI routes are defined in `app/routers/`.
|
||||
- Request/response models live in `app/models/`.
|
||||
- Dependency injection via `app/dependencies.py`.
|
||||
37
Docs/adr/ADR-003-React-Frontend-Framework.md
Normal file
37
Docs/adr/ADR-003-React-Frontend-Framework.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# ADR-003: React over Vue
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The frontend requires a component-based SPA framework with strong typing, a
|
||||
battle-tested component library, and broad ecosystem support.
|
||||
|
||||
## Decision
|
||||
Use **React 18+ with TypeScript** as the frontend framework.
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why React over Vue?
|
||||
- **Ecosystem maturity:** React has the largest frontend ecosystem. Libraries
|
||||
(date pickers, data grids, rich text editors) assume React availability first.
|
||||
- **Fluent UI v9:** Microsoft's official React component library is built for React.
|
||||
The Vue-compatible version (Fluent UI Vue) lags significantly in features and
|
||||
maintenance.
|
||||
- **Hiring and onboarding:** React is more widely known. New contributors are
|
||||
more likely to arrive with React experience than Vue experience.
|
||||
- **Concurrent features:** React 18's concurrent rendering (`useTransition`,
|
||||
`useDeferredValue`) provides a foundation for performance improvements in
|
||||
data-heavy views like the ban table.
|
||||
|
||||
### Why not Vue?
|
||||
- Fluent UI v9 does not provide first-class Vue support.
|
||||
- Vue's composition API is well-designed, but does not outweigh the Fluent UI
|
||||
constraint.
|
||||
- Ecosystem and hiring advantages strongly favor React for enterprise-adjacent
|
||||
projects.
|
||||
|
||||
## Consequences
|
||||
- Frontend is a React SPA in `frontend/src/`.
|
||||
- All components are functional components using hooks.
|
||||
- Global state via React context (`frontend/src/providers/`).
|
||||
48
Docs/adr/ADR-004-APScheduler-Background-Scheduler.md
Normal file
48
Docs/adr/ADR-004-APScheduler-Background-Scheduler.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# ADR-004: APScheduler over Celery
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
BanGUI requires background task scheduling for periodic work: geo cache flush,
|
||||
session cleanup, history sync, and blocklist imports.
|
||||
|
||||
## Decision
|
||||
Use **APScheduler 4.x (AsyncIOScheduler)** for background scheduling.
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why APScheduler over Celery?
|
||||
- **No infrastructure:** Celery requires a message broker (Redis or RabbitMQ).
|
||||
APScheduler runs in-process with no broker. Given BanGUI's single-instance
|
||||
constraint, a message queue adds unnecessary operational complexity.
|
||||
- **Async-native:** `AsyncIOScheduler` integrates directly with the asyncio event
|
||||
loop. All BanGUI's I/O (database, HTTP, fail2ban socket) is async. APScheduler
|
||||
jobs are `async def` functions that `await` without blocking.
|
||||
- **Simplicity:** BanGUI's job set is fixed and small. Celery's rich task routing,
|
||||
retry policies, and distributed execution are overkill. APScheduler covers
|
||||
cron-style scheduling with simpler semantics.
|
||||
- **Single-instance enforcement:** APScheduler's in-memory job store is a natural
|
||||
fit when there is only one scheduler. No distributed coordination needed.
|
||||
|
||||
### Why not Celery?
|
||||
- Celery's architecture (broker + workers + result backend) is designed for
|
||||
distributed systems. BanGUI is explicitly single-instance.
|
||||
- Celery tasks are synchronous wrappers around async code without careful
|
||||
handling. Native `async def` tasks require `async_task()` or explicit `run_sync`,
|
||||
creating friction in an async-first codebase.
|
||||
- Added operational burden: Redis or RabbitMQ must be available at startup.
|
||||
|
||||
### Trade-offs
|
||||
- **No horizontal scaling of workers:** APScheduler jobs run in the single
|
||||
uvicorn worker process. CPU-intensive jobs would block the event loop.
|
||||
(This is not a concern for BanGUI's I/O-bound jobs.)
|
||||
- **No built-in retry mechanism:** Failed jobs must re-raise exceptions or
|
||||
implement retry logic manually. This is acceptable given BanGUI's job
|
||||
idempotency guarantees.
|
||||
|
||||
## Consequences
|
||||
- Scheduler is configured in `app/startup.py` using `AsyncIOScheduler`.
|
||||
- Jobs live in `app/tasks/`.
|
||||
- Single-worker constraint is enforced via `BANGUI_WORKERS=1` validation and
|
||||
the `scheduler_lock` database table.
|
||||
61
Docs/adr/ADR-005-Single-Instance-Scheduler.md
Normal file
61
Docs/adr/ADR-005-Single-Instance-Scheduler.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# ADR-005: Single-Instance Scheduler Enforcement
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
APScheduler's `AsyncIOScheduler` is bound to a single asyncio event loop.
|
||||
Running multiple scheduler instances leads to duplicate jobs, database lock
|
||||
contention, and undefined behaviour.
|
||||
|
||||
## Decision
|
||||
Enforce exactly **one scheduler instance** across the entire application lifecycle,
|
||||
using a database-level distributed lock.
|
||||
|
||||
## Mechanism
|
||||
|
||||
### 1. Startup gate: `BANGUI_WORKERS=1`
|
||||
The Docker compose file is configured with `BANGUI_WORKERS=1` and the startup DAG
|
||||
validates this variable. If the variable is not set to `1`, startup aborts with
|
||||
a clear error message.
|
||||
|
||||
### 2. Runtime lock: `scheduler_lock` table
|
||||
During startup, after opening the SQLite database, the application attempts:
|
||||
|
||||
```sql
|
||||
INSERT INTO scheduler_lock (lock_name, heartbeat_at)
|
||||
VALUES ('scheduler', unixepoch())
|
||||
ON CONFLICT(lock_name) DO UPDATE SET heartbeat_at = unixepoch()
|
||||
WHERE (unixepoch() - heartbeat_at) < 30;
|
||||
```
|
||||
|
||||
- If the INSERT succeeds, this instance holds the lock and starts the scheduler.
|
||||
- If the INSERT is a no-op (heartbeat is recent), another instance holds the lock
|
||||
and startup continues without starting the scheduler.
|
||||
- A background task (`scheduler_lock_heartbeat`) updates the heartbeat every 10
|
||||
seconds. If the process crashes, the lock expires after 30 seconds, allowing
|
||||
a restart to acquire it immediately.
|
||||
|
||||
### 3. Deployment topology
|
||||
| Deployment | Behaviour |
|
||||
|---|---|
|
||||
| Single container | Scheduler runs normally |
|
||||
| Single Pod (Kubernetes) | Scheduler runs normally |
|
||||
| Accidental multi-process restart | Second process fails to start scheduler; first continues |
|
||||
| Intentional multi-worker | Not supported; requires external job store (future) |
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why this approach?
|
||||
- **No external coordination service:** No ZooKeeper, etcd, or Redis needed.
|
||||
The existing SQLite database is reused.
|
||||
- **Atomic:** SQLite's INSERT with ON CONFLICT is atomic; no race condition.
|
||||
- **Self-healing:** Lock expiry means a crashed instance automatically releases
|
||||
its lock. No manual cleanup required.
|
||||
- **Crash-safe:** A heartbeat-based TTL ensures stale locks are not held
|
||||
indefinitely.
|
||||
|
||||
## Consequences
|
||||
- `BANGUI_WORKERS` must always be `1`. This is documented and enforced.
|
||||
- Future multi-worker deployments require migration to a persistent job store
|
||||
(PostgreSQL + SQLAlchemy job store, or Redis).
|
||||
@@ -7,6 +7,75 @@ from pydantic import Field, field_validator
|
||||
|
||||
from app.models.response import BanGuiBaseModel
|
||||
|
||||
# Top-50 most-common plaintext passwords (lower-case).
|
||||
# Source: aggregated public breach compilations (Have I Been Pwned, Wikipedia).
|
||||
# Covers passwords that pass structural checks (uppercase + digit + special char)
|
||||
# but are trivial to guess.
|
||||
_COMMON_PASSWORDS: frozenset[str] = frozenset(
|
||||
{
|
||||
"password",
|
||||
"password1",
|
||||
"password123",
|
||||
"password1234",
|
||||
"password!",
|
||||
"letmein",
|
||||
"welcome",
|
||||
"admin",
|
||||
"admin123",
|
||||
"administrator",
|
||||
"qwerty",
|
||||
"qwerty123",
|
||||
"qwerty1234",
|
||||
"abc123",
|
||||
"abcdef",
|
||||
"123456",
|
||||
"1234567",
|
||||
"12345678",
|
||||
"123456789",
|
||||
"1234567890",
|
||||
"iloveyou",
|
||||
"iloveyou1",
|
||||
"monkey",
|
||||
"dragon",
|
||||
"master",
|
||||
"login",
|
||||
"login123",
|
||||
"passw0rd",
|
||||
"passw0rd!",
|
||||
"changeme",
|
||||
"default",
|
||||
"guest",
|
||||
"guest123",
|
||||
"fuckyou",
|
||||
"fuckyou1",
|
||||
"shit",
|
||||
"asshole",
|
||||
"hello",
|
||||
"hello123",
|
||||
"hello!",
|
||||
"world",
|
||||
"pass",
|
||||
"test",
|
||||
"test123",
|
||||
"test!",
|
||||
"root",
|
||||
"root123",
|
||||
"p@ssword",
|
||||
"p@ssword1",
|
||||
"p@ssw0rd",
|
||||
"p@ssw0rd!",
|
||||
"sunshine",
|
||||
"princess",
|
||||
"shadow",
|
||||
"shadow123",
|
||||
"access",
|
||||
"access123",
|
||||
"mypass",
|
||||
"mypass123",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SetupRequest(BanGuiBaseModel):
|
||||
"""Payload for ``POST /api/setup``."""
|
||||
|
||||
@@ -29,9 +98,9 @@ class SetupRequest(BanGuiBaseModel):
|
||||
if not any(char.isdigit() for char in value):
|
||||
raise ValueError("Password must include at least one number.")
|
||||
if not any(char in "!@#$%^&*()" for char in value):
|
||||
raise ValueError(
|
||||
"Password must include at least one special character (!@#$%^&*())."
|
||||
)
|
||||
raise ValueError("Password must include at least one special character (!@#$%^&*()).")
|
||||
if value.lower() in _COMMON_PASSWORDS:
|
||||
raise ValueError("Password is too common. Choose something more unique.")
|
||||
return value
|
||||
|
||||
database_path: str = Field(
|
||||
@@ -52,6 +121,7 @@ class SetupRequest(BanGuiBaseModel):
|
||||
description="Number of minutes a user session remains valid.",
|
||||
)
|
||||
|
||||
|
||||
class SetupResponse(BanGuiBaseModel):
|
||||
"""Response returned after a successful initial setup."""
|
||||
|
||||
@@ -59,11 +129,13 @@ class SetupResponse(BanGuiBaseModel):
|
||||
default="Setup completed successfully. Please log in.",
|
||||
)
|
||||
|
||||
|
||||
class SetupTimezoneResponse(BanGuiBaseModel):
|
||||
"""Response for ``GET /api/setup/timezone``."""
|
||||
|
||||
timezone: str = Field(..., description="Configured IANA timezone identifier.")
|
||||
|
||||
|
||||
class SetupStatusResponse(BanGuiBaseModel):
|
||||
"""Response indicating whether setup has been completed."""
|
||||
|
||||
|
||||
@@ -58,7 +58,9 @@ def normalise_ip(address: str) -> str:
|
||||
"""Return a normalised string representation of an IP address.
|
||||
|
||||
IPv6 addresses are compressed to their canonical short form.
|
||||
IPv4 addresses are returned unchanged.
|
||||
IPv4-mapped IPv6 addresses (e.g. ``::ffff:192.168.1.1``) are converted
|
||||
to their IPv4 equivalent (``192.168.1.1``).
|
||||
Plain IPv4 addresses are returned unchanged.
|
||||
|
||||
Args:
|
||||
address: A valid IP address string.
|
||||
@@ -69,7 +71,10 @@ def normalise_ip(address: str) -> str:
|
||||
Raises:
|
||||
ValueError: If *address* is not a valid IP address.
|
||||
"""
|
||||
return str(ipaddress.ip_address(address))
|
||||
ip = ipaddress.ip_address(address)
|
||||
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
|
||||
return str(ip.ipv4_mapped)
|
||||
return str(ip)
|
||||
|
||||
|
||||
def normalise_network(cidr: str) -> str:
|
||||
|
||||
@@ -77,6 +77,9 @@ class TestNormaliseIp:
|
||||
def test_normalise_ip_ipv6_compressed(self) -> None:
|
||||
assert normalise_ip("2001:0db8:0000:0000:0000:0000:0000:0001") == "2001:db8::1"
|
||||
|
||||
def test_normalise_ip_ipv4_mapped_ipv6_to_ipv4(self) -> None:
|
||||
assert normalise_ip("::ffff:192.168.1.1") == "192.168.1.1"
|
||||
|
||||
def test_normalise_ip_invalid_raises_value_error(self) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
normalise_ip("not-an-ip")
|
||||
|
||||
17
frontend/package-lock.json
generated
17
frontend/package-lock.json
generated
@@ -37,6 +37,7 @@
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.6.1",
|
||||
"jsdom": "^28.1.0",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
@@ -6152,6 +6153,22 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/husky": {
|
||||
"version": "9.1.7",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"husky": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.6.1",
|
||||
"jsdom": "^28.1.0",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
|
||||
@@ -241,6 +241,7 @@ export const BanTable = memo(function BanTable({ timeRange, origin, source }: Ba
|
||||
items={banItems}
|
||||
columns={banColumns}
|
||||
getRowId={(item: DashboardBanItem) => `${item.ip}:${item.jail}:${item.banned_at}`}
|
||||
aria-label="Ban records table"
|
||||
>
|
||||
<DataGridHeader>
|
||||
<DataGridRow>
|
||||
|
||||
@@ -360,6 +360,7 @@ export function HistoryPage(): React.JSX.Element {
|
||||
onClick={(): void => {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}}
|
||||
aria-label="Previous page"
|
||||
/>
|
||||
<Text size={200}>
|
||||
Page {String(currentPage)} / {String(totalPages)}
|
||||
@@ -372,6 +373,7 @@ export function HistoryPage(): React.JSX.Element {
|
||||
onClick={(): void => {
|
||||
setCurrentPage(currentPage + 1);
|
||||
}}
|
||||
aria-label="Next page"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user