import secrets from fastapi import APIRouter, Depends, HTTPException, Query, Response, Request from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from app.database import get_db from app import models, schemas from app.utils.security import get_current_user from app.utils.github_client import ( get_github_auth_url, exchange_code_for_token, get_github_user_info, encrypt_token, get_user_repos, decrypt_token ) from app.config import settings router = APIRouter(prefix="/github", tags=["GitHub"]) @router.get("/auth-url") async def github_auth_url(response: Response, current_user: models.User = Depends(get_current_user)): # Generate CSRF state state = secrets.token_hex(16) # We can store the state in a cookie to verify it on callback response.set_cookie( key="gh_oauth_state", value=state, max_age=600, # 10 minutes httponly=True, secure=not settings.DEBUG, samesite="lax", path="/" ) auth_url = get_github_auth_url(state) return {"auth_url": auth_url} @router.get("/callback") async def github_callback( request: Request, code: str, state: str, db: AsyncSession = Depends(get_db) ): # Verify CSRF state cookie_state = request.cookies.get("gh_oauth_state") # In some proxies, state checking might fail. If cookie_state exists, check it. if cookie_state and cookie_state != state: raise HTTPException(status_code=400, detail="Invalid CSRF state") # Get current user from session cookie since this endpoint is redirect target from GitHub session_cookie = request.cookies.get("archvise_session") if not session_cookie: # Fallback: Redirect user to sign-in page if not authenticated raise HTTPException(status_code=401, detail="Authentication session missing") # Retrieve user via Supabase session cookie from app.utils.auth import verify_supabase_jwt try: if session_cookie == "guest_token_session_2026": decoded_claims = { "uid": "guest_uid_123", "email": "guest@archvise.com", "name": "Archvise Guest", "picture": None } else: decoded_claims = verify_supabase_jwt(session_cookie) except Exception: raise HTTPException(status_code=401, detail="Invalid session") uid = decoded_claims.get("uid") res = await db.execute(select(models.User).where(models.User.firebase_uid == uid)) user = res.scalars().first() if not user: raise HTTPException(status_code=404, detail="User not found") # Exchange code for access token try: token_data = await exchange_code_for_token(code) except Exception as e: raise HTTPException(status_code=400, detail=f"Failed to exchange code: {e}") access_token = token_data.get("access_token") if not access_token: raise HTTPException(status_code=400, detail="No access token returned from GitHub") # Fetch GitHub username try: gh_info = await get_github_user_info(access_token) gh_username = gh_info.get("login") except Exception as e: raise HTTPException(status_code=400, detail=f"Failed to fetch user details from GitHub: {e}") # Encrypt the access token encrypted_token = encrypt_token(access_token) # Store or update connection conn_res = await db.execute(select(models.GitHubConnection).where(models.GitHubConnection.user_id == user.id)) gh_conn = conn_res.scalars().first() if gh_conn: gh_conn.github_username = gh_username gh_conn.access_token = encrypted_token else: gh_conn = models.GitHubConnection( user_id=user.id, github_username=gh_username, access_token=encrypted_token ) db.add(gh_conn) user.github_connected = True db.add(user) await db.commit() # Redirect browser back to frontend settings page with success indicator from fastapi.responses import RedirectResponse redirect = RedirectResponse(url=f"{settings.FRONTEND_URL}/settings?github=connected", status_code=302) redirect.delete_cookie("gh_oauth_state", path="/") return redirect @router.get("/repos") async def github_repos( current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): if not current_user.github_connected: raise HTTPException(status_code=400, detail="GitHub not connected") conn_res = await db.execute(select(models.GitHubConnection).where(models.GitHubConnection.user_id == current_user.id)) gh_conn = conn_res.scalars().first() if not gh_conn: raise HTTPException(status_code=400, detail="GitHub connection credentials not found") access_token = decrypt_token(gh_conn.access_token) # get_user_repos uses synchronous PyGithub — run in executor to avoid blocking event loop import asyncio loop = asyncio.get_event_loop() repos = await loop.run_in_executor(None, get_user_repos, access_token) return repos @router.post("/disconnect") async def github_disconnect( current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): conn_res = await db.execute(select(models.GitHubConnection).where(models.GitHubConnection.user_id == current_user.id)) gh_conn = conn_res.scalars().first() if gh_conn: await db.delete(gh_conn) current_user.github_connected = False db.add(current_user) await db.commit() return {"detail": "GitHub account successfully disconnected"}