feat: implement API versioning /api/v1/

- All backend routers moved to /api/v1/ prefix
- Frontend BASE_URL updated to /api/v1
- Setup redirect middleware updated to redirect to /api/v1/setup
- Health router path fixed: prefix=/api/v1/health, @router.get('')
- conftest.py: set server_status=online for test fixture
- Created Docs/API_VERSIONING.md with deprecation policy
- Updated Docs/Backend-Development.md with versioning section
- Updated Instructions.md curl examples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-02 21:29:30 +02:00
parent 0d5882b32f
commit cc6dbcf3f0
51 changed files with 1886 additions and 671 deletions

View File

@@ -572,19 +572,19 @@ async def bans_by_country(
if app_db is None:
raise ValueError("app_db must be provided when source is 'archive'")
all_rows = await history_archive_repo.get_all_archived_history(
# SQL aggregation — no row materialisation into Python memory.
ip_counts = await history_archive_repo.get_ip_ban_counts(
db=app_db,
since=since,
origin=origin,
action="ban",
)
total = len(all_rows)
# Total = sum of all event counts.
total = sum(int(row["event_count"]) for row in ip_counts)
agg_rows = {}
for row in all_rows:
ip = str(row["ip"])
agg_rows[ip] = agg_rows.get(ip, 0) + 1
# {ip: event_count} for downstream geo aggregation.
agg_rows = {row["ip"]: int(row["event_count"]) for row in ip_counts}
unique_ips = list(agg_rows.keys())
else:
@@ -653,7 +653,7 @@ async def bans_by_country(
results = await asyncio.gather(*(_safe_lookup(ip) for ip in unique_ips))
geo_map = {ip: geo for ip, geo in results if geo is not None}
companion_rows: list[dict[str, object] | fail2ban_db_repo.BanRecord]
companion_rows: list[dict[str, Any] | fail2ban_db_repo.BanRecord]
if country_code is None:
if source == "archive":
companion_rows, _ = await history_archive_repo.get_archived_history(
@@ -681,12 +681,15 @@ async def bans_by_country(
if source == "archive":
if matched_ips:
companion_rows = await history_archive_repo.get_all_archived_history(
# Use keyset pagination instead of loading all matched IPs at once.
companion_rows, _ = await history_archive_repo.get_archived_history(
db=app_db,
since=since,
origin=origin,
action="ban",
ip_filter=matched_ips,
page=1,
page_size=_MAX_COMPANION_BANS,
)
else:
companion_rows = []
@@ -830,20 +833,16 @@ async def ban_trend(
if app_db is None:
raise ValueError("app_db must be provided when source is 'archive'")
all_rows = await history_archive_repo.get_all_archived_history(
# SQL aggregation — no row materialisation into Python memory.
counts = await history_archive_repo.get_ban_counts_by_bucket(
db=app_db,
since=since,
bucket_secs=bucket_secs,
num_buckets=num_buckets,
origin=origin,
action="ban",
)
counts: list[int] = [0] * num_buckets
for row in all_rows:
timeofban = int(row["timeofban"])
bucket_index = int((timeofban - since) / bucket_secs)
if 0 <= bucket_index < num_buckets:
counts[bucket_index] += 1
log.info(
"ban_service_ban_trend",
source=source,
@@ -928,22 +927,17 @@ async def bans_by_jail(
if app_db is None:
raise ValueError("app_db must be provided when source is 'archive'")
all_rows = await history_archive_repo.get_all_archived_history(
# SQL aggregation — no row materialisation into Python memory.
total, jail_rows = await history_archive_repo.get_jail_ban_counts(
db=app_db,
since=since,
origin=origin,
action="ban",
)
jail_counter: dict[str, int] = {}
for row in all_rows:
jail_name = str(row["jail"])
jail_counter[jail_name] = jail_counter.get(jail_name, 0) + 1
total = sum(jail_counter.values())
jail_counts = [
DomainJailBanCount(jail=jail_name, count=count)
for jail_name, count in sorted(jail_counter.items(), key=lambda x: x[1], reverse=True)
DomainJailBanCount(jail=str(row["jail"]), count=int(row["event_count"]))
for row in jail_rows
]
log.debug(