diff --git a/deploy.sh b/deploy.sh index da1c2d87..a2c07731 100644 --- a/deploy.sh +++ b/deploy.sh @@ -129,6 +129,8 @@ compose_up_retry() { export IMAGE_TAG="$NEW_TAG" export POLYWEATHER_API_BASE_URL="${POLYWEATHER_FRONTEND_INTERNAL_API_BASE_URL:-http://polyweather_web:8000}" +export SUPABASE_URL="${SUPABASE_URL:-${NEXT_PUBLIC_SUPABASE_URL:-}}" +export SUPABASE_ANON_KEY="${SUPABASE_ANON_KEY:-${NEXT_PUBLIC_SUPABASE_ANON_KEY:-}}" pull_ok=0 for pull_attempt in $(seq 1 6); do docker compose pull && pull_ok=1 && break diff --git a/docker-compose.yml b/docker-compose.yml index 9863dd2b..8c2c1d72 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,6 +71,7 @@ services: NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL: ${NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL:-https://polygon-bor-rpc.publicnode.com} NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: ${NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID:-} + POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN: ${POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN} POLYWEATHER_API_BASE_URL: http://polyweather_web:8000 POLYWEATHER_AUTH_ENABLED: ${POLYWEATHER_AUTH_ENABLED:-true} POLYWEATHER_AUTH_REQUIRED: ${POLYWEATHER_AUTH_REQUIRED:-true} @@ -120,7 +121,11 @@ services: POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC: '30' POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS: ${POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS:-4} POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: ${POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED:-false} + POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN: ${POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN} POLYWEATHER_SERVICE_ROLE: web + SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY} + SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} + SUPABASE_URL: ${SUPABASE_URL} UVICORN_WORKERS: ${UVICORN_WORKERS:-2} healthcheck: interval: 30s diff --git a/frontend/app/api/feedback/route.ts b/frontend/app/api/feedback/route.ts index d3ff02f4..b86a40ba 100644 --- a/frontend/app/api/feedback/route.ts +++ b/frontend/app/api/feedback/route.ts @@ -50,6 +50,9 @@ export async function GET(req: NextRequest) { cache: "no-store", }); const raw = await res.text(); + if (res.status === 401 || res.status === 403) { + return applyAuthResponseCookies(emptyFeedbackResponse(), auth.response); + } if (!res.ok) { return applyAuthResponseCookies( buildUpstreamErrorResponse(res.status, raw, { diff --git a/frontend/components/dashboard/scan-terminal/__tests__/feedbackWorkflow.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/feedbackWorkflow.test.ts index d2041840..3a6b5aa5 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/feedbackWorkflow.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/feedbackWorkflow.test.ts @@ -72,6 +72,7 @@ export function runTests() { assert( feedbackRouteSource.includes("emptyFeedbackResponse") && feedbackRouteSource.includes("if (!auth.authUserId)") && + feedbackRouteSource.includes("if (res.status === 401 || res.status === 403)") && !feedbackRouteSource.includes("const authError = requireBackendPaymentAuth(auth);"), "feedback GET proxy must return an empty optional list instead of surfacing a 401 when no Supabase user identity is verified", ); diff --git a/tests/test_deployment_runtime_config.py b/tests/test_deployment_runtime_config.py index 2a04a702..5516c30d 100644 --- a/tests/test_deployment_runtime_config.py +++ b/tests/test_deployment_runtime_config.py @@ -297,6 +297,36 @@ def test_frontend_proxy_uses_internal_backend_url_not_public_site(): ) +def test_frontend_and_web_share_supabase_forwarded_identity_secrets(): + compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") + script = (ROOT / "deploy.sh").read_text(encoding="utf-8") + frontend_block = compose.split(" polyweather_frontend:", 1)[1].split( + "\n polyweather_web:", + 1, + )[0] + web_block = compose.split(" polyweather_web:", 1)[1].split( + "\n polyweather_collector:", + 1, + )[0] + + assert ( + "POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN: ${POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN}" + in frontend_block + ) + assert "SUPABASE_URL: ${SUPABASE_URL}" in web_block + assert "SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY}" in web_block + assert "SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY}" in web_block + assert ( + "POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN: ${POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN}" + in web_block + ) + assert 'export SUPABASE_URL="${SUPABASE_URL:-${NEXT_PUBLIC_SUPABASE_URL:-}}"' in script + assert ( + 'export SUPABASE_ANON_KEY="${SUPABASE_ANON_KEY:-${NEXT_PUBLIC_SUPABASE_ANON_KEY:-}}"' + in script + ) + + def test_web_container_raises_open_file_limit_for_sse_and_proxy_load(): compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") web_block = compose.split(" polyweather_web:", 1)[1].split( diff --git a/tests/test_user_feedback.py b/tests/test_user_feedback.py index 9b02ac55..290d92dd 100644 --- a/tests/test_user_feedback.py +++ b/tests/test_user_feedback.py @@ -2,8 +2,10 @@ from types import SimpleNamespace import pytest from fastapi import HTTPException +from starlette.requests import Request from src.database.db_manager import DBManager +import web.core as web_core from web.services import feedback_api from web.services import ops_api @@ -296,3 +298,42 @@ def test_list_current_user_feedback_requires_identity(tmp_path, monkeypatch): with pytest.raises(HTTPException) as exc_info: feedback_api.list_current_user_feedback(anonymous_request, limit=20) assert exc_info.value.status_code == 401 + + +def test_list_current_user_feedback_accepts_forwarded_identity_when_supabase_unconfigured( + tmp_path, monkeypatch +): + db = DBManager(str(tmp_path / "polyweather-feedback-forwarded.db")) + db.append_user_feedback( + category="bug", + message="Forwarded identity feedback.", + user_id="user-a", + user_email="a@example.com", + ) + db.append_user_feedback( + category="bug", + message="Other user feedback.", + user_id="user-b", + user_email="b@example.com", + ) + monkeypatch.setattr(feedback_api, "DBManager", lambda: db) + monkeypatch.setattr(web_core, "_ENTITLEMENT_TOKEN", "backend-token") + monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "supabase_url", "") + monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "anon_key", "") + + request = Request( + { + "type": "http", + "headers": [ + (b"x-polyweather-entitlement", b"backend-token"), + (b"x-polyweather-auth-user-id", b"user-a"), + (b"x-polyweather-auth-email", b"a@example.com"), + (b"authorization", b"Bearer access-token"), + ], + } + ) + + payload = feedback_api.list_current_user_feedback(request, limit=20) + + assert payload["total"] == 1 + assert payload["feedback"][0]["message"] == "Forwarded identity feedback." diff --git a/web/core.py b/web/core.py index dec6e364..cfdb6396 100644 --- a/web/core.py +++ b/web/core.py @@ -233,15 +233,15 @@ def _bind_forwarded_supabase_identity(request: Request) -> bool: def _bind_optional_supabase_identity(request: Request) -> None: + if _bind_forwarded_supabase_identity(request): + return if not SUPABASE_ENTITLEMENT.configured: return access_token = extract_bearer_token(request.headers.get("authorization")) if not access_token: - _bind_forwarded_supabase_identity(request) return identity = SUPABASE_ENTITLEMENT.get_identity(access_token) if not identity: - _bind_forwarded_supabase_identity(request) return request.state.auth_user_id = identity.user_id request.state.auth_email = identity.email