Compare commits
1 Commits
v0.9.19-rc
...
025c82a982
| Author | SHA1 | Date | |
|---|---|---|---|
| 025c82a982 |
@@ -18,7 +18,7 @@ WORKDIR /build
|
|||||||
COPY frontend/package.json frontend/package-lock.json* /build/
|
COPY frontend/package.json frontend/package-lock.json* /build/
|
||||||
RUN npm ci --ignore-scripts
|
RUN npm ci --ignore-scripts
|
||||||
|
|
||||||
# Copy source + local OpenAPI spec (avoids needing a running backend during build)
|
# Copy source and build
|
||||||
COPY frontend/ /build/
|
COPY frontend/ /build/
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v0.9.19-rc.5
|
v0.9.19
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
# BanGUI — Production Compose
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# docker compose -f Docker/compose.prod.yml up -d
|
|
||||||
# podman compose -f Docker/compose.prod.yml up -d
|
|
||||||
#
|
|
||||||
# Features:
|
|
||||||
# - Multi-stage built images (no volume-mounted source code)
|
|
||||||
# - Frontend served by nginx with API reverse proxy
|
|
||||||
# - Backend running uvicorn without --reload
|
|
||||||
# - Only port 8080 exposed to host
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
name: bangui
|
|
||||||
|
|
||||||
services:
|
|
||||||
# ── fail2ban ─────────────────────────────────────────────────
|
|
||||||
fail2ban:
|
|
||||||
image: lscr.io/linuxserver/fail2ban:latest
|
|
||||||
container_name: bangui-fail2ban
|
|
||||||
restart: unless-stopped
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
- NET_RAW
|
|
||||||
network_mode: host
|
|
||||||
environment:
|
|
||||||
TZ: "${BANGUI_TIMEZONE:-UTC}"
|
|
||||||
PUID: 0
|
|
||||||
PGID: 0
|
|
||||||
volumes:
|
|
||||||
- ../data/fail2ban-dev-config:/config
|
|
||||||
- fail2ban-run:/var/run/fail2ban
|
|
||||||
- /var/log:/var/log:ro
|
|
||||||
- ../data/log:/remotelogs/bangui
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "fail2ban-client", "ping"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
start_period: 15s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
# ── Backend (FastAPI + uvicorn) ─────────────────────────────
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: ..
|
|
||||||
dockerfile: Docker/Dockerfile.backend
|
|
||||||
target: runtime
|
|
||||||
container_name: bangui-backend
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
fail2ban:
|
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
|
||||||
BANGUI_DATABASE_PATH: "/data/bangui.db"
|
|
||||||
BANGUI_FAIL2BAN_SOCKET: "/var/run/fail2ban/fail2ban.sock"
|
|
||||||
BANGUI_FAIL2BAN_CONFIG_DIR: "/config/fail2ban"
|
|
||||||
BANGUI_LOG_FILE: "/data/log/bangui.log"
|
|
||||||
BANGUI_LOG_LEVEL: "${BANGUI_LOG_LEVEL:-info}"
|
|
||||||
BANGUI_SESSION_SECRET: "${BANGUI_SESSION_SECRET:?BANGUI_SESSION_SECRET must be set — generate with: python -c 'import secrets; print(secrets.token_hex(32))'}"
|
|
||||||
BANGUI_TIMEZONE: "${BANGUI_TIMEZONE:-UTC}"
|
|
||||||
BANGUI_SESSION_COOKIE_SECURE: "${BANGUI_SESSION_COOKIE_SECURE:-true}"
|
|
||||||
BANGUI_CORS_ALLOWED_ORIGINS: "${BANGUI_CORS_ALLOWED_ORIGINS:-}"
|
|
||||||
volumes:
|
|
||||||
- ../data:/data
|
|
||||||
- ../fail2ban-master:/app/fail2ban-master:ro
|
|
||||||
- fail2ban-run:/var/run/fail2ban:ro
|
|
||||||
- ../data/fail2ban-dev-config:/config:rw
|
|
||||||
networks:
|
|
||||||
- bangui-net
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:8000/api/v1/health/live || exit 1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
start_period: 40s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
# ── Frontend (nginx serving built SPA) ──────────────────────
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ..
|
|
||||||
dockerfile: Docker/Dockerfile.frontend
|
|
||||||
container_name: bangui-frontend
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
backend:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
|
||||||
- "${BANGUI_PORT:-8080}:80"
|
|
||||||
networks:
|
|
||||||
- bangui-net
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "wget -qO /dev/null http://localhost:80/ || exit 1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
start_period: 5s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
fail2ban-run:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
|
||||||
bangui-net:
|
|
||||||
driver: bridge
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
# ./release.sh
|
# ./release.sh
|
||||||
#
|
#
|
||||||
# The current version is stored in VERSION (next to this script).
|
# The current version is stored in VERSION (next to this script).
|
||||||
# You will be asked whether to bump major, minor, patch, or release candidate (rc).
|
# You will be asked whether to bump major, minor, or patch.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -24,60 +24,24 @@ CURRENT="$(cat "${VERSION_FILE}")"
|
|||||||
# Strip leading 'v' for arithmetic
|
# Strip leading 'v' for arithmetic
|
||||||
VERSION="${CURRENT#v}"
|
VERSION="${CURRENT#v}"
|
||||||
|
|
||||||
# Parse version: X.Y.Z or X.Y.Z-rc.N
|
IFS='.' read -r MAJOR MINOR PATCH <<< "${VERSION}"
|
||||||
if [[ "${VERSION}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-rc\.([0-9]+))?$ ]]; then
|
|
||||||
MAJOR="${BASH_REMATCH[1]}"
|
|
||||||
MINOR="${BASH_REMATCH[2]}"
|
|
||||||
PATCH="${BASH_REMATCH[3]}"
|
|
||||||
RC_SUFFIX="${BASH_REMATCH[4]:-}"
|
|
||||||
RC_NUM="${BASH_REMATCH[5]:-0}"
|
|
||||||
else
|
|
||||||
echo "Error: version '${VERSION}' does not match expected format X.Y.Z or X.Y.Z-rc.N" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo " BanGUI — Release"
|
echo " BanGUI — Release"
|
||||||
if [[ -n "${RC_SUFFIX}" ]]; then
|
echo " Current version: v${MAJOR}.${MINOR}.${PATCH}"
|
||||||
echo " Current version: v${MAJOR}.${MINOR}.${PATCH}-rc.${RC_NUM}"
|
|
||||||
else
|
|
||||||
echo " Current version: v${MAJOR}.${MINOR}.${PATCH}"
|
|
||||||
fi
|
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo ""
|
echo ""
|
||||||
echo "How would you like to bump the version?"
|
echo "How would you like to bump the version?"
|
||||||
if [[ -n "${RC_SUFFIX}" ]]; then
|
echo " 1) patch (v${MAJOR}.${MINOR}.${PATCH} → v${MAJOR}.${MINOR}.$((PATCH + 1)))"
|
||||||
echo " 1) patch (v${MAJOR}.${MINOR}.${PATCH}-rc.${RC_NUM} → v${MAJOR}.${MINOR}.${PATCH})"
|
echo " 2) minor (v${MAJOR}.${MINOR}.${PATCH} → v${MAJOR}.$((MINOR + 1)).0)"
|
||||||
echo " 2) minor (v${MAJOR}.${MINOR}.${PATCH}-rc.${RC_NUM} → v${MAJOR}.$((MINOR + 1)).0)"
|
echo " 3) major (v${MAJOR}.${MINOR}.${PATCH} → v$((MAJOR + 1)).0.0)"
|
||||||
echo " 3) major (v${MAJOR}.${MINOR}.${PATCH}-rc.${RC_NUM} → v$((MAJOR + 1)).0.0)"
|
|
||||||
echo " 4) rc (v${MAJOR}.${MINOR}.${PATCH}-rc.${RC_NUM} → v${MAJOR}.${MINOR}.${PATCH}-rc.$((RC_NUM + 1)))"
|
|
||||||
else
|
|
||||||
echo " 1) patch (v${MAJOR}.${MINOR}.${PATCH} → v${MAJOR}.${MINOR}.$((PATCH + 1)))"
|
|
||||||
echo " 2) minor (v${MAJOR}.${MINOR}.${PATCH} → v${MAJOR}.$((MINOR + 1)).0)"
|
|
||||||
echo " 3) major (v${MAJOR}.${MINOR}.${PATCH} → v$((MAJOR + 1)).0.0)"
|
|
||||||
echo " 4) rc (v${MAJOR}.${MINOR}.${PATCH} → v${MAJOR}.${MINOR}.${PATCH}-rc.1)"
|
|
||||||
fi
|
|
||||||
echo ""
|
echo ""
|
||||||
read -rp "Enter choice [1/2/3/4]: " CHOICE
|
read -rp "Enter choice [1/2/3]: " CHOICE
|
||||||
|
|
||||||
case "${CHOICE}" in
|
case "${CHOICE}" in
|
||||||
1)
|
1) NEW_TAG="v${MAJOR}.${MINOR}.$((PATCH + 1))" ;;
|
||||||
if [[ -n "${RC_SUFFIX}" ]]; then
|
|
||||||
# Release the RC: strip RC suffix
|
|
||||||
NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
|
|
||||||
else
|
|
||||||
NEW_TAG="v${MAJOR}.${MINOR}.$((PATCH + 1))"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
2) NEW_TAG="v${MAJOR}.$((MINOR + 1)).0" ;;
|
2) NEW_TAG="v${MAJOR}.$((MINOR + 1)).0" ;;
|
||||||
3) NEW_TAG="v$((MAJOR + 1)).0.0" ;;
|
3) NEW_TAG="v$((MAJOR + 1)).0.0" ;;
|
||||||
4)
|
|
||||||
if [[ "${RC_NUM}" -gt 0 ]]; then
|
|
||||||
NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}-rc.$((RC_NUM + 1))"
|
|
||||||
else
|
|
||||||
NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}-rc.1"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "Invalid choice. Aborting." >&2
|
echo "Invalid choice. Aborting." >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -117,13 +81,7 @@ fi
|
|||||||
# Push containers
|
# Push containers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
bash "${SCRIPT_DIR}/push.sh" "${NEW_TAG}"
|
bash "${SCRIPT_DIR}/push.sh" "${NEW_TAG}"
|
||||||
|
bash "${SCRIPT_DIR}/push.sh"
|
||||||
# Push to "latest" or "latestRC" depending on whether this is a release candidate
|
|
||||||
if [[ "${NEW_TAG}" == *-rc* ]]; then
|
|
||||||
bash "${SCRIPT_DIR}/push.sh" "latestRC"
|
|
||||||
else
|
|
||||||
bash "${SCRIPT_DIR}/push.sh" "latest"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -102,15 +102,10 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Ordered list of DDL statements to execute on initialisation.
|
# Ordered list of DDL statements to execute on initialisation.
|
||||||
# NOTE: _CREATE_SESSIONS_TOKEN_INDEX is intentionally omitted here.
|
|
||||||
# The old 0.8.0 schema has a `sessions.token` column (not `token_hash`), so
|
|
||||||
# running CREATE INDEX … ON sessions (token_hash) in migration 1 would fail
|
|
||||||
# with "no such column: token_hash" on legacy databases. Migration 2 drops
|
|
||||||
# and recreates the sessions table with token_hash and also creates the index,
|
|
||||||
# so there is no need to create it in migration 1.
|
|
||||||
_SCHEMA_STATEMENTS: list[str] = [
|
_SCHEMA_STATEMENTS: list[str] = [
|
||||||
_CREATE_SETTINGS,
|
_CREATE_SETTINGS,
|
||||||
_CREATE_SESSIONS,
|
_CREATE_SESSIONS,
|
||||||
|
_CREATE_SESSIONS_TOKEN_INDEX,
|
||||||
_CREATE_BLOCKLIST_SOURCES,
|
_CREATE_BLOCKLIST_SOURCES,
|
||||||
_CREATE_IMPORT_LOG,
|
_CREATE_IMPORT_LOG,
|
||||||
_CREATE_GEO_CACHE,
|
_CREATE_GEO_CACHE,
|
||||||
@@ -138,24 +133,8 @@ CREATE UNIQUE INDEX idx_sessions_token_hash ON sessions (token_hash);
|
|||||||
3: """
|
3: """
|
||||||
-- Migration 3: Add last_seen timestamp to geo_cache for retention policy.
|
-- Migration 3: Add last_seen timestamp to geo_cache for retention policy.
|
||||||
-- Tracks when each IP was last referenced to enable purging of stale entries.
|
-- Tracks when each IP was last referenced to enable purging of stale entries.
|
||||||
-- SQLite rejects ALTER TABLE ADD COLUMN with a non-constant NOT NULL default
|
-- Default to current timestamp for existing rows.
|
||||||
-- when the table already contains rows, so we rebuild the table instead.
|
ALTER TABLE geo_cache ADD COLUMN last_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
|
||||||
-- Existing rows receive last_seen = cached_at as a reasonable approximation
|
|
||||||
-- (the IP was at least seen when it was first cached).
|
|
||||||
DROP TABLE IF EXISTS geo_cache_new;
|
|
||||||
CREATE TABLE geo_cache_new (
|
|
||||||
ip TEXT PRIMARY KEY,
|
|
||||||
country_code TEXT,
|
|
||||||
country_name TEXT,
|
|
||||||
asn TEXT,
|
|
||||||
org TEXT,
|
|
||||||
cached_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
||||||
last_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
||||||
);
|
|
||||||
INSERT INTO geo_cache_new (ip, country_code, country_name, asn, org, cached_at, last_seen)
|
|
||||||
SELECT ip, country_code, country_name, asn, org, cached_at, cached_at FROM geo_cache;
|
|
||||||
DROP TABLE geo_cache;
|
|
||||||
ALTER TABLE geo_cache_new RENAME TO geo_cache;
|
|
||||||
""",
|
""",
|
||||||
4: """
|
4: """
|
||||||
-- Migration 4: Add scheduler_lock table for multi-worker safety.
|
-- Migration 4: Add scheduler_lock table for multi-worker safety.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "bangui-backend"
|
name = "bangui-backend"
|
||||||
version = "0.9.19-rc.4"
|
version = "0.9.19"
|
||||||
description = "BanGUI backend — fail2ban web management interface"
|
description = "BanGUI backend — fail2ban web management interface"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
147
check_auth.py
147
check_auth.py
@@ -1,147 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Diagnostic script for BanGUI auth/session 401 issue.
|
|
||||||
|
|
||||||
Tests the full auth flow against http://192.168.178.43:8080/api/v1/auth
|
|
||||||
using password "Hallo123!".
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python3 check_auth.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import urllib.error
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
BASE_URL = "http://192.168.178.43:8080/api/v1"
|
|
||||||
PASSWORD = "Hallo123!"
|
|
||||||
|
|
||||||
|
|
||||||
def make_request(url, method="GET", data=None, headers=None, cookie=None):
|
|
||||||
"""Make an HTTP request and return (status, headers, body, cookies)."""
|
|
||||||
req_headers = headers or {}
|
|
||||||
if data:
|
|
||||||
req_headers["Content-Type"] = "application/json"
|
|
||||||
if cookie:
|
|
||||||
req_headers["Cookie"] = cookie
|
|
||||||
|
|
||||||
req = urllib.request.Request(
|
|
||||||
url,
|
|
||||||
data=json.dumps(data).encode("utf-8") if data else None,
|
|
||||||
headers=req_headers,
|
|
||||||
method=method,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(req) as resp:
|
|
||||||
body = resp.read().decode("utf-8")
|
|
||||||
cookies = resp.headers.get_all("Set-Cookie") or []
|
|
||||||
return resp.status, dict(resp.headers), body, cookies
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
body = e.read().decode("utf-8")
|
|
||||||
cookies = e.headers.get_all("Set-Cookie") or []
|
|
||||||
return e.code, dict(e.headers), body, cookies
|
|
||||||
except Exception as e:
|
|
||||||
return None, {}, str(e), []
|
|
||||||
|
|
||||||
|
|
||||||
def extract_cookie_value(set_cookie_headers, cookie_name):
|
|
||||||
"""Extract cookie value from Set-Cookie headers."""
|
|
||||||
for header in set_cookie_headers:
|
|
||||||
if header.startswith(cookie_name + "="):
|
|
||||||
return header.split(";")[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("=" * 60)
|
|
||||||
print("BanGUI Auth Diagnostic Script")
|
|
||||||
print("Target:", BASE_URL)
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# 1. Check health endpoint (no auth needed)
|
|
||||||
print("\n[1] GET /health")
|
|
||||||
status, headers, body, _ = make_request(f"{BASE_URL}/health")
|
|
||||||
print(f" Status: {status}")
|
|
||||||
print(f" Response: {body[:200]}")
|
|
||||||
|
|
||||||
# 2. Check CORS preflight for login
|
|
||||||
print("\n[2] OPTIONS /auth/login (CORS preflight)")
|
|
||||||
status, headers, body, _ = make_request(
|
|
||||||
f"{BASE_URL}/auth/login",
|
|
||||||
method="OPTIONS",
|
|
||||||
headers={
|
|
||||||
"Origin": "http://192.168.178.43:8080",
|
|
||||||
"Access-Control-Request-Method": "POST",
|
|
||||||
"Access-Control-Request-Headers": "Content-Type",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
print(f" Status: {status}")
|
|
||||||
print(f" Access-Control-Allow-Origin: {headers.get('Access-Control-Allow-Origin', 'MISSING')}")
|
|
||||||
print(f" Access-Control-Allow-Credentials: {headers.get('Access-Control-Allow-Credentials', 'MISSING')}")
|
|
||||||
|
|
||||||
# 3. Login
|
|
||||||
print(f"\n[3] POST /auth/login (password: {PASSWORD})")
|
|
||||||
status, headers, body, cookies = make_request(
|
|
||||||
f"{BASE_URL}/auth/login",
|
|
||||||
method="POST",
|
|
||||||
data={"password": PASSWORD},
|
|
||||||
headers={"Origin": "http://192.168.178.43:8080"},
|
|
||||||
)
|
|
||||||
print(f" Status: {status}")
|
|
||||||
print(f" Response: {body}")
|
|
||||||
print(f" Set-Cookie headers: {cookies}")
|
|
||||||
|
|
||||||
session_cookie = extract_cookie_value(cookies, "bangui_session")
|
|
||||||
if session_cookie:
|
|
||||||
print(f" Extracted session cookie: {session_cookie[:50]}...")
|
|
||||||
else:
|
|
||||||
print(" WARNING: No bangui_session cookie received!")
|
|
||||||
|
|
||||||
# 4. Validate session with cookie
|
|
||||||
print("\n[4] GET /auth/session (with cookie)")
|
|
||||||
if session_cookie:
|
|
||||||
status, headers, body, _ = make_request(
|
|
||||||
f"{BASE_URL}/auth/session",
|
|
||||||
cookie=session_cookie,
|
|
||||||
headers={"Origin": "http://192.168.178.43:8080"},
|
|
||||||
)
|
|
||||||
print(f" Status: {status}")
|
|
||||||
print(f" Response: {body}")
|
|
||||||
else:
|
|
||||||
print(" SKIPPED (no cookie from login)")
|
|
||||||
|
|
||||||
# 5. Validate session WITHOUT cookie (should be 401)
|
|
||||||
print("\n[5] GET /auth/session (without cookie)")
|
|
||||||
status, headers, body, _ = make_request(f"{BASE_URL}/auth/session")
|
|
||||||
print(f" Status: {status}")
|
|
||||||
print(f" Response: {body}")
|
|
||||||
|
|
||||||
# 6. Check backend settings (if available via /setup or other endpoint)
|
|
||||||
print("\n[6] GET /setup (check if setup is complete)")
|
|
||||||
status, headers, body, _ = make_request(f"{BASE_URL}/setup")
|
|
||||||
print(f" Status: {status}")
|
|
||||||
print(f" Response: {body[:200]}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("DIAGNOSIS SUMMARY")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
if session_cookie and "Secure" in str(cookies):
|
|
||||||
print("\n PROBLEM FOUND: Session cookie has 'Secure' flag set,")
|
|
||||||
print(" but you are accessing over HTTP (not HTTPS).")
|
|
||||||
print(" Browsers will NOT send Secure cookies over HTTP!")
|
|
||||||
print("\n FIX: Set SESSION_COOKIE_SECURE=false in your backend .env")
|
|
||||||
print(" and restart the backend.")
|
|
||||||
|
|
||||||
if not session_cookie and status == 401:
|
|
||||||
print("\n PROBLEM FOUND: Login succeeded but no session cookie was set.")
|
|
||||||
print(" This usually means the cookie is being rejected by the browser")
|
|
||||||
print(" due to Secure flag on HTTP, or SameSite restrictions.")
|
|
||||||
|
|
||||||
print("\n If CORS Access-Control-Allow-Origin is missing or wrong,")
|
|
||||||
print(" add your frontend origin to CORS_ALLOWED_ORIGINS in .env")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
10343
frontend/openapi.json
10343
frontend/openapi.json
File diff suppressed because it is too large
Load Diff
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "bangui-frontend",
|
"name": "bangui-frontend",
|
||||||
"version": "0.9.19-rc.4",
|
"version": "0.9.19",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bangui-frontend",
|
"name": "bangui-frontend",
|
||||||
"version": "0.9.19-rc.4",
|
"version": "0.9.19",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react-components": "^9.55.0",
|
"@fluentui/react-components": "^9.55.0",
|
||||||
"@fluentui/react-icons": "^2.0.257",
|
"@fluentui/react-icons": "^2.0.257",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "bangui-frontend",
|
"name": "bangui-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.9.19-rc.5",
|
"version": "0.9.19",
|
||||||
"description": "BanGUI frontend — fail2ban web management interface",
|
"description": "BanGUI frontend — fail2ban web management interface",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"generate:types": "openapi-typescript ./openapi.json -o src/types/generated.ts",
|
"generate:types": "openapi-typescript http://localhost:8000/api/openapi.json -o src/types/generated.ts",
|
||||||
"validate:types": "bash scripts/validate-types.sh",
|
"validate:types": "bash scripts/validate-types.sh",
|
||||||
"build": "npm run generate:types && tsc --noEmit && vite build",
|
"build": "npm run generate:types && tsc --noEmit && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
|||||||
@@ -17,23 +17,17 @@ GENERATED_FILE="${TYPES_DIR}/generated.ts"
|
|||||||
TEMP_FILE=$(mktemp)
|
TEMP_FILE=$(mktemp)
|
||||||
trap "rm -f $TEMP_FILE" EXIT
|
trap "rm -f $TEMP_FILE" EXIT
|
||||||
|
|
||||||
# Determine OpenAPI source: local file or backend URL
|
# Check if backend is accessible
|
||||||
BACKEND_URL="${BANGUI_BACKEND_URL:-http://localhost:8000}"
|
BACKEND_URL="${BANGUI_BACKEND_URL:-http://localhost:8000}"
|
||||||
OPENAPI_SOURCE=""
|
if ! curl -sf "${BACKEND_URL}/api/openapi.json" > /dev/null 2>&1; then
|
||||||
|
echo "❌ Backend not accessible at ${BACKEND_URL}/api/openapi.json" >&2
|
||||||
if [[ -f "${FRONTEND_DIR}/openapi.json" ]]; then
|
|
||||||
OPENAPI_SOURCE="${FRONTEND_DIR}/openapi.json"
|
|
||||||
echo "📋 Validating OpenAPI schema types (local openapi.json)..."
|
|
||||||
elif curl -sf "${BACKEND_URL}/api/openapi.json" > /dev/null 2>&1; then
|
|
||||||
OPENAPI_SOURCE="${BACKEND_URL}/api/openapi.json"
|
|
||||||
echo "📋 Validating OpenAPI schema types (backend ${BACKEND_URL})..."
|
|
||||||
else
|
|
||||||
echo "❌ Backend not accessible at ${BACKEND_URL}/api/openapi.json and no local openapi.json found" >&2
|
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "📋 Validating OpenAPI schema types..."
|
||||||
|
|
||||||
# Generate types to a temporary file
|
# Generate types to a temporary file
|
||||||
if ! npx openapi-typescript "${OPENAPI_SOURCE}" -o "$TEMP_FILE" 2>&1; then
|
if ! npx openapi-typescript "${BACKEND_URL}/api/openapi.json" -o "$TEMP_FILE" 2>&1; then
|
||||||
echo "❌ Failed to generate types from OpenAPI schema" >&2
|
echo "❌ Failed to generate types from OpenAPI schema" >&2
|
||||||
exit 3
|
exit 3
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { ErrorBoundary } from "../ErrorBoundary";
|
import { ErrorBoundary } from "../ErrorBoundary";
|
||||||
|
import * as telemetry from "../../utils/telemetry";
|
||||||
|
|
||||||
// Mock telemetry to verify it's called
|
// Mock telemetry to verify it's called
|
||||||
vi.mock("../../utils/telemetry");
|
vi.mock("../../utils/telemetry");
|
||||||
|
|||||||
@@ -468,10 +468,13 @@ describe("useFetchData", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("last subscriber abort cancels underlying request", async () => {
|
it("last subscriber abort cancels underlying request", async () => {
|
||||||
|
let resolveFirst: ((value: { value: string }) => void) | null = null;
|
||||||
const abortSignals: AbortSignal[] = [];
|
const abortSignals: AbortSignal[] = [];
|
||||||
const fetcher = vi.fn().mockImplementation((signal: AbortSignal) => {
|
const fetcher = vi.fn().mockImplementation((signal: AbortSignal) => {
|
||||||
abortSignals.push(signal);
|
abortSignals.push(signal);
|
||||||
return new Promise(() => {});
|
return new Promise((resolve) => {
|
||||||
|
resolveFirst = resolve;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const selector = vi.fn((response: { value: string }) => response.value);
|
const selector = vi.fn((response: { value: string }) => response.value);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ describe("useJailBannedIps", () => {
|
|||||||
const fetchMock = vi.mocked(api.fetchJailBannedIps);
|
const fetchMock = vi.mocked(api.fetchJailBannedIps);
|
||||||
const unbanMock = vi.mocked(api.unbanIp);
|
const unbanMock = vi.mocked(api.unbanIp);
|
||||||
|
|
||||||
fetchMock.mockResolvedValue({ items: [{ ip: "1.2.3.4", jail: "sshd", banned_at: "2025-01-01T10:00:00+00:00", expires_at: "2025-01-01T10:10:00+00:00", ban_count: 1, country: "US" }], total: 1, page: 1, page_size: 25, total_pages: 1, pagination_mode: "offset" });
|
fetchMock.mockResolvedValue({ items: [{ ip: "1.2.3.4", jail: "sshd", banned_at: "2025-01-01T10:00:00+00:00", expires_at: "2025-01-01T10:10:00+00:00", ban_count: 1, country: "US" }], total: 1, page: 1, page_size: 25 });
|
||||||
unbanMock.mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
unbanMock.mockResolvedValue({ message: "ok", jail: "sshd", success: true });
|
||||||
|
|
||||||
const { result } = renderHook(() => useJailBannedIps("sshd"));
|
const { result } = renderHook(() => useJailBannedIps("sshd"));
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ describe("usePolledData", () => {
|
|||||||
vi.runAllTimersAsync();
|
vi.runAllTimersAsync();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const callCountAfterInitial = fetcher.mock.calls.length;
|
||||||
|
|
||||||
// Reset timer and advance to ensure no more polls
|
// Reset timer and advance to ensure no more polls
|
||||||
vi.clearAllTimers();
|
vi.clearAllTimers();
|
||||||
fetcher.mockClear();
|
fetcher.mockClear();
|
||||||
@@ -64,6 +66,8 @@ describe("usePolledData", () => {
|
|||||||
vi.advanceTimersByTime(100);
|
vi.advanceTimersByTime(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const initialCalls = fetcher.mock.calls.length;
|
||||||
|
|
||||||
// Clear for clean test
|
// Clear for clean test
|
||||||
fetcher.mockClear();
|
fetcher.mockClear();
|
||||||
|
|
||||||
@@ -131,6 +135,7 @@ describe("usePolledData", () => {
|
|||||||
vi.advanceTimersByTime(100);
|
vi.advanceTimersByTime(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const initialCalls = fetcher.mock.calls.length;
|
||||||
fetcher.mockClear();
|
fetcher.mockClear();
|
||||||
|
|
||||||
// Call refresh
|
// Call refresh
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import * as authApi from "../api/auth";
|
import * as authApi from "../api/auth";
|
||||||
import { ApiError, setUnauthorizedHandler, resetLogoutState, clearSessionCorrelationId } from "../api/client";
|
import { setUnauthorizedHandler, resetLogoutState, clearSessionCorrelationId } from "../api/client";
|
||||||
import { setAuthErrorHandler, resetLogoutState as resetFetchErrorLogoutState } from "../utils/fetchError";
|
import { setAuthErrorHandler, resetLogoutState as resetFetchErrorLogoutState } from "../utils/fetchError";
|
||||||
import { STORAGE_KEY_AUTHENTICATED } from "../utils/constants";
|
import { STORAGE_KEY_AUTHENTICATED } from "../utils/constants";
|
||||||
import { SessionValidationLoading } from "../components/SessionValidationLoading";
|
import { SessionValidationLoading } from "../components/SessionValidationLoading";
|
||||||
@@ -133,11 +133,6 @@ export function AuthProvider({
|
|||||||
|
|
||||||
const handleValidationError = useCallback(
|
const handleValidationError = useCallback(
|
||||||
(error: Error): void => {
|
(error: Error): void => {
|
||||||
// Suppress noisy warning for 5xx gateway errors (e.g. 502 Bad Gateway)
|
|
||||||
// during startup — these are server-side issues, not network issues.
|
|
||||||
if (error instanceof ApiError && error.status >= 500) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Network error — log but don't logout.
|
// Network error — log but don't logout.
|
||||||
console.warn("Session validation network error:", error);
|
console.warn("Session validation network error:", error);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -177,6 +177,11 @@ export interface paths {
|
|||||||
* On success the token is also set as an ``HttpOnly`` ``SameSite=Lax``
|
* On success the token is also set as an ``HttpOnly`` ``SameSite=Lax``
|
||||||
* cookie so the browser SPA benefits from automatic credential handling.
|
* cookie so the browser SPA benefits from automatic credential handling.
|
||||||
*
|
*
|
||||||
|
* Rate limiting: Exponential backoff on failed attempts. Each wrong password
|
||||||
|
* incurs an increasing delay (0.5s, 1s, 2s, 4s, 5s max per IP address).
|
||||||
|
* Requests during the penalty period return ``429 Too Many Requests`` with
|
||||||
|
* a ``Retry-After`` header.
|
||||||
|
*
|
||||||
* Cache invalidation: On successful login, any existing cached sessions for
|
* Cache invalidation: On successful login, any existing cached sessions for
|
||||||
* the same user are invalidated so that stale tokens (e.g., from a stolen
|
* the same user are invalidated so that stale tokens (e.g., from a stolen
|
||||||
* device) cannot be reused beyond the cache TTL window.
|
* device) cannot be reused beyond the cache TTL window.
|
||||||
@@ -187,6 +192,7 @@ export interface paths {
|
|||||||
* request: The incoming HTTP request (used to extract client IP).
|
* request: The incoming HTTP request (used to extract client IP).
|
||||||
* session_ctx: Session service context containing db and repository.
|
* session_ctx: Session service context containing db and repository.
|
||||||
* settings: Application settings (used for session duration and trusted proxies).
|
* settings: Application settings (used for session duration and trusted proxies).
|
||||||
|
* rate_limiter: The login rate limiter (per IP).
|
||||||
* session_cache: Session cache for invalidating old sessions on login.
|
* session_cache: Session cache for invalidating old sessions on login.
|
||||||
*
|
*
|
||||||
* Returns:
|
* Returns:
|
||||||
@@ -194,6 +200,7 @@ export interface paths {
|
|||||||
*
|
*
|
||||||
* Raises:
|
* Raises:
|
||||||
* AuthenticationError: if the password is incorrect.
|
* AuthenticationError: if the password is incorrect.
|
||||||
|
* RateLimitError: if the rate limit is exceeded.
|
||||||
*/
|
*/
|
||||||
post: operations["login_api_v1_auth_login_post"];
|
post: operations["login_api_v1_auth_login_post"];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -6267,6 +6274,13 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
content?: never;
|
content?: never;
|
||||||
};
|
};
|
||||||
|
/** @description Too many login attempts, retry after delay */
|
||||||
|
429: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
/** @description Setup not complete */
|
/** @description Setup not complete */
|
||||||
503: {
|
503: {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
Reference in New Issue
Block a user