#5 Gateway Trust Model: - Token validation now uses protected endpoints, not health checks - Unknown services rejected (no fallback to unprotected endpoint) - Trust model documented in docs/trust-model.md #8 CI Enforcement: - Added .gitea/workflows/security.yml with: - Dependency audit (npm audit --audit-level=high for budget) - Secret scanning (checks for tracked .env/.db, hardcoded secrets) - Dockerfile lint (non-root USER, HEALTHCHECK presence) #9 Performance Hardening: - Budget /summary: 1-minute in-memory cache (avoids repeated account fan-out) - Gateway /api/dashboard: 30-second per-user cache (50x faster on repeat) - Inventory health endpoint added before auth middleware Closes #5, #8, #9
This commit is contained in:
@@ -89,16 +89,26 @@ def handle_set_connection(handler, user, body):
|
||||
handler._send_json({"error": f"Unknown service: {service}"}, 400)
|
||||
return
|
||||
|
||||
# Test the token
|
||||
# Validate token against a protected endpoint (not health check)
|
||||
if service == "trips":
|
||||
test_url = f"{target}/api/trips"
|
||||
headers = {"Authorization": f"Bearer {auth_token}"}
|
||||
elif service == "fitness":
|
||||
test_url = f"{target}/api/users"
|
||||
test_url = f"{target}/api/user"
|
||||
headers = {"Authorization": f"Bearer {auth_token}"}
|
||||
elif service == "inventory":
|
||||
test_url = f"{target}/summary"
|
||||
headers = {"X-API-Key": auth_token}
|
||||
elif service == "budget":
|
||||
test_url = f"{target}/summary"
|
||||
headers = {"X-API-Key": auth_token}
|
||||
elif service == "reader":
|
||||
test_url = f"{target}/v1/feeds/counters"
|
||||
headers = {"X-Auth-Token": auth_token}
|
||||
else:
|
||||
test_url = f"{target}/api/health"
|
||||
headers = {"Authorization": f"Bearer {auth_token}"}
|
||||
# Unknown service — reject, don't fall back to health check
|
||||
handler._send_json({"error": f"Cannot validate token for service: {service}"}, 400)
|
||||
return
|
||||
|
||||
status, _, _ = proxy_request(test_url, "GET", headers, timeout=10)
|
||||
if status == 401:
|
||||
@@ -158,8 +168,19 @@ def handle_get_pinned(handler, user):
|
||||
handler._send_json({"pinned": [dict(r) for r in rows]})
|
||||
|
||||
|
||||
import time as _time
|
||||
_dashboard_cache = {"data": None, "expires": 0, "user_id": None}
|
||||
_DASHBOARD_TTL = 30 # seconds
|
||||
|
||||
def handle_dashboard(handler, user):
|
||||
"""Aggregate dashboard data from connected services -- all fetches in parallel."""
|
||||
"""Aggregate dashboard data from connected services -- all fetches in parallel.
|
||||
Results cached for 30 seconds per user to avoid repeated slow aggregation."""
|
||||
# Return cached if fresh and same user
|
||||
if (_dashboard_cache["data"] and _dashboard_cache["user_id"] == user["id"]
|
||||
and _time.time() < _dashboard_cache["expires"]):
|
||||
handler._send_json(_dashboard_cache["data"])
|
||||
return
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
conn = get_db()
|
||||
@@ -311,4 +332,8 @@ def handle_dashboard(handler, user):
|
||||
conn2.close()
|
||||
pinned = [dict(r) for r in pinned_rows]
|
||||
|
||||
handler._send_json({"widgets": widgets, "pinned": pinned})
|
||||
result = {"widgets": widgets, "pinned": pinned}
|
||||
_dashboard_cache["data"] = result
|
||||
_dashboard_cache["expires"] = _time.time() + _DASHBOARD_TTL
|
||||
_dashboard_cache["user_id"] = user["id"]
|
||||
handler._send_json(result)
|
||||
|
||||
Reference in New Issue
Block a user