RouteForge v0.6.0-beta
Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR.
diff --git a/README.md b/README.md
index a01cd71..3fffc63 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
- Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR. {systemStatusError} Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR. {systemStatus.status} RouteForge liefert nachvollziehbare Routing-Preflightchecks für technische Operator-Workflows. Version: v0.6.0-beta Version: v0.6.1-beta Create the first administrator account.
+
@@ -43,7 +43,7 @@ Routing changes often require fast but traceable checks across multiple external
## Current Alpha Status
-RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.6.0-beta**.
+RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.6.1-beta**.
## Quickstart with Docker Compose
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 02832f7..4ff8a6d 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,15 +1,16 @@
# Release Notes
-## v0.6.0-beta
+## v0.6.1-beta
-**SQLite Volume Permission Hotfix**
+**Auth Bootstrap Fix**
### Highlights
-- Fixes SQLite readonly database errors after non-root container hardening.
-- Backend entrypoint now prepares `/app/data` permissions for the `routeforge` runtime user.
-- Runtime remains non-root.
-- Troubleshooting documentation added.
+- Fixed missing Initial Admin Setup screen.
+- App now blocks dashboard/check views until setup/login state is resolved.
+- Fixed NoneType crash when running checks without authenticated user.
+- Check endpoints now return 401/403 instead of HTTP 500.
+- API requests include session cookies consistently.
---
diff --git a/backend/app/api/routes_health.py b/backend/app/api/routes_health.py
index 5d9281b..1bf30bf 100644
--- a/backend/app/api/routes_health.py
+++ b/backend/app/api/routes_health.py
@@ -8,4 +8,4 @@
@router.get('/health')
def health() -> dict:
- return {"status": "ok", "version": "v0.5.5-beta", "database": get_database_status(engine).get("status", "unknown")}
+ return {"status": "ok", "version": "v0.6.1-beta", "database": get_database_status(engine).get("status", "unknown")}
diff --git a/backend/app/api/routes_system.py b/backend/app/api/routes_system.py
index 7dc234b..e6f24db 100644
--- a/backend/app/api/routes_system.py
+++ b/backend/app/api/routes_system.py
@@ -12,7 +12,7 @@
def system_info():
return {
'name': 'RouteForge',
- 'version': 'v0.6.0-beta',
+ 'version': 'v0.6.1-beta',
'demo_mode': settings.demo_mode,
'read_only': True,
'data_sources': ['RIPEstat', 'RIPEstat Whois/Registry'],
diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py
index 15435ac..1eb480d 100644
--- a/backend/app/core/auth.py
+++ b/backend/app/core/auth.py
@@ -60,8 +60,6 @@ def require_authenticated_user(user: User = Depends(get_current_user)) -> User:
def require_role(*roles: str):
def checker(request: Request, db: Session = Depends(get_db)):
- if db.query(User).count() == 0:
- return None
current = get_current_user(request, db)
if current.role not in roles:
raise HTTPException(status_code=403, detail="Insufficient role")
diff --git a/backend/app/core/system_status.py b/backend/app/core/system_status.py
index 3dba033..8a3cf23 100644
--- a/backend/app/core/system_status.py
+++ b/backend/app/core/system_status.py
@@ -117,7 +117,7 @@ def build_system_status(engine: Engine | None) -> dict:
return {
"status": "ok",
"name": settings.app_name,
- "version": "v0.6.0-beta",
+ "version": "v0.6.1-beta",
"read_only": True,
"mode": "demo" if settings.demo_mode else "live",
"demo_mode": settings.demo_mode,
diff --git a/backend/app/main.py b/backend/app/main.py
index f5c6566..6244fe0 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -17,7 +17,7 @@
logging.basicConfig(level=getattr(logging, settings.log_level.upper(), logging.INFO))
logger = logging.getLogger("routeforge")
-app = FastAPI(title="RouteForge", version="0.6.0")
+app = FastAPI(title="RouteForge", version="0.6.1")
app.add_middleware(
CORSMiddleware,
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 28e7291..64c0b37 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "routeforge-backend"
-version = "0.6.0"
+version = "0.6.1"
description = "RouteForge backend"
license = "AGPL-3.0-or-later"
requires-python = ">=3.12"
diff --git a/backend/tests/test_api_smoke.py b/backend/tests/test_api_smoke.py
index eeb28ad..62a08e3 100644
--- a/backend/tests/test_api_smoke.py
+++ b/backend/tests/test_api_smoke.py
@@ -14,10 +14,20 @@ def _client() -> TestClient:
import app.database as database
importlib.reload(main_module)
+ database.Base.metadata.drop_all(bind=database.engine)
database.Base.metadata.create_all(bind=database.engine)
return TestClient(main_module.app)
+def _setup_and_login(client: TestClient, username: str = "admin", password: str = "AdminPass123!") -> None:
+ setup = client.post('/api/auth/setup', json={'username': username, 'email': 'admin@example.org', 'password': password, 'password_confirm': password})
+ if setup.status_code == 403:
+ login = client.post('/api/auth/login', json={'username': username, 'password': password})
+ assert login.status_code == 200
+ return
+ assert setup.status_code == 200
+
+
def test_health() -> None:
client = _client()
response = client.get('/health')
@@ -27,6 +37,7 @@ def test_health() -> None:
def test_prefix_check_without_origin_as() -> None:
client = _client()
+ _setup_and_login(client)
response = client.post('/api/check/prefix', json={'prefix': '193.0.6.0/24'})
assert response.status_code == 200
payload = response.json()
@@ -42,6 +53,7 @@ def test_prefix_check_without_origin_as() -> None:
def test_asn_check() -> None:
client = _client()
+ _setup_and_login(client)
response = client.post('/api/check/asn', json={'asn': 'AS3320'})
assert response.status_code == 200
payload = response.json()
@@ -56,6 +68,7 @@ def test_asn_check() -> None:
def test_asn_check_without_prefixes_has_batch_reason() -> None:
client = _client()
+ _setup_and_login(client)
response = client.post('/api/check/asn', json={'asn': 'AS4491'})
assert response.status_code == 200
details = response.json().get('details', {})
@@ -67,6 +80,7 @@ def test_asn_check_without_prefixes_has_batch_reason() -> None:
def test_asn_rpki_batch() -> None:
client = _client()
+ _setup_and_login(client)
response = client.post('/api/check/asn-rpki', json={'asn': 'AS3320', 'limit': 3})
assert response.status_code == 200
payload = response.json()
@@ -79,6 +93,7 @@ def test_asn_rpki_batch() -> None:
def test_asn_rpki_batch_without_prefixes() -> None:
client = _client()
+ _setup_and_login(client)
response = client.post('/api/check/asn-rpki', json={'asn': 'AS4491', 'limit': 25})
assert response.status_code == 200
payload = response.json()
@@ -99,6 +114,7 @@ def test_system_info() -> None:
def test_reports_list_empty_or_present() -> None:
client = _client()
+ _setup_and_login(client)
response = client.get('/api/reports')
assert response.status_code == 200
payload = response.json()
@@ -107,6 +123,7 @@ def test_reports_list_empty_or_present() -> None:
def test_preflight_check() -> None:
client = _client()
+ _setup_and_login(client)
response = client.post('/api/check/preflight', json={'prefix': '192.0.2.0/24', 'planned_origin_as': 'AS3320'})
assert response.status_code == 200
payload = response.json()
@@ -124,6 +141,7 @@ def test_preflight_check() -> None:
def test_report_export_endpoints() -> None:
client = _client()
+ _setup_and_login(client)
check_response = client.post('/api/check/prefix', json={'prefix': '193.0.6.0/24'})
assert check_response.status_code == 200
report_id = check_response.json().get('report_id')
@@ -146,6 +164,7 @@ def test_report_export_endpoints() -> None:
def test_report_export_not_found() -> None:
client = _client()
+ _setup_and_login(client)
for endpoint in ('summary', 'markdown', 'html'):
response = client.get(f'/api/reports/999999/{endpoint}')
assert response.status_code == 404
@@ -154,10 +173,11 @@ def test_report_export_not_found() -> None:
def test_system_status_endpoint() -> None:
client = _client()
+ _setup_and_login(client)
response = client.get('/api/system/status')
assert response.status_code == 200
payload = response.json()
- assert payload.get('version') == 'v0.5.5-beta'
+ assert payload.get('version') == 'v0.6.1-beta'
assert payload.get('read_only') is True
assert payload.get('database', {}).get('status')
assert payload.get('ripestat', {}).get('cache_ttl_seconds') is not None
@@ -172,6 +192,7 @@ def test_safe_database_url() -> None:
def test_system_status_includes_migration_fields() -> None:
client = _client()
+ _setup_and_login(client)
response = client.get('/api/system/status')
assert response.status_code == 200
database = response.json().get('database', {})
@@ -190,3 +211,39 @@ def connect(self):
payload = ss.get_database_status(FakeBrokenEngine())
assert payload.get('status') == 'error'
assert payload.get('migration_status') == 'error'
+
+
+def test_setup_required_without_users() -> None:
+ client = _client()
+ response = client.get('/api/auth/setup-required')
+ assert response.status_code == 200
+ assert response.json().get('setup_required') is True
+
+
+def test_asn_check_requires_authentication() -> None:
+ client = _client()
+ response = client.post('/api/check/asn', json={'asn': 'AS3320'})
+ assert response.status_code == 401
+
+
+def test_asn_check_forbidden_for_viewer() -> None:
+ client = _client()
+ _setup_and_login(client)
+ create = client.post('/api/users', json={'username': 'viewer', 'email': 'viewer@example.org', 'password': 'ViewerPass123!', 'role': 'viewer'})
+ assert create.status_code == 200
+ client.post('/api/auth/logout')
+ login = client.post('/api/auth/login', json={'username': 'viewer', 'password': 'ViewerPass123!'})
+ assert login.status_code == 200
+ response = client.post('/api/check/asn', json={'asn': 'AS3320'})
+ assert response.status_code == 403
+
+
+def test_asn_check_allowed_for_operator() -> None:
+ client = _client()
+ _setup_and_login(client)
+ create = client.post('/api/users', json={'username': 'operator1', 'email': 'op@example.org', 'password': 'OperatorPass123!', 'role': 'operator'})
+ assert create.status_code == 200
+ client.post('/api/auth/logout')
+ assert client.post('/api/auth/login', json={'username': 'operator1', 'password': 'OperatorPass123!'}).status_code == 200
+ response = client.post('/api/check/asn', json={'asn': 'AS3320'})
+ assert response.status_code == 200
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e1ef36a..4e79bba 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "routeforge-frontend",
- "version": "0.6.0",
+ "version": "0.6.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "routeforge-frontend",
- "version": "0.6.0",
+ "version": "0.6.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"react": "^18.3.1",
diff --git a/frontend/package.json b/frontend/package.json
index 151d409..c3b3636 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "routeforge-frontend",
- "version": "0.6.0",
+ "version": "0.6.1",
"private": true,
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 18594f2..012210e 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,54 +1,90 @@
import { useEffect, useMemo, useState } from 'react'
-import { getReportHtml, getReportMarkdown, getReportSummary, getReports, getSystemInfo, getSystemStatus } from './api'
+import { ApiError, getMe, getReportHtml, getReportMarkdown, getReportSummary, getReports, getSetupRequired, getSystemInfo, getSystemStatus, login, setupAdmin } from './api'
import { AsnCheckForm } from './components/AsnCheckForm'
import { Layout } from './components/Layout'
+import { LoginView } from './components/LoginView'
import { PrefixCheckForm } from './components/PrefixCheckForm'
import { PreflightCheckForm } from './components/PreflightCheckForm'
+import { SetupView } from './components/SetupView'
import { StatusBadge } from './components/StatusBadge'
import type { ReportListItem, SystemInfo, SystemStatus } from './types'
type NavKey = 'dashboard' | 'asn' | 'prefix' | 'preflight' | 'reports' | 'system' | 'about'
+type AuthMode = 'loading' | 'setup' | 'login' | 'app' | 'error'
export default function App() {
+ const [authMode, setAuthMode] = useState
RouteForge v0.6.0-beta
System Health
{systemStatusError ? RouteForge v0.6.1-beta
Reports
{reports.length===0 ? {reports.map(r=>Zeitpunkt Typ Resource Origin-AS Holder Status Kurzfassung Actions )}{new Date(r.created_at).toLocaleString()} {r.check_type === 'preflight' ? 'Preflight' : r.check_type} {r.input_resource} {r.origin_as || '-'} {r.holder || 'Unknown'} {r.summary} Overall Status
Security Warnings
{systemStatus.security_warnings.map((warning) =>
RIPEstat Settings
{JSON.stringify(systemStatus.ripestat, null, 2)}Features
{JSON.stringify(systemStatus.features, null, 2)}Reports
{reports.length===0 ? {reports.map(r=>
)}{r.summary} Login
+
+ Initial Admin Setup
+