diff --git a/src/backend/main.py b/src/backend/main.py index a9d2f1d..c54ea76 100644 --- a/src/backend/main.py +++ b/src/backend/main.py @@ -44,6 +44,7 @@ async def lifespan(_: FastAPI): ) app.mount("/assets", StaticFiles(directory=ASSETS_DIR), name="assets") +app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") @app.get("/") async def read_root(request: Request, auth: Optional[SessionData] = Depends(optional_auth)): diff --git a/src/backend/routers/auth.py b/src/backend/routers/auth.py index 0d013bc..25194fd 100644 --- a/src/backend/routers/auth.py +++ b/src/backend/routers/auth.py @@ -2,9 +2,10 @@ import jwt import httpx from fastapi import APIRouter, Request, HTTPException, Depends -from fastapi.responses import RedirectResponse +from fastapi.responses import RedirectResponse, FileResponse +import os -from config import get_auth_url, get_token_url, OIDC_CONFIG, sessions +from config import get_auth_url, get_token_url, OIDC_CONFIG, sessions, STATIC_DIR from dependencies import SessionData, require_auth from coder import CoderAPI @@ -12,18 +13,21 @@ coder_api = CoderAPI() @auth_router.get("/login") -async def login(request: Request, kc_idp_hint: str = None): +async def login(request: Request, kc_idp_hint: str = None, popup: str = None): session_id = secrets.token_urlsafe(32) auth_url = get_auth_url() + state = "popup" if popup == "1" else "default" if kc_idp_hint: auth_url = f"{auth_url}&kc_idp_hint={kc_idp_hint}" + # Add state param to OIDC URL + auth_url = f"{auth_url}&state={state}" response = RedirectResponse(auth_url) response.set_cookie('session_id', session_id) return response @auth_router.get("/callback") -async def callback(request: Request, code: str): +async def callback(request: Request, code: str, state: str = "default"): session_id = request.cookies.get('session_id') if not session_id: raise HTTPException(status_code=400, detail="No session") @@ -53,19 +57,32 @@ async def callback(request: Request, code: str): user_info ) coder_api.ensure_workspace_exists(user_data['username']) - - except Exception as e: print(f"Error in user/workspace setup: {str(e)}") # Continue with login even if Coder API fails - - return RedirectResponse('/') + + if state == "popup": + return FileResponse(os.path.join(STATIC_DIR, "auth/popup-close.html")) + else: + return RedirectResponse('/') @auth_router.get("/logout") async def logout(request: Request): session_id = request.cookies.get('session_id') if session_id in sessions: del sessions[session_id] - response = RedirectResponse('/') - response.delete_cookie('session_id') + + # Create a response that doesn't redirect but still clears the cookie + from fastapi.responses import JSONResponse + response = JSONResponse({"status": "success", "message": "Logged out successfully"}) + + # Clear the session_id cookie with all necessary parameters + response.delete_cookie( + key="session_id", + path="/", + domain=None, # Use None to match the current domain + secure=request.url.scheme == "https", + httponly=True + ) + return response diff --git a/src/frontend/public/auth/popup-close.html b/src/frontend/public/auth/popup-close.html new file mode 100644 index 0000000..ef2b783 --- /dev/null +++ b/src/frontend/public/auth/popup-close.html @@ -0,0 +1,34 @@ + + + + + Authentication Complete + + + +
Authentication complete! You may close this window.
+ + + diff --git a/src/frontend/src/api/hooks.ts b/src/frontend/src/api/hooks.ts index b8562cd..91373d1 100644 --- a/src/frontend/src/api/hooks.ts +++ b/src/frontend/src/api/hooks.ts @@ -132,10 +132,16 @@ export function useUserProfile(options?: UseQueryOptions) { } export function useWorkspaceState(options?: UseQueryOptions) { + // Get the current auth state from the query cache + const authState = queryClient.getQueryData(['auth']); + return useQuery({ queryKey: ['workspaceState'], queryFn: api.getWorkspaceState, - refetchInterval: 5000, // Poll every 5 seconds + // Only poll if authenticated + refetchInterval: authState === true ? 5000 : false, // Poll every 5 seconds if authenticated, otherwise don't poll + // Don't retry on error if not authenticated + retry: authState === true ? 1 : false, ...options, }); } diff --git a/src/frontend/src/auth/AuthModal.tsx b/src/frontend/src/auth/AuthModal.tsx index 7dc4273..088bdb8 100644 --- a/src/frontend/src/auth/AuthModal.tsx +++ b/src/frontend/src/auth/AuthModal.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import ReactDOM from "react-dom"; import { capture } from "../utils/posthog"; import { Mail } from "lucide-react"; +import { queryClient } from "../api/queryClient"; import "../styles/AuthModal.scss"; interface AuthModalProps { @@ -16,11 +17,22 @@ const AuthModal: React.FC = ({ useEffect(() => { setIsMounted(true); capture("auth_modal_shown"); - // Prevent scrolling when modal is open - document.body.style.overflow = "hidden"; + }, []); + useEffect(() => { + const checkLocalStorage = () => { + const authCompleted = localStorage.getItem('auth_completed'); + if (authCompleted) { + localStorage.removeItem('auth_completed'); + queryClient.invalidateQueries({ queryKey: ['auth'] }); + clearInterval(intervalId); + } + }; + + const intervalId = setInterval(checkLocalStorage, 500); + return () => { - document.body.style.overflow = "auto"; + clearInterval(intervalId); }; }, []); @@ -62,7 +74,11 @@ const AuthModal: React.FC = ({