8 Commits

Author SHA1 Message Date
5a12d1c22f chore: release v0.9.19-rc.5 2026-05-23 21:32:21 +02:00
aebe0d0236 chore(release): bump version to 0.9.19-rc.4
- Add production Docker Compose configuration

- Add check_auth.py diagnostic script for session 401 debugging
2026-05-23 21:27:52 +02:00
99e1b74405 chore: release v0.9.19-rc.4 2026-05-22 21:49:01 +02:00
9fe52755a5 fix(db): fix migration failures when upgrading from 0.8.0 schema
Migration 1: remove idx_sessions_token_hash from _SCHEMA_STATEMENTS.
The legacy schema has sessions.token (not token_hash). The IF NOT EXISTS
guard only prevents duplicate index names — it still requires the column
to exist. Migration 2 drops and rebuilds sessions with token_hash anyway,
so creating the index in migration 1 was redundant.

Migration 3: replace ALTER TABLE ADD COLUMN with a table rebuild.
SQLite rejects ALTER TABLE ADD COLUMN NOT NULL DEFAULT <expression> when
the table already contains rows. The old DB has ~181k geo_cache rows, so
the ALTER always failed. Rebuild copies existing rows with last_seen set
to cached_at as a reasonable approximation of last-seen time.
2026-05-22 21:47:32 +02:00
9d2d6fadf3 chore: release v0.9.19-rc.3 2026-05-22 20:49:12 +02:00
2e5ac092bf fix(auth): suppress misleading 502 warning during session validation
A 502 Bad Gateway is a server/gateway error, not a network error.
Logging it as a 'Session validation network error' is noisy and
misleading during startup when nginx is temporarily unreachable.

Silently skip the console.warn for 5xx errors in handleValidationError
while keeping the warning for actual network errors.
2026-05-22 20:47:57 +02:00
dcee222a41 chore: release v0.9.19-rc.2 2026-05-22 20:38:33 +02:00
12fe70d768 chore: bump to v0.9.19-rc.1 and add local OpenAPI build support
- Add release candidate (rc) support to release.sh with latestRC tagging
- Bump VERSION, backend pyproject.toml, and frontend package.json to 0.9.19-rc.1
- Add local frontend/openapi.json so build no longer needs running backend
- Update generate:types and validate-types.sh to use local openapi.json
- Fix frontend tests: remove unused imports/variables and update mock data
2026-05-22 20:36:14 +02:00
17 changed files with 10697 additions and 51 deletions

View File

@@ -18,7 +18,7 @@ WORKDIR /build
COPY frontend/package.json frontend/package-lock.json* /build/
RUN npm ci --ignore-scripts
# Copy source and build
# Copy source + local OpenAPI spec (avoids needing a running backend during build)
COPY frontend/ /build/
RUN npm run build

View File

@@ -1 +1 @@
v0.9.19
v0.9.19-rc.5

105
Docker/compose.prod.yml Normal file
View File

@@ -0,0 +1,105 @@
# ──────────────────────────────────────────────────────────────
# 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

View File

