Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="frontend/public/routeforge.png" alt="RouteForge Logo" width="420">
</p>
<p align="center">
<img src="https://img.shields.io/badge/version-v0.5.5--beta-blue" alt="Version">
<img src="https://img.shields.io/badge/version-v0.6.1--beta-blue" alt="Version">
<img src="https://img.shields.io/badge/license-AGPL--3.0--or--later-orange" alt="License">
<img src="https://img.shields.io/badge/status-beta-yellow" alt="Status">
<img src="https://img.shields.io/badge/selfhosted-ready-success" alt="Selfhosted">
Expand Down Expand Up @@ -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

Expand Down
13 changes: 7 additions & 6 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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.

---

Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/routes_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")}
2 changes: 1 addition & 1 deletion backend/app/api/routes_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
2 changes: 0 additions & 2 deletions backend/app/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion backend/app/core/system_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
59 changes: 58 additions & 1 deletion backend/tests/test_api_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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', {})
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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')
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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', {})
Expand All @@ -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
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading
Loading