diff --git a/app/views/status.py b/app/views/status.py
index 585a9df4..11fa3b0b 100644
--- a/app/views/status.py
+++ b/app/views/status.py
@@ -6,7 +6,8 @@ import logging
import os
from datetime import datetime
-from fastapi import Request
+from fastapi import Request, status
+from fastapi.responses import RedirectResponse
from app.utils.config_validator import get_provider_status
from app.views.base import APIRouter, require_login, settings, templates
@@ -15,12 +16,25 @@ logger = logging.getLogger(__name__)
router = APIRouter()
+def _require_admin(request: Request):
+ """Return the session user if they are an admin, else return None."""
+ user = request.session.get("user")
+ if not user or not user.get("is_admin"):
+ user_email = user.get("email", "anonymous") if user else "anonymous"
+ logger.warning(f"Non-admin user {user_email} attempted to access /status")
+ return None
+ return user
+
+
@router.get("/status")
@require_login
async def status_dashboard(request: Request):
"""
- Status dashboard showing all configured integration targets
+ Status dashboard showing all configured integration targets (admin only).
"""
+ user = _require_admin(request)
+ if user is None:
+ return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
# Get provider status
providers = get_provider_status()
diff --git a/frontend/templates/base.html b/frontend/templates/base.html
index 37a7414b..7b0d9d14 100644
--- a/frontend/templates/base.html
+++ b/frontend/templates/base.html
@@ -168,18 +168,14 @@
Backup & Restore
+
+ Status
+
-
-
- Status
-
-
{% endif %}{# end multi_user_enabled / is_logged_in check #}
@@ -314,16 +310,13 @@
Backup & Restore
+
+ Status
+
-
-
- Status
-
-
{% endif %}{# end multi_user_enabled / is_logged_in check #}
diff --git a/tests/test_views_status.py b/tests/test_views_status.py
index be90a83b..353af12b 100644
--- a/tests/test_views_status.py
+++ b/tests/test_views_status.py
@@ -1,16 +1,74 @@
"""Tests for app/views/status.py module."""
-from unittest.mock import Mock, mock_open, patch
+from unittest.mock import MagicMock, Mock, mock_open, patch
import pytest
+@pytest.mark.unit
+class TestRequireAdmin:
+ """Unit tests for the _require_admin helper in views/status.py."""
+
+ def test_returns_none_when_no_user_in_session(self):
+ """_require_admin returns None when session has no user."""
+ from app.views.status import _require_admin
+
+ mock_request = MagicMock()
+ mock_request.session = {}
+
+ result = _require_admin(mock_request)
+ assert result is None
+
+ def test_returns_none_for_non_admin_user(self):
+ """_require_admin returns None when user is not an admin."""
+ from app.views.status import _require_admin
+
+ mock_request = MagicMock()
+ mock_request.session = {"user": {"email": "user@example.com", "is_admin": False}}
+
+ result = _require_admin(mock_request)
+ assert result is None
+
+ def test_logs_warning_for_non_admin(self):
+ """_require_admin logs a warning when a non-admin attempts access."""
+ from app.views.status import _require_admin
+
+ mock_request = MagicMock()
+ mock_request.session = {"user": {"email": "user@example.com", "is_admin": False}}
+
+ with patch("app.views.status.logger") as mock_logger:
+ _require_admin(mock_request)
+ mock_logger.warning.assert_called_once()
+
+ def test_logs_warning_when_no_user(self):
+ """_require_admin logs a warning when there is no user in the session."""
+ from app.views.status import _require_admin
+
+ mock_request = MagicMock()
+ mock_request.session = {}
+
+ with patch("app.views.status.logger") as mock_logger:
+ _require_admin(mock_request)
+ mock_logger.warning.assert_called_once()
+
+ def test_returns_user_for_admin(self):
+ """_require_admin returns the user dict when the user is an admin."""
+ from app.views.status import _require_admin
+
+ admin_user = {"email": "admin@example.com", "is_admin": True}
+ mock_request = MagicMock()
+ mock_request.session = {"user": admin_user}
+
+ result = _require_admin(mock_request)
+ assert result == admin_user
+
+
@pytest.mark.integration
class TestStatusViews:
"""Tests for status view routes."""
def test_status_dashboard(self, client):
- """Test status dashboard page."""
+ """Test status dashboard page returns 200 (auth disabled in tests)."""
response = client.get("/status")
assert response.status_code == 200
@@ -19,6 +77,63 @@ class TestStatusViews:
class TestStatusDashboard:
"""Tests for status_dashboard function."""
+ @pytest.mark.asyncio
+ async def test_redirects_non_admin_to_home(self):
+ """status_dashboard redirects to '/' when user is not an admin."""
+ from fastapi.responses import RedirectResponse
+
+ from app.views.status import status_dashboard
+
+ mock_request = MagicMock()
+ mock_request.session = {"user": {"email": "user@example.com", "is_admin": False}}
+
+ result = await status_dashboard(mock_request)
+
+ assert isinstance(result, RedirectResponse)
+ assert result.status_code == 302
+ assert result.headers["location"] == "/"
+
+ @pytest.mark.asyncio
+ async def test_redirects_when_no_user_in_session(self):
+ """status_dashboard redirects to '/' when no user is in the session."""
+ from fastapi.responses import RedirectResponse
+
+ from app.views.status import status_dashboard
+
+ mock_request = MagicMock()
+ mock_request.session = {}
+
+ result = await status_dashboard(mock_request)
+
+ assert isinstance(result, RedirectResponse)
+ assert result.status_code == 302
+
+ @patch("app.views.status.get_provider_status")
+ @patch("app.views.status.templates")
+ @patch("app.views.status.settings")
+ @patch("app.views.status.os.path.exists")
+ @pytest.mark.asyncio
+ async def test_returns_template_for_admin_user(self, mock_exists, mock_settings, mock_templates, mock_providers):
+ """status_dashboard renders the template for an authenticated admin user."""
+ from app.views.status import status_dashboard
+
+ mock_exists.return_value = False
+ mock_providers.return_value = {}
+ mock_settings.version = "1.0.0"
+ mock_settings.build_date = "2024-01-01"
+ mock_settings.debug = False
+ mock_settings.git_sha = "abc123"
+ mock_settings.notification_urls = []
+
+ mock_request = MagicMock()
+ mock_request.session = {"user": {"email": "admin@example.com", "is_admin": True}}
+
+ await status_dashboard(mock_request)
+
+ mock_templates.TemplateResponse.assert_called_once()
+ call_args = mock_templates.TemplateResponse.call_args
+ assert call_args[0][0] == "status_dashboard.html"
+
@patch("app.views.status.get_provider_status")
@patch("app.views.status.templates")
@patch("app.views.status.settings")