@@ -6,7 +6,7 @@
# ./release.sh
#
# The current version is stored in VERSION (next to this script).
# You will be asked whether to bump major, minor, or patch.
# You will be asked whether to bump major, minor, patch, or release candidate (rc).
set -euo pipefail
@@ -24,24 +24,60 @@ CURRENT="$(cat "${VERSION_FILE}")"
# Strip leading 'v' for arithmetic
VERSION="${CURRENT#v}"
IFS='.' read -r MAJOR MINOR PATCH <<< "${VERSION}"
# Parse version: X.Y.Z or X.Y.Z-rc.N
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 " BanGUI — Release"
echo " Current version: v${MAJOR}.${MINOR}.${PATCH}"
if [[ -n "${RC_SUFFIX}" ]]; then
echo " Current version: v${MAJOR}.${MINOR}.${PATCH}-rc.${RC_NUM}"
else
echo " Current version: v${MAJOR}.${MINOR}.${PATCH}"
fi
echo "============================================"
echo ""
echo "How would you like to bump the version?"
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)"
if [[ -n "${RC_SUFFIX}" ]]; then
echo " 1) patch (v${MAJOR}.${MINOR}.${PATCH}-rc.${RC_NUM} → v${MAJOR}.${MINOR}.${PATCH})"
echo " 2) minor (v${MAJOR}.${MINOR}.${PATCH}-rc.${RC_NUM} → v${MAJOR}.$((MINOR + 1)).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 ""
read -rp "Enter choice [1/2/3]: " CHOICE
read -rp "Enter choice [1/2/3/4]: " CHOICE
case "${CHOICE}" in
1) NEW_TAG="v${MAJOR}.${MINOR}.$((PATCH + 1))" ;;
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" ;;
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
exit 1
@@ -81,7 +117,13 @@ fi
# Push containers
# ---------------------------------------------------------------------------
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
# ---------------------------------------------------------------------------

View File

