diff --git a/backend/app/routers/broker.py b/backend/app/routers/broker.py index 2d2a2ce..61105fc 100644 --- a/backend/app/routers/broker.py +++ b/backend/app/routers/broker.py @@ -1,5 +1,6 @@ import os from datetime import datetime, timedelta +from urllib.parse import urlsplit, urlunsplit from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import RedirectResponse @@ -53,6 +54,7 @@ from app.services.zerodha_storage import ( ) router = APIRouter(prefix="/api/broker") +DEFAULT_PRODUCTION_DASHBOARD_URL = "https://app.quantfortune.com/dashboard?armed=false" def _require_user(request: Request): @@ -72,6 +74,31 @@ def _require_session_id(request: Request) -> str: return session_id +def _build_broker_dashboard_url(request: Request) -> str: + configured_dashboard = (os.getenv("BROKER_DASHBOARD_URL") or "").strip() + if configured_dashboard: + return configured_dashboard + + app_base_url = (os.getenv("APP_BASE_URL") or "").strip() + if app_base_url: + return f"{app_base_url.rstrip('/')}/dashboard?armed=false" + + base_url = str(request.base_url).rstrip("/") + parsed = urlsplit(base_url) + hostname = (parsed.hostname or "").strip().lower() + scheme = parsed.scheme or "https" + + if hostname in {"localhost", "127.0.0.1"}: + return f"{scheme}://{hostname}:5173/dashboard?armed=false" + + if hostname.startswith("api.") and len(hostname) > 4: + scheme = "https" + frontend_netloc = parsed.netloc.replace(hostname, f"app.{hostname[4:]}", 1) + return urlunsplit((scheme, frontend_netloc, "/dashboard", "armed=false", "")) + + return DEFAULT_PRODUCTION_DASHBOARD_URL + + def _first_number(*values, default: float = 0.0) -> float: for value in values: try: @@ -654,7 +681,7 @@ async def broker_callback(request: Request, request_token: str = "", state: str broker_user_id=session_data.get("user_id"), auth_state="VALID", ) - target_url = os.getenv("BROKER_DASHBOARD_URL") or "/dashboard?armed=false" + target_url = _build_broker_dashboard_url(request) return RedirectResponse(target_url) diff --git a/backend/tests/test_security_hardening.py b/backend/tests/test_security_hardening.py index 02f6774..338d06b 100644 --- a/backend/tests/test_security_hardening.py +++ b/backend/tests/test_security_hardening.py @@ -173,6 +173,103 @@ def test_wrong_or_expired_broker_callback_state_fails(monkeypatch): assert response.json() == {"detail": "Invalid or expired broker callback state"} +def test_reconnect_callback_redirects_to_app_dashboard_by_default(monkeypatch): + import app.main as app_main + import app.routers.broker as broker_router + + monkeypatch.setenv("APP_ENV", "test") + monkeypatch.setenv("DISABLE_STARTUP_TASKS", "1") + monkeypatch.setenv("CORS_ORIGINS", "http://localhost:3000") + monkeypatch.delenv("BROKER_DASHBOARD_URL", raising=False) + monkeypatch.delenv("APP_BASE_URL", raising=False) + importlib.reload(app_main) + app = app_main.create_app() + client = TestClient(app) + + monkeypatch.setattr(broker_router, "get_user_for_session", lambda _sid: {"id": "user-1", "username": "user@example.com"}) + monkeypatch.setattr( + broker_router, + "consume_broker_callback_state", + lambda **kwargs: {"id": "state-1", "expires_at": datetime.now(timezone.utc).isoformat()}, + ) + monkeypatch.setattr( + broker_router, + "get_broker_credentials", + lambda _user_id: {"api_key": "kite-key", "api_secret": "kite-secret"}, + ) + monkeypatch.setattr( + broker_router, + "exchange_request_token", + lambda api_key, api_secret, token: { + "access_token": "access-token", + "request_token": token, + "user_name": "Trader", + "user_id": "Z123", + }, + ) + monkeypatch.setattr(broker_router, "set_zerodha_session", lambda user_id, payload: None) + monkeypatch.setattr(broker_router, "set_connected_broker", lambda user_id, broker, token, **kwargs: None) + + response = client.get( + "/api/broker/callback", + params={"request_token": "request-token", "state": "valid-state"}, + cookies={"session_id": "session-1"}, + follow_redirects=False, + headers={"host": "api.quantfortune.com"}, + ) + + assert response.status_code == 307 + assert response.headers["location"] == "https://app.quantfortune.com/dashboard?armed=false" + + +def test_reconnect_callback_uses_configured_dashboard_url(monkeypatch): + import app.main as app_main + import app.routers.broker as broker_router + + monkeypatch.setenv("APP_ENV", "test") + monkeypatch.setenv("DISABLE_STARTUP_TASKS", "1") + monkeypatch.setenv("CORS_ORIGINS", "http://localhost:3000") + monkeypatch.setenv("BROKER_DASHBOARD_URL", "https://app.quantfortune.com/dashboard?armed=false") + importlib.reload(app_main) + app = app_main.create_app() + client = TestClient(app) + + monkeypatch.setattr(broker_router, "get_user_for_session", lambda _sid: {"id": "user-1", "username": "user@example.com"}) + monkeypatch.setattr( + broker_router, + "consume_broker_callback_state", + lambda **kwargs: {"id": "state-1", "expires_at": datetime.now(timezone.utc).isoformat()}, + ) + monkeypatch.setattr( + broker_router, + "get_broker_credentials", + lambda _user_id: {"api_key": "kite-key", "api_secret": "kite-secret"}, + ) + monkeypatch.setattr( + broker_router, + "exchange_request_token", + lambda api_key, api_secret, token: { + "access_token": "access-token", + "request_token": token, + "user_name": "Trader", + "user_id": "Z123", + }, + ) + monkeypatch.setattr(broker_router, "set_zerodha_session", lambda user_id, payload: None) + monkeypatch.setattr(broker_router, "set_connected_broker", lambda user_id, broker, token, **kwargs: None) + + response = client.get( + "/api/broker/callback", + params={"request_token": "request-token", "state": "valid-state"}, + cookies={"session_id": "session-1"}, + follow_redirects=False, + headers={"host": "api.quantfortune.com"}, + ) + + assert response.status_code == 307 + assert response.headers["location"] == "https://app.quantfortune.com/dashboard?armed=false" + + def test_broker_callback_state_service_rejects_wrong_user_and_expired_state(monkeypatch): import app.services.broker_callback_state as callback_state