Normalize remaining dashboard proxy URLs

parent 2313402a
...@@ -222,6 +222,7 @@ const analyticsData = { ...@@ -222,6 +222,7 @@ const analyticsData = {
</script> </script>
<script> <script>
const BASE_PATH = {{ request.scope.get('root_path', '') | tojson }};
document.getElementById('timeRangeSelect').addEventListener('change', function() { document.getElementById('timeRangeSelect').addEventListener('change', function() {
var customRange = document.getElementById('customDateRange'); var customRange = document.getElementById('customDateRange');
if (this.value === 'custom') { if (this.value === 'custom') {
...@@ -287,7 +288,7 @@ function escHtml(s) { ...@@ -287,7 +288,7 @@ function escHtml(s) {
// Search users via API // Search users via API
async function searchUsers(query) { async function searchUsers(query) {
try { try {
const response = await fetch(`/api/users/search?q=${encodeURIComponent(query)}`); const response = await fetch(`${BASE_PATH}/api/users/search?q=${encodeURIComponent(query)}`);
const data = await response.json(); const data = await response.json();
return data.users || []; return data.users || [];
} catch (error) { } catch (error) {
...@@ -1033,7 +1034,7 @@ function confirmDeleteAnalytics(scope) { ...@@ -1033,7 +1034,7 @@ function confirmDeleteAnalytics(scope) {
: 'Delete ALL analytics including all user data? This cannot be undone.'; : 'Delete ALL analytics including all user data? This cannot be undone.';
if (!confirm(msg)) return; if (!confirm(msg)) return;
if (!confirm('Are you sure? This is irreversible.')) return; if (!confirm('Are you sure? This is irreversible.')) return;
fetch('/api/admin/analytics/delete-' + scope, {method: 'POST'}) fetch(`${BASE_PATH}/api/admin/analytics/delete-${scope}`, {method: 'POST'})
.then(r => r.json()) .then(r => r.json())
.then(d => { .then(d => {
alert('Deleted ' + d.deleted + ' records.'); alert('Deleted ' + d.deleted + ' records.');
...@@ -1045,7 +1046,7 @@ function confirmDeleteAnalytics(scope) { ...@@ -1045,7 +1046,7 @@ function confirmDeleteAnalytics(scope) {
{% endif %} {% endif %}
<div style="margin-top: 30px; display: flex; gap: 10px; flex-wrap: wrap;"> <div style="margin-top: 30px; display: flex; gap: 10px; flex-wrap: wrap;">
<a href="/dashboard" class="btn btn-secondary" data-i18n="analytics_page.back">Back to Dashboard</a> <a href="{{ url_for(request, '/dashboard') }}" class="btn btn-secondary" data-i18n="analytics_page.back">Back to Dashboard</a>
<a href="{{ url_for(request, '/dashboard/analytics/provider-quotas') }}" class="btn btn-secondary">Provider Quota Debug</a> <a href="{{ url_for(request, '/dashboard/analytics/provider-quotas') }}" class="btn btn-secondary">Provider Quota Debug</a>
{% if is_admin %} {% if is_admin %}
<a href="{{ url_for(request, '/dashboard/rate-limits') }}" class="btn">Rate Limits</a> <a href="{{ url_for(request, '/dashboard/rate-limits') }}" class="btn">Rate Limits</a>
......
...@@ -301,6 +301,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -301,6 +301,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<script> <script>
// Utility functions // Utility functions
const BASE_PATH = {{ request.scope.get('root_path', '') | tojson }};
const baseUrl = document.getElementById('url-data').dataset.baseUrl; const baseUrl = document.getElementById('url-data').dataset.baseUrl;
function getUrlParams() { function getUrlParams() {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
...@@ -773,7 +774,7 @@ async function performBulkAction(action, description, destructive = false, extra ...@@ -773,7 +774,7 @@ async function performBulkAction(action, description, destructive = false, extra
requestData.extra_data = extraData; requestData.extra_data = extraData;
} }
fetch('/dashboard/users/bulk', { fetch(`${BASE_PATH}/dashboard/users/bulk`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...@@ -1092,7 +1093,7 @@ async function submitNotification() { ...@@ -1092,7 +1093,7 @@ async function submitNotification() {
return; return;
} }
try { try {
const res = await fetch('/dashboard/api/admin/notifications/send', { const res = await fetch(`${BASE_PATH}/dashboard/api/admin/notifications/send`, {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({user_ids: _notifyUserIds, title, message}) body: JSON.stringify({user_ids: _notifyUserIds, title, message})
......
...@@ -10,6 +10,7 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape ...@@ -10,6 +10,7 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
from aisbf.routes.dashboard import market as dashboard_market from aisbf.routes.dashboard import market as dashboard_market
from aisbf.routes.dashboard import providers as dashboard_providers from aisbf.routes.dashboard import providers as dashboard_providers
from aisbf.routes.dashboard import settings as dashboard_settings
sys.path.insert(0, str(Path(__file__).resolve().parents[2])) sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from main import app from main import app
...@@ -151,6 +152,25 @@ class RegistryStub: ...@@ -151,6 +152,25 @@ class RegistryStub:
return self._db return self._db
class DashboardUsersDbStub:
def __init__(self):
self.paginated_calls = []
def get_users_paginated(self, **kwargs):
self.paginated_calls.append(kwargs)
return {
"users": [{"id": 1, "username": "alice", "display_name": "alice", "email": "alice@example.test", "role": "user", "tier_id": 10, "created_by": "admin", "created_at": "2026-01-01", "last_login": None, "is_active": True}],
"total": 1,
}
def get_all_tiers(self):
return [{"id": 10, "name": "Pro", "is_visible": True}]
class DashboardSettingsRegistryStub(RegistryStub):
pass
def _find_session_secret() -> str: def _find_session_secret() -> str:
for middleware in app.user_middleware: for middleware in app.user_middleware:
kwargs = getattr(middleware, "kwargs", {}) kwargs = getattr(middleware, "kwargs", {})
...@@ -303,3 +323,73 @@ def test_market_page_uses_proxy_aware_market_api_urls(monkeypatch): ...@@ -303,3 +323,73 @@ def test_market_page_uses_proxy_aware_market_api_urls(monkeypatch):
assert 'const BASE_PATH = "/proxy/app"' in response.text assert 'const BASE_PATH = "/proxy/app"' in response.text
assert "fetch(`${BASE_PATH}/api/market/listings`" in response.text assert "fetch(`${BASE_PATH}/api/market/listings`" in response.text
assert "fetch('/api/market/listings'" not in response.text assert "fetch('/api/market/listings'" not in response.text
def test_dashboard_users_page_uses_proxy_aware_bulk_and_notification_urls(monkeypatch):
db = DashboardUsersDbStub()
capture = TemplateCapture()
client = TestClient(app)
_login_as_admin(client)
monkeypatch.setattr(dashboard_settings, "DatabaseRegistry", DashboardSettingsRegistryStub(db))
monkeypatch.setattr(dashboard_settings, "_templates", capture)
response = client.get("/dashboard/users", headers=_forwarded_prefix_headers())
assert response.status_code == 200
assert capture.calls[-1]["name"] == "dashboard/users.html"
assert 'const BASE_PATH = "/proxy/app"' in response.text
assert "fetch(`${BASE_PATH}/dashboard/users/bulk`" in response.text
assert "fetch(`${BASE_PATH}/dashboard/api/admin/notifications/send`" in response.text
assert "fetch('/dashboard/users/bulk'" not in response.text
assert "fetch('/dashboard/api/admin/notifications/send'" not in response.text
def test_analytics_page_uses_proxy_aware_search_delete_and_back_urls(monkeypatch):
capture = TemplateCapture()
request = type("Req", (), {"scope": {"root_path": "/proxy/app"}, "session": {}})()
response = capture.TemplateResponse(
request=request,
name="dashboard/analytics.html",
context={
"request": request,
"selected_time_range": "24h",
"from_date": None,
"to_date": None,
"date_range_usage": None,
"currency_symbol": "$",
"available_providers": [],
"available_models": [],
"available_rotations": [],
"available_autoselects": [],
"available_users": [
{"id": idx, "username": f"user{idx}", "role": "user"}
for idx in range(1, 26)
],
"selected_provider": None,
"selected_model": None,
"selected_rotation": None,
"selected_autoselect": None,
"selected_user": None,
"global_only": "",
"is_admin": True,
"is_config_admin": True,
"provider_stats": [],
"cost_overview": {"total_estimated_cost_today": 0, "date_range": None, "providers": []},
"optimization_savings": None,
"model_performance": [],
"token_over_time": "[]",
"rotation_breakdown": [],
"autoselect_breakdown": [],
},
)
response_text = response.body.decode()
assert 'const BASE_PATH = "/proxy/app"' in response_text
assert "fetch(`${BASE_PATH}/api/users/search?q=${encodeURIComponent(query)}`)" in response_text
assert "fetch(`${BASE_PATH}/api/admin/analytics/delete-${scope}`, {method: 'POST'})" in response_text
assert '<a href="/proxy/app/dashboard" class="btn btn-secondary" data-i18n="analytics_page.back">Back to Dashboard</a>' in response_text
assert "fetch(`/api/users/search?q=${encodeURIComponent(query)}`)" not in response_text
assert "fetch('/api/admin/analytics/delete-' + scope" not in response_text
assert '<a href="/dashboard" class="btn btn-secondary" data-i18n="analytics_page.back">Back to Dashboard</a>' not in response_text
...@@ -101,6 +101,10 @@ class RegistryStub: ...@@ -101,6 +101,10 @@ class RegistryStub:
return self._db return self._db
class DashboardSettingsRegistryStub(RegistryStub):
pass
class AsyncJsonRequest: class AsyncJsonRequest:
def __init__(self, session, body): def __init__(self, session, body):
self.session = session self.session = session
...@@ -349,3 +353,20 @@ def test_dashboard_context_middleware_hides_market_link_when_market_disabled(mon ...@@ -349,3 +353,20 @@ def test_dashboard_context_middleware_hides_market_link_when_market_disabled(mon
assert response.status_code == 200 assert response.status_code == 200
assert templates.calls[-1]["request"].state.market_enabled is False assert templates.calls[-1]["request"].state.market_enabled is False
assert ">Market</a>" not in response.text assert ">Market</a>" not in response.text
def test_admin_payment_settings_uses_proxy_aware_market_admin_link(monkeypatch):
db = MarketSettingsDbStub()
templates = TemplateCapture()
templates._env.globals["url_for"] = lambda request, path, **kwargs: f"{request.scope.get('root_path', '')}{path}"
client = TestClient(app)
_login_as_admin(client)
monkeypatch.setattr(dashboard_admin, "DatabaseRegistry", RegistryStub(db))
monkeypatch.setattr(dashboard_admin, "_templates", templates)
response = client.get("/dashboard/admin/payment-settings", headers={"X-Forwarded-Prefix": "/proxy/app"})
assert response.status_code == 200
assert 'href="/proxy/app/dashboard/admin/market"' in response.text
assert 'href="/dashboard/admin/market"' not in response.text
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment