diff --git a/routes/settings.py b/routes/settings.py index 7f03da8..071b644 100644 --- a/routes/settings.py +++ b/routes/settings.py @@ -1,7 +1,7 @@ """Settings, TTS routes, routing log, server logs, and voice design presets endpoints.""" from __future__ import annotations -from fastapi import APIRouter, HTTPException, Request +from fastapi import APIRouter, HTTPException, Request, Response from core.config import _load_settings, _save_settings, _normalize_settings, _SETTINGS_KEYS, _ensure_external_api_key from core.routing import _load_tts_routes, _save_tts_routes @@ -19,7 +19,7 @@ _SECRET_MASK = "__configured__" @router.get("/api/settings") -async def get_settings(request: Request): +async def get_settings(request: Request, response: Response): s = _load_settings() # Lazily generated on first read, not at server startup — a fresh # install shows a real usable key in Settings immediately without a @@ -46,15 +46,19 @@ async def get_settings(request: Request): s[key] = _SECRET_MASK else: s.pop(key, None) + response.headers["Cache-Control"] = "no-store, no-cache, max-age=0" + response.headers["Pragma"] = "no-cache" return s @router.post("/api/settings/regenerate-api-key") -async def regenerate_api_key(): +async def regenerate_api_key(response: Response): import secrets s = _load_settings() s["external_api_key"] = secrets.token_urlsafe(32) _save_settings(s) + response.headers["Cache-Control"] = "no-store, no-cache, max-age=0" + response.headers["Pragma"] = "no-cache" return {"external_api_key": s["external_api_key"]} diff --git a/tests/test_security.py b/tests/test_security.py index 3db913a..5172f0b 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -79,6 +79,7 @@ class SecurityValidationTests(unittest.TestCase): payload = response.json() self.assertEqual(payload.get("llm_api_key"), "__configured__") self.assertEqual(payload.get("engine_api_keys", {}).get("local"), "__configured__") + self.assertIn("no-store", response.headers.get("cache-control", "")) self.assertNotIn("provider-secret", response.text) self.assertNotIn("engine-secret", response.text) @@ -93,6 +94,15 @@ class SecurityValidationTests(unittest.TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(_load_settings().get("llm_api_key"), "keep-this-secret") + def test_explicit_external_key_regeneration_is_not_cacheable(self): + from fastapi.testclient import TestClient + from server import app + with TestClient(app) as client: + response = client.post("/api/settings/regenerate-api-key", headers={"origin": "http://testserver"}) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.json().get("external_api_key")) + self.assertIn("no-store", response.headers.get("cache-control", "")) + def test_fish_cache_eviction_is_bounded(self): from pathlib import Path from tempfile import TemporaryDirectory