@@ -102,10 +102,15 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
"""
# 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] = [
_CREATE_SETTINGS,
_CREATE_SESSIONS,
_CREATE_SESSIONS_TOKEN_INDEX,
_CREATE_BLOCKLIST_SOURCES,
_CREATE_IMPORT_LOG,
_CREATE_GEO_CACHE,
@@ -133,8 +138,24 @@ CREATE UNIQUE INDEX idx_sessions_token_hash ON sessions (token_hash);
3: """
-- 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.
-- Default to current timestamp for existing rows.
ALTER TABLE geo_cache ADD COLUMN last_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
-- SQLite rejects ALTER TABLE ADD COLUMN with a non-constant NOT NULL default
-- when the table already contains rows, so we rebuild the table instead.
-- 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: """
-- Migration 4: Add scheduler_lock table for multi-worker safety.

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bangui-backend"
version = "0.9.19"
version = "0.9.19-rc.4"
description = "BanGUI backend — fail2ban web management interface"
requires-python = ">=3.12"
dependencies = [

147
check_auth.py Normal file
View File

@@ -0,0 +1,147 @@
#!/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 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
{
"name": "bangui-frontend",
"version": "0.9.19",
"version": "0.9.19-rc.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bangui-frontend",
"version": "0.9.19",
"version": "0.9.19-rc.4",
"dependencies": {
"@fluentui/react-components": "^9.55.0",
"@fluentui/react-icons": "^2.0.257",

View File

@@ -1,12 +1,12 @@
{
"name": "bangui-frontend",
"private": true,
"version": "0.9.19",
"version": "0.9.19-rc.5",
"description": "BanGUI frontend — fail2ban web management interface",
"type": "module",
"scripts": {
"dev": "vite",
"generate:types": "openapi-typescript http://localhost:8000/api/openapi.json -o src/types/generated.ts",
"generate:types": "openapi-typescript ./openapi.json -o src/types/generated.ts",
"validate:types": "bash scripts/validate-types.sh",
"build": "npm run generate:types && tsc --noEmit && vite build",
"preview": "vite preview",

View File

@@ -17,17 +17,23 @@ GENERATED_FILE="${TYPES_DIR}/generated.ts"
TEMP_FILE=$(mktemp)
trap "rm -f $TEMP_FILE" EXIT
# Check if backend is accessible
# Determine OpenAPI source: local file or backend URL
BACKEND_URL="${BANGUI_BACKEND_URL:-http://localhost:8000}"
if ! curl -sf "${BACKEND_URL}/api/openapi.json" > /dev/null 2>&1; then
echo "❌ Backend not accessible at ${BACKEND_URL}/api/openapi.json" >&2
OPENAPI_SOURCE=""
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
fi
echo "📋 Validating OpenAPI schema types..."
# Generate types to a temporary file
if ! npx openapi-typescript "${BACKEND_URL}/api/openapi.json" -o "$TEMP_FILE" 2>&1; then
if ! npx openapi-typescript "${OPENAPI_SOURCE}" -o "$TEMP_FILE" 2>&1; then
echo "❌ Failed to generate types from OpenAPI schema" >&2
exit 3
fi

View File

@@ -1,7 +1,6 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { ErrorBoundary } from "../ErrorBoundary";
import * as telemetry from "../../utils/telemetry";
// Mock telemetry to verify it's called
vi.mock("../../utils/telemetry");

View File

@@ -468,13 +468,10 @@ describe("useFetchData", () => {
});
it("last subscriber abort cancels underlying request", async () => {
let resolveFirst: ((value: { value: string }) => void) | null = null;
const abortSignals: AbortSignal[] = [];
const fetcher = vi.fn().mockImplementation((signal: AbortSignal) => {
abortSignals.push(signal);
return new Promise((resolve) => {
resolveFirst = resolve;
});
return new Promise(() => {});
});
const selector = vi.fn((response: { value: string }) => response.value);

View File

@@ -10,7 +10,7 @@ describe("useJailBannedIps", () => {
const fetchMock = vi.mocked(api.fetchJailBannedIps);
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 });
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" });
unbanMock.mockResolvedValue({ message: "ok", jail: "sshd", success: true });
const { result } = renderHook(() => useJailBannedIps("sshd"));

View File

@@ -34,8 +34,6 @@ describe("usePolledData", () => {
vi.runAllTimersAsync();
});
const callCountAfterInitial = fetcher.mock.calls.length;
// Reset timer and advance to ensure no more polls
vi.clearAllTimers();
fetcher.mockClear();
@@ -66,8 +64,6 @@ describe("usePolledData", () => {
vi.advanceTimersByTime(100);
});
const initialCalls = fetcher.mock.calls.length;
// Clear for clean test
fetcher.mockClear();
@@ -135,7 +131,6 @@ describe("usePolledData", () => {
vi.advanceTimersByTime(100);
});
const initialCalls = fetcher.mock.calls.length;
fetcher.mockClear();
// Call refresh

View File

@@ -56,7 +56,7 @@ import React, {
} from "react";
import { useNavigate } from "react-router-dom";
import * as authApi from "../api/auth";
import { setUnauthorizedHandler, resetLogoutState, clearSessionCorrelationId } from "../api/client";
import { ApiError, setUnauthorizedHandler, resetLogoutState, clearSessionCorrelationId } from "../api/client";
import { setAuthErrorHandler, resetLogoutState as resetFetchErrorLogoutState } from "../utils/fetchError";
import { STORAGE_KEY_AUTHENTICATED } from "../utils/constants";
import { SessionValidationLoading } from "../components/SessionValidationLoading";
@@ -133,6 +133,11 @@ export function AuthProvider({
const handleValidationError = useCallback(
(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.
console.warn("Session validation network error:", error);
},

View File

@@ -177,11 +177,6 @@ export interface paths {
* On success the token is also set as an ``HttpOnly`` ``SameSite=Lax``
* 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
* the same user are invalidated so that stale tokens (e.g., from a stolen
* device) cannot be reused beyond the cache TTL window.
@@ -192,7 +187,6 @@ export interface paths {
* request: The incoming HTTP request (used to extract client IP).
* session_ctx: Session service context containing db and repository.
* 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.
*
* Returns:
@@ -200,7 +194,6 @@ export interface paths {
*
* Raises:
* AuthenticationError: if the password is incorrect.
* RateLimitError: if the rate limit is exceeded.
*/
post: operations["login_api_v1_auth_login_post"];
delete?: never;
@@ -6274,13 +6267,6 @@ export interface operations {
};
content?: never;
};
/** @description Too many login attempts, retry after delay */
429: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Setup not complete */
503: {
headers: {