RouteForge v0.6.5-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 e247126..5e59b13 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-
+
@@ -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.5-beta**.
+RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.6.6-beta**.
## Quickstart with Docker Compose
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 7bbc10b..bc1930f 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,26 @@
# Release Notes
+## v0.6.6-beta
+
+**Audit Log UI & Session Hardening**
+
+### Highlights
+
+- Added centralized backend audit logging service and audit events for setup/auth/user/check/report actions.
+- Added admin-only `GET /api/audit-log` API with basic filters and newest-first sorting.
+- Added admin-only Audit Log UI with table, loading/error/empty states and filters.
+- Session cookie handling now consistently uses `SESSION_COOKIE_NAME` and `COOKIE_SECURE` settings for set/delete.
+- Added backend tests for audit permissions/events and session cookie hardening behavior.
+
+### Security notes
+
+- Audit events exclude passwords, password hashes, session tokens and secrets.
+- `login_failed` entries store only username/reason and request metadata.
+- RouteForge remains read-only against RIPE DB, RPKI and routers; no write paths were introduced.
+- Audit logging is local to RouteForge storage only.
+
+---
+
## v0.6.5-beta
**Auth UX & Admin Console Polish**
diff --git a/ROADMAP.md b/ROADMAP.md
index 8b2528f..3072aee 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,35 +1,18 @@
# RouteForge Roadmap
## Current Status
-v0.5.0-beta, beta foundation for production selfhosting, read-only
-
-## v0.3.x
-- demo polish
-- export polish
-- UI refinements
-
-## v0.4.0-alpha
-- data source hardening
-- RIPEstat error classification
-- cache visibility
-- retry handling
-
-## v0.5.0-beta
-- production Docker Compose
-- PostgreSQL hardening
-- backup/restore docs
-- reverse proxy examples
-- healthchecks
-
-## v0.6.0-beta
-- user management
-- roles
-- audit log
+v0.6.6-beta, audit log UI/API and session hardening completed, read-only
## v0.7.0-beta
- projects/change cases
- grouped preflight reports
+## v0.7.1-beta
+- bgp visibility details
+
+## v0.7.2-beta
+- roa planner / roa preflight
+
## v0.8.0-beta
- watch mode / scheduled rechecks
diff --git a/backend/app/api/routes_audit.py b/backend/app/api/routes_audit.py
new file mode 100644
index 0000000..97e3994
--- /dev/null
+++ b/backend/app/api/routes_audit.py
@@ -0,0 +1,47 @@
+from fastapi import APIRouter, Depends, Query
+from sqlalchemy.orm import Session
+
+from app.core.auth import require_admin
+from app.database import get_db
+from app.models import AuditLog, User
+
+router = APIRouter(prefix='/api/audit-log', tags=['audit'])
+
+@router.get('')
+def list_audit_log(
+ action: str | None = Query(default=None),
+ user_id: int | None = Query(default=None),
+ target_type: str | None = Query(default=None),
+ limit: int = Query(default=100, ge=1, le=500),
+ offset: int = Query(default=0, ge=0),
+ _: User = Depends(require_admin),
+ db: Session = Depends(get_db),
+):
+ query = db.query(AuditLog, User.username).outerjoin(User, AuditLog.user_id == User.id)
+ if action:
+ query = query.filter(AuditLog.action == action)
+ if user_id is not None:
+ query = query.filter(AuditLog.user_id == user_id)
+ if target_type:
+ query = query.filter(AuditLog.target_type == target_type)
+
+ rows = query.order_by(AuditLog.created_at.desc(), AuditLog.id.desc()).offset(offset).limit(limit).all()
+ return {
+ 'items': [
+ {
+ 'id': audit.id,
+ 'created_at': audit.created_at.isoformat(),
+ 'user_id': audit.user_id,
+ 'username': username,
+ 'action': audit.action,
+ 'target_type': audit.target_type,
+ 'target_id': audit.target_id,
+ 'ip_address': audit.ip_address,
+ 'user_agent': audit.user_agent,
+ 'details_json': audit.details_json,
+ }
+ for audit, username in rows
+ ],
+ 'limit': limit,
+ 'offset': offset,
+ }
diff --git a/backend/app/api/routes_auth.py b/backend/app/api/routes_auth.py
index 517aedd..1e4876f 100644
--- a/backend/app/api/routes_auth.py
+++ b/backend/app/api/routes_auth.py
@@ -1,8 +1,10 @@
from datetime import datetime
-from fastapi import APIRouter, Depends, HTTPException, Response
+from fastapi import APIRouter, Depends, HTTPException, Request, Response
from pydantic import BaseModel
from sqlalchemy.orm import Session
+import app.config as config
+from app.core.audit import write_audit_log_for_request
from app.core.auth import create_session_token, require_authenticated_user
from app.core.security import hash_password, validate_password_strength, verify_password
from app.database import get_db
@@ -25,7 +27,7 @@ def setup_required(db: Session = Depends(get_db)):
return {"setup_required": db.query(User).count() == 0}
@router.post('/setup')
-def setup(payload: SetupRequest, response: Response, db: Session = Depends(get_db)):
+def setup(payload: SetupRequest, request: Request, response: Response, db: Session = Depends(get_db)):
if db.query(User).count() > 0:
raise HTTPException(status_code=403, detail='Setup already completed')
if payload.password != payload.password_confirm:
@@ -36,22 +38,26 @@ def setup(payload: SetupRequest, response: Response, db: Session = Depends(get_d
user = User(username=payload.username.strip(), email=payload.email, password_hash=hash_password(payload.password), role='admin', is_active=True)
db.add(user); db.commit(); db.refresh(user)
token = create_session_token(user)
- response.set_cookie('routeforge_session', token, httponly=True, samesite='lax')
+ response.set_cookie(config.settings.session_cookie_name, token, httponly=True, samesite='lax', secure=config.settings.cookie_secure)
+ write_audit_log_for_request(db, request, action='initial_admin_setup', actor=user, target_type='user', target_id=str(user.id), details_json={'username': user.username, 'role': user.role})
return {"user": {"id": user.id, "username": user.username, "email": user.email, "role": user.role}}
@router.post('/login')
-def login(payload: LoginRequest, response: Response, db: Session = Depends(get_db)):
+def login(payload: LoginRequest, request: Request, response: Response, db: Session = Depends(get_db)):
user = db.query(User).filter(User.username == payload.username).first()
if not user or not verify_password(payload.password, user.password_hash) or not user.is_active:
+ write_audit_log_for_request(db, request, action='login_failed', target_type='auth', details_json={'username': payload.username, 'reason': 'invalid_credentials'})
raise HTTPException(status_code=401, detail='Invalid credentials')
user.last_login_at = datetime.utcnow(); db.commit()
token = create_session_token(user)
- response.set_cookie('routeforge_session', token, httponly=True, samesite='lax')
+ response.set_cookie(config.settings.session_cookie_name, token, httponly=True, samesite='lax', secure=config.settings.cookie_secure)
+ write_audit_log_for_request(db, request, action='login_success', actor=user, target_type='user', target_id=str(user.id), details_json={'username': user.username})
return {"user": {"id": user.id, "username": user.username, "email": user.email, "role": user.role}}
@router.post('/logout')
-def logout(response: Response):
- response.delete_cookie('routeforge_session')
+def logout(request: Request, response: Response, user: User = Depends(require_authenticated_user), db: Session = Depends(get_db)):
+ write_audit_log_for_request(db, request, action='logout', actor=user, target_type='user', target_id=str(user.id), details_json={'username': user.username})
+ response.delete_cookie(config.settings.session_cookie_name, httponly=True, samesite='lax', secure=config.settings.cookie_secure)
return {"ok": True}
@router.get('/me')
diff --git a/backend/app/api/routes_checks.py b/backend/app/api/routes_checks.py
index b45faae..424abba 100644
--- a/backend/app/api/routes_checks.py
+++ b/backend/app/api/routes_checks.py
@@ -3,6 +3,7 @@
from sqlalchemy.orm import Session
from app.core.auth import require_operator_or_admin
+from app.core.audit import write_audit_log
from app.database import get_db
from app.models import Check, Report
from app.schemas import AsnCheckRequest, AsnRpkiBatchRequest, CheckResponse, PrefixCheckRequest, PreflightCheckRequest
@@ -71,6 +72,8 @@ def _store_and_respond(db: Session, ctype: str, resource: str, origin_as: str |
db.add(report)
db.commit()
db.refresh(report)
+ write_audit_log(db, user_id=user_id, action='check_executed', target_type='check', target_id=str(check.id), details_json={'check_type': ctype, 'input_resource': resource, 'status': result.get('status')})
+ write_audit_log(db, user_id=user_id, action='report_generated', target_type='report', target_id=str(report.id), details_json={'check_id': check.id, 'check_type': ctype})
return CheckResponse(report_id=report.id, status=result["status"], summary=result["summary"], explanation=result.get("explanation"), risk=result.get("risk"), recommendations=result["recommendations"], input=result.get("input"), checks=result.get("checks"), details=result["details"], markdown=md, html=html)
except OperationalError as exc:
db.rollback()
diff --git a/backend/app/api/routes_system.py b/backend/app/api/routes_system.py
index e397853..b83e513 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.5-beta',
+ 'version': 'v0.6.6-beta',
'demo_mode': settings.demo_mode,
'read_only': True,
'data_sources': ['RIPEstat', 'RIPEstat Whois/Registry'],
diff --git a/backend/app/api/routes_users.py b/backend/app/api/routes_users.py
index 3ef1927..90b5480 100644
--- a/backend/app/api/routes_users.py
+++ b/backend/app/api/routes_users.py
@@ -1,7 +1,8 @@
-from fastapi import APIRouter, Depends, HTTPException
+from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from sqlalchemy.orm import Session
+from app.core.audit import write_audit_log_for_request
from app.core.auth import require_admin
from app.core.security import hash_password, validate_password_strength
from app.database import get_db
@@ -27,17 +28,18 @@ def list_users(_: User = Depends(require_admin), db: Session = Depends(get_db)):
return [{"id":u.id,"username":u.username,"email":u.email,"role":u.role,"is_active":u.is_active,"created_at":u.created_at.isoformat(),"updated_at":u.updated_at.isoformat(),"last_login_at":u.last_login_at.isoformat() if u.last_login_at else None} for u in users]
@router.post('')
-def create_user(payload: UserCreate, _: User = Depends(require_admin), db: Session = Depends(get_db)):
+def create_user(payload: UserCreate, request: Request, admin: User = Depends(require_admin), db: Session = Depends(get_db)):
if payload.role not in {'admin','operator','viewer'}:
raise HTTPException(status_code=400, detail='Invalid role')
errs=validate_password_strength(payload.password)
if errs: raise HTTPException(status_code=400, detail='; '.join(errs))
user=User(username=payload.username.strip(), email=payload.email, password_hash=hash_password(payload.password), role=payload.role, is_active=True)
db.add(user); db.commit(); db.refresh(user)
+ write_audit_log_for_request(db, request, action='user_created', actor=admin, target_type='user', target_id=str(user.id), details_json={'username': user.username, 'role': user.role})
return {"id":user.id,"username":user.username,"email":user.email,"role":user.role,"is_active":user.is_active,"created_at":user.created_at.isoformat(),"updated_at":user.updated_at.isoformat(),"last_login_at":user.last_login_at.isoformat() if user.last_login_at else None}
@router.patch('/{user_id}')
-def patch_user(user_id:int,payload:UserPatch,_:User=Depends(require_admin),db:Session=Depends(get_db)):
+def patch_user(user_id:int,payload:UserPatch,request: Request,admin:User=Depends(require_admin),db:Session=Depends(get_db)):
user=db.query(User).filter(User.id==user_id).first()
if not user: raise HTTPException(status_code=404, detail='User not found')
if payload.email is not None: user.email=payload.email
@@ -51,4 +53,5 @@ def patch_user(user_id:int,payload:UserPatch,_:User=Depends(require_admin),db:Se
if errs: raise HTTPException(status_code=400, detail='; '.join(errs))
user.password_hash=hash_password(payload.password)
db.commit(); db.refresh(user)
+ write_audit_log_for_request(db, request, action='user_updated', actor=admin, target_type='user', target_id=str(user.id), details_json={'username': user.username, 'role': user.role, 'is_active': user.is_active})
return {"id":user.id,"username":user.username,"email":user.email,"role":user.role,"is_active":user.is_active,"created_at":user.created_at.isoformat(),"updated_at":user.updated_at.isoformat(),"last_login_at":user.last_login_at.isoformat() if user.last_login_at else None}
diff --git a/backend/app/core/audit.py b/backend/app/core/audit.py
new file mode 100644
index 0000000..d16ee2c
--- /dev/null
+++ b/backend/app/core/audit.py
@@ -0,0 +1,66 @@
+import logging
+from typing import Any
+
+from fastapi import Request
+from sqlalchemy.orm import Session
+
+from app.models import AuditLog, User
+
+logger = logging.getLogger("routeforge.audit")
+
+
+def extract_request_meta(request: Request) -> tuple[str | None, str | None]:
+ ip_address = request.client.host if request.client else None
+ user_agent = request.headers.get("user-agent")
+ return ip_address, user_agent
+
+
+def write_audit_log(
+ db: Session,
+ *,
+ user_id: int | None,
+ action: str,
+ target_type: str | None = None,
+ target_id: str | None = None,
+ ip_address: str | None = None,
+ user_agent: str | None = None,
+ details_json: dict[str, Any] | None = None,
+) -> None:
+ try:
+ entry = AuditLog(
+ user_id=user_id,
+ action=action,
+ target_type=target_type,
+ target_id=target_id,
+ ip_address=ip_address,
+ user_agent=user_agent,
+ details_json=details_json,
+ )
+ db.add(entry)
+ db.commit()
+ except Exception:
+ db.rollback()
+ logger.exception("audit log write failed", extra={"action": action, "target_type": target_type, "target_id": target_id})
+
+
+def write_audit_log_for_request(
+ db: Session,
+ request: Request,
+ *,
+ action: str,
+ actor: User | None = None,
+ target_type: str | None = None,
+ target_id: str | None = None,
+ details_json: dict[str, Any] | None = None,
+) -> None:
+ ip_address, user_agent = extract_request_meta(request)
+ write_audit_log(
+ db,
+ user_id=actor.id if actor else None,
+ action=action,
+ target_type=target_type,
+ target_id=target_id,
+ ip_address=ip_address,
+ user_agent=user_agent,
+ details_json=details_json,
+ )
diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py
index 1eb480d..ce42513 100644
--- a/backend/app/core/auth.py
+++ b/backend/app/core/auth.py
@@ -9,13 +9,13 @@
from fastapi import Depends, HTTPException, Request
from sqlalchemy.orm import Session
-from app.config import settings
+import app.config as config
from app.database import get_db
from app.models import User
def _sign(data: str) -> str:
- return hmac.new(settings.secret_key.encode(), data.encode(), hashlib.sha256).hexdigest()
+ return hmac.new(config.settings.secret_key.encode(), data.encode(), hashlib.sha256).hexdigest()
def create_session_token(user: User) -> str:
@@ -23,7 +23,7 @@ def create_session_token(user: User) -> str:
"user_id": user.id,
"username": user.username,
"role": user.role,
- "exp": int((datetime.now(timezone.utc) + timedelta(hours=settings.session_expire_hours)).timestamp()),
+ "exp": int((datetime.now(timezone.utc) + timedelta(hours=config.settings.session_expire_hours)).timestamp()),
}
raw = json.dumps(payload, separators=(",", ":"), sort_keys=True)
b64 = base64.urlsafe_b64encode(raw.encode()).decode()
@@ -44,7 +44,7 @@ def _decode_token(token: str) -> dict:
def get_current_user(request: Request, db: Session = Depends(get_db)) -> User:
- token = request.cookies.get(settings.session_cookie_name)
+ token = request.cookies.get(config.settings.session_cookie_name)
if not token:
raise HTTPException(status_code=401, detail="Not authenticated")
payload = _decode_token(token)
diff --git a/backend/app/core/system_status.py b/backend/app/core/system_status.py
index 3ab8f7c..2b23c69 100644
--- a/backend/app/core/system_status.py
+++ b/backend/app/core/system_status.py
@@ -139,7 +139,7 @@ def build_system_status(engine: Engine | None) -> dict:
return {
"status": "ok",
"name": settings.app_name,
- "version": "v0.6.5-beta",
+ "version": "v0.6.6-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 39e7a72..fdfd85d 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -4,6 +4,7 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
+from app.api.routes_audit import router as audit_router
from app.api.routes_auth import router as auth_router
from app.api.routes_checks import router as checks_router
from app.api.routes_health import router as health_router
@@ -17,7 +18,7 @@
logging.basicConfig(level=getattr(logging, settings.log_level.upper(), logging.INFO))
logger = logging.getLogger("routeforge")
-app = FastAPI(title="RouteForge", version="0.6.5")
+app = FastAPI(title="RouteForge", version="0.6.6")
app.add_middleware(
CORSMiddleware,
@@ -52,3 +53,4 @@ def startup() -> None:
app.include_router(system_router)
app.include_router(auth_router)
app.include_router(users_router)
+app.include_router(audit_router)
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index f9a8e1f..13fb7c6 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "routeforge-backend"
-version = "0.6.5"
+version = "0.6.6"
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 b3f13be..ad9d957 100644
--- a/backend/tests/test_api_smoke.py
+++ b/backend/tests/test_api_smoke.py
@@ -6,10 +6,10 @@
from fastapi.testclient import TestClient
-def _client() -> TestClient:
+def _client(**overrides) -> TestClient:
import app.config as config
- config.settings = config.Settings(_env_file=None, ROUTEFORGE_DEMO_MODE=True)
+ config.settings = config.Settings(_env_file=None, ROUTEFORGE_DEMO_MODE=True, **overrides)
import app.main as main_module
import app.database as database
@@ -177,7 +177,7 @@ def test_system_status_endpoint() -> None:
response = client.get('/api/system/status')
assert response.status_code == 200
payload = response.json()
- assert payload.get('version') == 'v0.6.5-beta'
+ assert payload.get('version') == 'v0.6.6-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
@@ -320,3 +320,52 @@ def test_inactive_user_cannot_login() -> None:
client.post('/api/auth/logout')
login = client.post('/api/auth/login', json={'username': 'inactive1', 'password': 'InactivePass123!'})
assert login.status_code == 401
+
+def test_audit_log_admin_only_and_contains_login_success() -> None:
+ client = _client()
+ _setup_and_login(client)
+ resp = client.get('/api/audit-log')
+ assert resp.status_code == 200
+ actions = [i.get('action') for i in resp.json().get('items', [])]
+ assert 'login_success' in actions or 'initial_admin_setup' in actions
+
+
+def test_audit_log_forbidden_for_operator_and_viewer() -> None:
+ client = _client()
+ _setup_and_login(client)
+ assert client.post('/api/users', json={'username': 'op3', 'email': 'op3@example.org', 'password': 'OperatorPass123!', 'role': 'operator'}).status_code == 200
+ assert client.post('/api/users', json={'username': 'vw3', 'email': 'vw3@example.org', 'password': 'ViewerPass123!', 'role': 'viewer'}).status_code == 200
+ client.post('/api/auth/logout')
+ assert client.post('/api/auth/login', json={'username': 'op3', 'password': 'OperatorPass123!'}).status_code == 200
+ assert client.get('/api/audit-log').status_code == 403
+ client.post('/api/auth/logout')
+ assert client.post('/api/auth/login', json={'username': 'vw3', 'password': 'ViewerPass123!'}).status_code == 200
+ assert client.get('/api/audit-log').status_code == 403
+
+
+def test_login_failed_audit_has_no_password() -> None:
+ client = _client()
+ _setup_and_login(client)
+ bad = client.post('/api/auth/login', json={'username': 'admin', 'password': 'wrong'})
+ assert bad.status_code == 401
+ resp = client.get('/api/audit-log')
+ events = [i for i in resp.json().get('items', []) if i.get('action') == 'login_failed']
+ assert events
+ assert 'password' not in str(events[0].get('details_json', {})).lower()
+
+
+def test_cookie_name_and_logout_cookie_settings() -> None:
+ secure_client = _client(SESSION_COOKIE_NAME='rf_cookie_test', COOKIE_SECURE=True)
+ setup = secure_client.post('/api/auth/setup', json={'username': 'admin', 'email': 'admin@example.org', 'password': 'AdminPass123!', 'password_confirm': 'AdminPass123!'})
+ assert setup.status_code == 200
+ cookie_header = setup.headers.get('set-cookie', '')
+ assert 'rf_cookie_test=' in cookie_header
+ assert 'Secure' in cookie_header
+
+ client = _client(SESSION_COOKIE_NAME='rf_cookie_test', COOKIE_SECURE=False)
+ setup2 = client.post('/api/auth/setup', json={'username': 'admin', 'email': 'admin@example.org', 'password': 'AdminPass123!', 'password_confirm': 'AdminPass123!'})
+ assert setup2.status_code == 200
+ out = client.post('/api/auth/logout')
+ assert out.status_code == 200
+ logout_cookie = out.headers.get('set-cookie', '')
+ assert 'rf_cookie_test=' in logout_cookie
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 1e71bec..d7b2272 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "routeforge-frontend",
- "version": "0.6.5",
+ "version": "0.6.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "routeforge-frontend",
- "version": "0.6.5",
+ "version": "0.6.6",
"license": "AGPL-3.0-or-later",
"dependencies": {
"react": "^18.3.1",
diff --git a/frontend/package.json b/frontend/package.json
index 324d1af..053bbbc 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "routeforge-frontend",
- "version": "0.6.5",
+ "version": "0.6.6",
"private": true,
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 34b159a..5621ba5 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
import { ApiError, getMe, getReportHtml, getReportMarkdown, getReportSummary, getReports, getSetupRequired, getSystemInfo, getSystemStatus, login, logout, setupAdmin } from './api'
+import { AuditLogView } from './components/AuditLogView'
import { AsnCheckForm } from './components/AsnCheckForm'
import { Layout } from './components/Layout'
import { LoginView } from './components/LoginView'
@@ -10,7 +11,7 @@ import { StatusBadge } from './components/StatusBadge'
import type { ReportListItem, SystemInfo, SystemStatus, User, UserRole } from './types'
import { UsersView } from './components/UsersView'
-type NavKey = 'dashboard' | 'asn' | 'prefix' | 'preflight' | 'reports' | 'system' | 'users' | 'about'
+type NavKey = 'dashboard' | 'asn' | 'prefix' | 'preflight' | 'reports' | 'system' | 'users' | 'audit' | 'about'
type AuthMode = 'loading' | 'setup' | 'login' | 'app' | 'error'
export default function App() {
@@ -69,8 +70,8 @@ export default function App() {
if (authMode === 'login') return
Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR.
Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR.
| {r.summary} |
Version: v0.6.5-beta
Version: v0.6.6-beta
| Time | User | Action | Target | IP | User Agent | Details |
|---|---|---|---|---|---|---|
| {a.created_at} | {a.username || a.user_id || '-'} | {a.action} | {a.target_type || '-'}:{a.target_id || '-'} | {a.ip_address || '-'} | {a.user_agent || '-'} | {JSON.stringify(a.details_json || {}, null, 2)} |