refactor(backend): clean up models setup, improve ip utils, add adr docs
- Extract ADR documents for architectural decisions (SQLite, FastAPI, React, APScheduler, Scheduler) - Refactor setup.py: improve code structure and readability - Add IP validation utilities with test coverage - Update frontend components (BanTable, HistoryPage) - Add pre-commit hooks and CONTRIBUTING.md - Add .editorconfig for consistent coding standards
This commit is contained in:
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
|
### Issue #28: LOW-MEDIUM - Missing Pre-Commit Hooks
|
||||||
|
|
||||||
**Where found**:
|
**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
|
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):
|
class SetupRequest(BanGuiBaseModel):
|
||||||
"""Payload for ``POST /api/setup``."""
|
"""Payload for ``POST /api/setup``."""
|
||||||
|
|
||||||
@@ -29,9 +98,9 @@ class SetupRequest(BanGuiBaseModel):
|
|||||||
if not any(char.isdigit() for char in value):
|
if not any(char.isdigit() for char in value):
|
||||||
raise ValueError("Password must include at least one number.")
|
raise ValueError("Password must include at least one number.")
|
||||||
if not any(char in "!@#$%^&*()" for char in value):
|
if not any(char in "!@#$%^&*()" for char in value):
|
||||||
raise ValueError(
|
raise ValueError("Password must include at least one special character (!@#$%^&*()).")
|
||||||
"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
|
return value
|
||||||
|
|
||||||
database_path: str = Field(
|
database_path: str = Field(
|
||||||
@@ -52,6 +121,7 @@ class SetupRequest(BanGuiBaseModel):
|
|||||||
description="Number of minutes a user session remains valid.",
|
description="Number of minutes a user session remains valid.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SetupResponse(BanGuiBaseModel):
|
class SetupResponse(BanGuiBaseModel):
|
||||||
"""Response returned after a successful initial setup."""
|
"""Response returned after a successful initial setup."""
|
||||||
|
|
||||||
@@ -59,11 +129,13 @@ class SetupResponse(BanGuiBaseModel):
|
|||||||
default="Setup completed successfully. Please log in.",
|
default="Setup completed successfully. Please log in.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SetupTimezoneResponse(BanGuiBaseModel):
|
class SetupTimezoneResponse(BanGuiBaseModel):
|
||||||
"""Response for ``GET /api/setup/timezone``."""
|
"""Response for ``GET /api/setup/timezone``."""
|
||||||
|
|
||||||
timezone: str = Field(..., description="Configured IANA timezone identifier.")
|
timezone: str = Field(..., description="Configured IANA timezone identifier.")
|
||||||
|
|
||||||
|
|
||||||
class SetupStatusResponse(BanGuiBaseModel):
|
class SetupStatusResponse(BanGuiBaseModel):
|
||||||
"""Response indicating whether setup has been completed."""
|
"""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.
|
"""Return a normalised string representation of an IP address.
|
||||||
|
|
||||||
IPv6 addresses are compressed to their canonical short form.
|
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:
|
Args:
|
||||||
address: A valid IP address string.
|
address: A valid IP address string.
|
||||||
@@ -69,7 +71,10 @@ def normalise_ip(address: str) -> str:
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: If *address* is not a valid IP address.
|
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:
|
def normalise_network(cidr: str) -> str:
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ class TestNormaliseIp:
|
|||||||
def test_normalise_ip_ipv6_compressed(self) -> None:
|
def test_normalise_ip_ipv6_compressed(self) -> None:
|
||||||
assert normalise_ip("2001:0db8:0000:0000:0000:0000:0000:0001") == "2001:db8::1"
|
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:
|
def test_normalise_ip_invalid_raises_value_error(self) -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
normalise_ip("not-an-ip")
|
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-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.6.1",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.1.0",
|
||||||
"openapi-typescript": "^7.13.0",
|
"openapi-typescript": "^7.13.0",
|
||||||
@@ -6152,6 +6153,22 @@
|
|||||||
"node": ">= 14"
|
"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": {
|
"node_modules/ignore": {
|
||||||
"version": "7.0.5",
|
"version": "7.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.6.1",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.1.0",
|
||||||
"openapi-typescript": "^7.13.0",
|
"openapi-typescript": "^7.13.0",
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ export const BanTable = memo(function BanTable({ timeRange, origin, source }: Ba
|
|||||||
items={banItems}
|
items={banItems}
|
||||||
columns={banColumns}
|
columns={banColumns}
|
||||||
getRowId={(item: DashboardBanItem) => `${item.ip}:${item.jail}:${item.banned_at}`}
|
getRowId={(item: DashboardBanItem) => `${item.ip}:${item.jail}:${item.banned_at}`}
|
||||||
|
aria-label="Ban records table"
|
||||||
>
|
>
|
||||||
<DataGridHeader>
|
<DataGridHeader>
|
||||||
<DataGridRow>
|
<DataGridRow>
|
||||||
|
|||||||
@@ -360,6 +360,7 @@ export function HistoryPage(): React.JSX.Element {
|
|||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
setCurrentPage(currentPage - 1);
|
setCurrentPage(currentPage - 1);
|
||||||
}}
|
}}
|
||||||
|
aria-label="Previous page"
|
||||||
/>
|
/>
|
||||||
<Text size={200}>
|
<Text size={200}>
|
||||||
Page {String(currentPage)} / {String(totalPages)}
|
Page {String(currentPage)} / {String(totalPages)}
|
||||||
@@ -372,6 +373,7 @@ export function HistoryPage(): React.JSX.Element {
|
|||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
setCurrentPage(currentPage + 1);
|
setCurrentPage(currentPage + 1);
|
||||||
}}
|
}}
|
||||||
|
aria-label="Next page"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user