Skip to content

feat: GitHub OAuth one-click repo import#230

Merged
DevanshuNEU merged 17 commits into
OpenCodeIntel:mainfrom
DevanshuNEU:feat/github-oauth-import
Jan 28, 2026
Merged

feat: GitHub OAuth one-click repo import#230
DevanshuNEU merged 17 commits into
OpenCodeIntel:mainfrom
DevanshuNEU:feat/github-oauth-import

Conversation

@DevanshuNEU

@DevanshuNEU DevanshuNEU commented Jan 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add one-click GitHub repository import feature with secure server-side OAuth token storage.

Features

  • One-click import: Users can connect their GitHub account and import repositories with a single click
  • Secure OAuth flow: Tokens stored server-side only, never exposed to frontend
  • Private repo support: Full repo scope for accessing private repositories
  • Separate OAuth app: Uses dedicated OAuth app, doesn't interfere with Supabase login

Screenshots

Connect GitHub Select Repos Import Complete
Modal prompts user to connect Browse and select repos Repos added and indexing

Implementation Details

Backend

  • routes/github.py - OAuth endpoints (/connect, /callback, /status, /repos, /disconnect)
  • services/github.py - GitHub API wrapper with pagination support
  • migrations/003_github_connections.sql - Secure token storage table with RLS

Frontend

  • GitHubRepoSelector.tsx - Modal for browsing/selecting repos
  • GitHubCallbackPage.tsx - OAuth redirect handler
  • useGitHubRepos.ts - React hook for GitHub API integration

Security

  • CSRF protection via state parameter
  • Token validation on each use
  • Auto-cleanup on token revocation
  • Supabase RLS prevents direct token access

Setup Required

  1. Create GitHub OAuth App for repo import (separate from Supabase login)
  2. Add credentials to backend .env:
    GITHUB_CLIENT_ID=xxx
    GITHUB_CLIENT_SECRET=xxx
    GITHUB_REDIRECT_URI=http://localhost:3000/github/callback
    FRONTEND_URL=http://localhost:3000
    
  3. Run migration: supabase/migrations/003_github_connections.sql

Testing

  • OAuth flow completes successfully
  • Repos list populates correctly
  • Private repos visible with lock icon
  • Multi-select with quota enforcement
  • Import triggers indexing
  • Token stored server-side only

Related Issues

Closes #XXX (if applicable)


Note: For production, create separate OAuth app with production callback URL.

Summary by CodeRabbit

  • New Features

    • GitHub OAuth: connect, callback, status, disconnect, and list user repositories.
    • Searchable multi-select modal to choose and import multiple GitHub repos.
    • Frontend OAuth callback page to finalize connection and return to dashboard.
  • Improvements

    • Dashboard: "Import from GitHub" option, import progress UI, error handling, and repo-cap enforcement.
    • Exposed client access to support integration flows.
  • Chores

    • Expanded/reorganized backend env configuration and DB migration for GitHub connections (safe public view).

✏️ Tip: You can customize this high-level summary in your review settings.

Secure server-side OAuth implementation for importing repos from GitHub.

Backend:
- Add github_connections table for secure token storage (RLS protected)
- OAuth flow: /github/connect, /github/callback, /github/status
- GitHubService for API interactions (repos, user info)
- Token never exposed to frontend, stored server-side only
- CSRF protection via state parameter

Frontend:
- GitHubRepoSelector modal for browsing/selecting repos
- GitHubCallbackPage for OAuth redirect handling
- useGitHubRepos hook for API integration
- Dashboard integration with 'Import from GitHub' button
- Free tier: 3 repo limit enforced in UI

Security:
- Separate OAuth scopes (user:email for login, repo for import)
- Token validation on each use
- Auto-cleanup on token revocation
- Supabase service_role for server-side token access
- Add clear section headers for organization
- Document GitHub OAuth setup for repo import feature
- Explain separation from Supabase login OAuth
- Add instructions for dev vs production setup
@vercel

vercel Bot commented Jan 28, 2026

Copy link
Copy Markdown

@DevanshuNEU is attempting to deploy a commit to the Dev's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jan 28, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds GitHub OAuth-based repository import: environment updates, backend GitHub router and service, DB migration for github_connections, frontend hook, selector modal, callback page/route, dashboard import flow, and exports Supabase client.

Changes

Cohort / File(s) Summary
Backend env & config
backend/.env.example
Reorganized env vars: added Voyage AI key, GitHub OAuth vars (GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_REDIRECT_URI, FRONTEND_URL), SUPABASE_SERVICE_ROLE_KEY, BACKEND_API_URL, API_KEY, SENTRY_DSN, and ENVIRONMENT with updated grouping/comments.
Backend routing
backend/main.py
Registered new GitHub APIRouter under API prefix.
Backend GitHub API & service
backend/routes/github.py, backend/services/github.py
New APIRouter /github with endpoints /status, /connect, /callback, /disconnect, /repos; new async GitHubService, dataclasses for user/repo, token exchange/validation, repo listing with pagination, DB helpers for persisting connections and state handling.
Database migration
supabase/migrations/003_github_connections.sql
New github_connections table (RLS enabled), github_connections_public view excluding access_token, index on user_id, updated_at trigger/function, SELECT policy, and comments.
Frontend hook & types
frontend/src/hooks/useGitHubRepos.ts
New useGitHubRepos hook and GitHubRepo/GitHubStatus types to manage connect/disconnect, completeConnect, fetchRepos, status, pagination, and errors.
Frontend callback page & route
frontend/src/pages/GitHubCallbackPage.tsx, frontend/src/App.tsx
New OAuth callback page handling code/state/error, calls completeConnect; added protected /github/callback route.
Frontend repo selector & dashboard
frontend/src/components/GitHubRepoSelector.tsx, frontend/src/components/dashboard/DashboardHome.tsx
New modal for listing/searching/selecting/importing GitHub repos; dashboard integrates "Import from GitHub" flow, multi-import handling, indexing kickoff, and MAX_FREE_REPOS enforcement.
Frontend auth export
frontend/src/contexts/AuthContext.tsx
Exported supabase client for external use by hooks/components.

Sequence Diagram(s)

sequenceDiagram
    participant User as User / Browser
    participant Frontend as Frontend App
    participant Backend as Backend API
    participant GitHub as GitHub OAuth/API
    participant DB as Database

    User->>Frontend: Click "Import from GitHub"
    Frontend->>Backend: GET /github/connect
    Backend->>GitHub: Build auth URL (client_id, state, redirect_uri)
    Backend-->>Frontend: Return auth_url + state
    Frontend->>GitHub: Redirect user to auth_url
    GitHub->>User: Authorize & redirect to `/github/callback?code&state`
    Frontend->>Frontend: GitHubCallbackPage reads code & state
    Frontend->>Backend: POST /github/callback { code, state }
    Backend->>GitHub: Exchange code for access_token
    GitHub-->>Backend: Return access_token
    Backend->>GitHub: GET /user (validate & fetch)
    GitHub-->>Backend: Return user info
    Backend->>DB: INSERT/UPDATE `github_connections` (store token server-side)
    Backend-->>Frontend: Return connection status
    Frontend->>Backend: GET /github/repos
    Backend->>GitHub: GET /user/repos (paginated)
    GitHub-->>Backend: Return repos
    Backend-->>Frontend: Return repo list
    User->>Frontend: Select repos -> Import
    Frontend->>Backend: Create repo records / trigger indexing
    Backend->>DB: Insert repo entries / enqueue indexing jobs
    Backend-->>Frontend: Confirm import / provide progress
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇

I hopped through OAuth, chased a key,
Gathered tiny repos for tea and me,
Stored them safe behind a vault,
Now code and carrots waltz by default! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature added: GitHub OAuth integration enabling one-click repository imports. It directly reflects the primary purpose of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@backend/routes/github.py`:
- Around line 173-188: The generated state (state =
f"{auth.user_id}:{secrets.token_urlsafe(32)}") is not fully validated on
callback; update the flow so the full state is stored server-side with expiry
and tied to auth.user_id (e.g., write the state into Redis/DB when creating the
GitHubConnectResponse) and on the OAuth callback fetch-and-compare the entire
stored state against the returned state before trusting it; if persistent
storage isn't available yet, at minimum validate the returned state format and
token portion (ensure it matches user_id:token, check token length/charset
produced by secrets.token_urlsafe and reject mismatches) and replace the current
partial check that only verifies prefix matching.
- Around line 117-127: The update in _update_last_used currently writes the
literal string "now()" which PostgreSQL will not evaluate as NOW(); change it to
send a real timestamp instead by importing datetime and set last_used_at to a
proper Python datetime (e.g. datetime.datetime.utcnow()) when calling
db.table("github_connections").update(...). Keep the same function name
_update_last_used and the same db.table("github_connections").update call;
ensure the value is a datetime object (or an ISO 8601 string) so
Supabase/Postgres stores a proper timestamp rather than the literal "now()".

In `@backend/services/github.py`:
- Around line 63-79: The get_user method lacks exception handling and can raise
network or parsing errors; wrap the HTTP request and JSON parsing in a
try/except (similar to validate_token) to catch httpx.RequestError and
httpx.TimeoutException (or broad httpx.HTTPError) and JSON/KeyError parsing
errors, log the error via your logger or return None on failure, and ensure you
still return a GitHubUser only on successful response.status_code == 200 and
valid JSON; update get_user and related JSON extraction
(login/id/avatar_url/name) to handle missing keys gracefully or fall back to
None.

In `@frontend/src/components/dashboard/DashboardHome.tsx`:
- Around line 153-156: When indexing fails (the branch checking indexResponse.ok
in DashboardHome.tsx), add a user-visible warning toast in addition to the
console.error so users know indexing didn't start for that repo; extract the
error payload you already await (err) and include repo.name and a concise error
message in the toast, using the project's existing toast utility (e.g., toast or
useToast) instead of only logging to console, and keep the console.error for
debugging.

In `@frontend/src/components/GitHubRepoSelector.tsx`:
- Around line 30-43: The issue is unstable callback references caused by the
full Supabase `session` object changing, which forces `checkStatus`,
`clearError`, and `fetchRepos` to change and retrigger the useEffects in
GitHubRepoSelector; fix by stabilizing the dependency chain: in useGitHubRepos
(functions getHeaders, checkStatus, fetchRepos) replace `session` as a
dependency with only the `access_token` (e.g., read session?.access_token once
and use that primitive in the dependency arrays) or alternatively memoize the
`session` object in your AuthContext via useMemo so its reference is stable;
update the dependency lists of checkStatus/clearError/fetchRepos to depend on
the stable access_token or memoized session to prevent unnecessary re-renders.

In `@frontend/src/pages/GitHubCallbackPage.tsx`:
- Around line 13-49: The effect calling useEffect -> handleCallback should guard
against double-execution and clear the redirect timeout: add a ref (import
useRef) like callbackRanRef to skip re-running handleCallback if already
executed, and store the setTimeout ID in a ref or variable so the effect returns
a cleanup function that clears that timeout (clearTimeout) and flips any
mounted/aborted flag to avoid calling setStatus or navigate after unmount;
ensure the guard checks callbackRanRef before running completeConnect and set
the ref once handled, and clear the timeout in the cleanup to prevent the memory
leak.

In `@supabase/migrations/003_github_connections.sql`:
- Around line 23-32: The current RLS policy "Users can view own connection
exists" on github_connections still allows SELECT of all columns (including
access_token); fix by exposing only non-sensitive fields via a view or
column-limited policy: either (A) create a view github_connections_public that
SELECTs all columns except access_token and grant auth users SELECT on that view
while keeping RLS on the base table for service_role-only access, or (B) if
using PostgreSQL 15+, replace the policy with a column-restricted SELECT policy
(limiting returned columns to the safe set) so auth.uid() = user_id still
applies but access_token is excluded; update any frontend queries to use
github_connections_public (or the column-limited policy) instead of selecting
directly from github_connections.
🧹 Nitpick comments (9)
backend/services/github.py (2)

50-61: Consider logging exceptions for observability.

Silently swallowing all exceptions makes it difficult to diagnose token validation failures (e.g., network issues vs. rate limits vs. invalid tokens).

♻️ Suggested improvement
+import logging
+
+logger = logging.getLogger(__name__)
+
     async def validate_token(self) -> bool:
         """Check if the token is valid by fetching user info"""
         try:
             async with httpx.AsyncClient() as client:
                 response = await client.get(
                     f"{GITHUB_API_BASE}/user",
                     headers=self.headers,
                     timeout=10.0
                 )
                 return response.status_code == 200
-        except Exception:
+        except Exception as e:
+            logger.warning(f"Token validation failed: {e}")
             return False

109-110: Preserve error details from GitHub API response.

The generic exception loses valuable debugging information. Consider including the response body which often contains error details from GitHub.

♻️ Suggested improvement
             if response.status_code != 200:
-                raise Exception(f"GitHub API error: {response.status_code}")
+                error_detail = response.text[:200] if response.text else "No details"
+                raise Exception(f"GitHub API error: {response.status_code} - {error_detail}")
supabase/migrations/003_github_connections.sql (1)

20-21: Redundant index - UNIQUE constraint already creates one.

The UNIQUE(user_id) constraint on line 17 automatically creates an index on user_id. This explicit index is unnecessary.

♻️ Remove redundant index
--- Remove this line as UNIQUE constraint already creates an index
-CREATE INDEX IF NOT EXISTS idx_github_connections_user_id ON github_connections(user_id);
frontend/src/components/dashboard/DashboardHome.tsx (1)

119-176: Loading state management issue in sequential import loop.

The setLoading(true) is called inside the loop on each iteration (line 125), but if an exception is thrown and caught in the inner try-catch, the loop continues without resetting loading. More critically, if the loop is interrupted unexpectedly before reaching line 174, the loading state will remain true.

Consider moving setLoading(true) before the loop and ensuring cleanup in a finally block:

♻️ Proposed refactor
  const handleGitHubImport = async (githubRepos: GitHubRepo[]) => {
    if (githubRepos.length === 0) return
    
+   setLoading(true)
+   try {
      // Import repos one at a time
      for (const repo of githubRepos) {
        try {
-         setLoading(true)
          const response = await fetch(`${API_URL}/repos`, {
            // ... rest of fetch logic
          })
          // ... rest of loop body
        } catch (error) {
          console.error(`Error importing ${repo.name}:`, error)
          toast.error(`Failed to import ${repo.name}`, { 
            description: error instanceof Error ? error.message : 'Please try again' 
          })
        }
      }
-   
-   setLoading(false)
-   await fetchRepos()
+     await fetchRepos()
+   } finally {
+     setLoading(false)
+   }
  }
frontend/src/components/GitHubRepoSelector.tsx (1)

114-119: Add accessibility attributes to the close button.

The close button lacks an aria-label, which is important for screen reader users. Consider also adding keyboard support for closing with Escape key.

♿ Accessibility improvement
            <button
              onClick={onClose}
+             aria-label="Close dialog"
              className="w-8 h-8 flex items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
            >
              <X className="w-4 h-4" />
            </button>

For Escape key support, add a useEffect:

useEffect(() => {
  const handleEscape = (e: KeyboardEvent) => {
    if (e.key === 'Escape' && isOpen) onClose();
  };
  document.addEventListener('keydown', handleEscape);
  return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen, onClose]);
frontend/src/hooks/useGitHubRepos.ts (2)

179-212: Potential stale closure in pagination logic.

Line 203 uses repos from the closure when appending paginated results. This could lead to stale data if multiple fetch calls overlap or if the component re-renders between the state update. Use a functional state update to ensure you're working with the latest state.

♻️ Proposed fix
      const data = await response.json();
-     setRepos(page === 1 ? data : [...repos, ...data]);
+     setRepos(prev => page === 1 ? data : [...prev, ...data]);
      return data;

This also allows removing repos from the useCallback dependency array, preventing unnecessary callback recreation.


4-4: Remove duplicate API_URL definition and use config module.

The API URL is defined locally but also exists in frontend/src/config/api.ts. The config module exports API_URL that already includes the /api/v1 prefix (evaluates to http://localhost:8000/api/v1), whereas the local definition here is just the base URL without the API version prefix. Consolidate by importing from the config module, but note that endpoints will need adjustment since the config's API_URL already includes /api/v1:

♻️ Suggested fix

Change from:

-const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
+import { API_URL } from '@/config/api';

Then update all endpoints from ${API_URL}/api/v1/github/* to ${API_URL}/github/*, or alternatively use the provided buildApiUrl helper:

-const response = await fetch(`${API_URL}/api/v1/github/status`, { headers });
+const response = await fetch(buildApiUrl('/github/status'), { headers });
backend/routes/github.py (2)

76-102: Consider encrypting GitHub access tokens at rest.

The access token is stored in plaintext in the database (line 91). While RLS prevents direct client access, the tokens remain readable by anyone with service role access or database admin privileges. For sensitive credentials like OAuth tokens, consider encrypting at rest using a KMS or application-level encryption.

This is a good-to-have security enhancement that could be addressed in a follow-up if not feasible now.


333-344: Error detection via string matching is fragile but acceptable.

The check for "401" or "Bad credentials" in the error string (line 337) works but could miss other authentication error formats. Consider catching specific exception types from the GitHub service if available, or at least document this behavior.

The auto-cleanup of revoked connections is a good UX pattern.

Comment thread backend/routes/github.py
Comment thread backend/routes/github.py
Comment thread backend/services/github.py Outdated
Comment thread frontend/src/components/dashboard/DashboardHome.tsx Outdated
Comment thread frontend/src/components/GitHubRepoSelector.tsx
Comment thread frontend/src/pages/GitHubCallbackPage.tsx
Comment thread supabase/migrations/003_github_connections.sql
_update_last_used was passing string 'now()' which PostgreSQL stores
literally instead of evaluating. Now uses datetime.now(timezone.utc).isoformat()
for proper timestamp storage.
Wrap HTTP request and JSON parsing in try/except to handle network errors,
timeouts, and missing keys gracefully. Returns None on failure instead of
raising. Matches the pattern used in validate_token.
Users now see a visible warning when repo is added but indexing fails,
instead of only logging to console. Keeps console.error for debugging.
- Add callbackRanRef to prevent handleCallback running twice in StrictMode
- Add mounted flag to prevent state updates after unmount
- Store timeout in ref and clear on cleanup to prevent memory leak
- Extract access_token from session to use as stable primitive dependency
- Use useMemo for headers object based on stable access_token
- All callbacks now depend on memoized headers instead of session object
- Use functional state update in fetchRepos to avoid stale closure

Prevents unnecessary re-renders when session object reference changes.
- Validate state format is exactly 'user_id:token'
- Check user_id matches authenticated user
- Verify token length (43 chars from token_urlsafe(32))
- Validate token charset (URL-safe base64: A-Za-z0-9_-)

Replaces weak prefix-only check with full format validation.
- Add github_connections_public view excluding access_token column
- Grant SELECT on view to authenticated users
- RLS policy still enforces user_id match
- Backend continues using service_role for full table access
- Adds defense-in-depth against direct table queries

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@backend/routes/github.py`:
- Around line 349-360: The response currently returns internal error text via
HTTPException(detail=f"Failed to fetch repositories: {str(e)}"); change this to
return a generic, non-sensitive message while keeping the full exception logged
with logger.error. Specifically, in the except block in backend/routes/github.py
(the handler that calls _delete_github_connection on 401), remove or stop
including str(e) in the 500 HTTPException detail and replace it with a generic
message like "Failed to fetch repositories" (preserve the existing 401 branch
behavior), ensuring logger.error still records error=str(e) for diagnostics.

In `@backend/services/github.py`:
- Around line 76-81: The GitHubUser dataclass has avatar_url typed as str but
the factory code in the function returning GitHubUser uses
data.get("avatar_url") which can be None; either change the
GitHubUser.avatar_url annotation to Optional[str] (importing typing.Optional) or
ensure the returned value is non-null by using a default in the factory (e.g.
use data.get("avatar_url", "") or another sensible default) when constructing
GitHubUser in the function that returns GitHubUser.

In `@frontend/src/components/dashboard/DashboardHome.tsx`:
- Around line 147-167: The modal is opened even when starting async indexing
fails; update the logic after the fetch to only call setIndexingRepoId,
setIndexingRepoName, and setShowIndexingModal when indexResponse.ok is true
(i.e., indexing started successfully). Specifically, check indexResponse.ok
before setting indexing state and showing the modal (the block that currently
runs for the last repo: repo === githubRepos[githubRepos.length - 1]) so that
failures (when !indexResponse.ok) do not trigger the modal; keep the existing
error logging/toast behavior for failure cases.

In `@frontend/src/pages/GitHubCallbackPage.tsx`:
- Around line 1-72: The handleCallback flow in GitHubCallbackPage can leave the
UI stuck in 'processing' if completeConnect throws; wrap the await
completeConnect(code, state) call in a try/catch inside handleCallback, on catch
setStatus('error') and setErrorMessage with the caught error (or a user-friendly
fallback), and ensure you still respect the mounted flag before calling setState
or navigate; reference the handleCallback function, completeConnect, setStatus,
setErrorMessage, timeoutRef and mounted to locate where to add the try/catch and
error handling.
🧹 Nitpick comments (4)
backend/services/github.py (1)

118-137: Add defensive access for nested owner field to avoid potential KeyError/TypeError.

Direct access to repo["owner"]["login"] and repo["owner"]["avatar_url"] will raise an exception if owner is None or missing. While GitHub's API typically includes this field, defensive coding improves resilience.

♻️ Proposed fix
                 repos.append(GitHubRepo(
                     id=repo["id"],
                     name=repo["name"],
                     full_name=repo["full_name"],
                     description=repo.get("description"),
                     html_url=repo["html_url"],
                     clone_url=repo["clone_url"],
                     ssh_url=repo["ssh_url"],
                     default_branch=repo.get("default_branch", "main"),
                     private=repo["private"],
                     fork=repo.get("fork", False),
                     stargazers_count=repo.get("stargazers_count", 0),
                     language=repo.get("language"),
                     size=repo.get("size", 0),
-                    owner_login=repo["owner"]["login"],
-                    owner_avatar=repo["owner"]["avatar_url"]
+                    owner_login=repo.get("owner", {}).get("login", ""),
+                    owner_avatar=repo.get("owner", {}).get("avatar_url", "")
                 ))
backend/routes/github.py (3)

26-29: Missing environment variables could cause silent failures.

GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET are loaded but not validated at startup. If missing, errors only surface when endpoints are called. Consider validating during app initialization.

💡 Optional improvement

Add a startup check or log warning in the module:

if not GITHUB_CLIENT_ID or not GITHUB_CLIENT_SECRET:
    logger.warning("GitHub OAuth credentials not configured - GitHub integration disabled")

286-293: Consider revoking GitHub token on disconnect (optional).

Currently, disconnect only removes the local record. The GitHub access token remains valid. For enhanced security, consider calling GitHub's token revocation endpoint.

💡 Optional enhancement
`@router.delete`("/disconnect")
async def disconnect_github(auth: AuthContext = Depends(require_auth)):
    """Remove GitHub connection and revoke token"""
    if not auth.user_id:
        raise HTTPException(status_code=401, detail="User ID required")

    connection = await _get_github_connection(auth.user_id)
    if connection and connection.get("access_token"):
        # Optionally revoke token on GitHub
        try:
            async with httpx.AsyncClient() as client:
                await client.delete(
                    f"https://api.github.com/applications/{GITHUB_CLIENT_ID}/token",
                    auth=(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET),
                    json={"access_token": connection["access_token"]},
                    timeout=10.0
                )
        except Exception:
            pass  # Best effort revocation

    deleted = await _delete_github_connection(auth.user_id)
    return {"success": deleted}

352-358: Token revocation detection is fragile.

Checking for "401" or "Bad credentials" in the string representation of the exception is brittle. If GitHubService.get_repos is updated to raise a more specific exception type, this check would be easier to maintain.

♻️ Suggested approach

Consider having GitHubService.get_repos raise a custom exception (e.g., GitHubAuthError) for authentication failures, then catch that specifically:

# In services/github.py
class GitHubAuthError(Exception):
    """Raised when GitHub token is invalid or revoked"""
    pass

# In get_repos:
if response.status_code == 401:
    raise GitHubAuthError("Token invalid or revoked")

# In routes/github.py:
except GitHubAuthError:
    await _delete_github_connection(auth.user_id)
    raise HTTPException(status_code=401, detail="GitHub access revoked...")
except Exception as e:
    logger.error(...)
    raise HTTPException(status_code=500, detail="Failed to fetch repositories")

Comment thread backend/routes/github.py Outdated
Comment thread backend/services/github.py
Comment thread frontend/src/components/dashboard/DashboardHome.tsx
Comment thread frontend/src/pages/GitHubCallbackPage.tsx
Replace 'Failed to fetch repositories: {str(e)}' with generic message.
logger.error still records full error for diagnostics.
data.get('avatar_url') can return None, type annotation must match.
Move modal logic into else branch so it only triggers when indexResponse.ok.
Failed indexing still shows warning toast but no progress modal.
If completeConnect throws (not just returns false), UI would get stuck in
'processing' state. Now catches exceptions and shows error message.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@backend/services/github.py`:
- Around line 140-158: The current get_all_repos function silently stops after
page > 10, truncating large accounts; change it to either (a) accept a
configurable max_pages parameter (default None for no cap) and check that
instead of hard-coded 10, or (b) drive pagination from the GitHub Link header
returned by get_repos (modify get_repos to return response headers or a
next-page flag). Also add a processLogger.warning (or logger.warn) inside
get_all_repos to emit a clear message when pagination is stopped due to the
configured cap (include page/max_pages and repo count) so callers know results
were truncated.
- Around line 86-138: get_repos currently does an unprotected httpx request and
JSON parse; wrap the client.get and response.json() in a try/except that catches
httpx.RequestError (and httpx.HTTPStatusError if you call raise_for_status) and
JSONDecodeError/ValueError, log the error (use self.logger.error(...) if
available) and then either return a controlled value (e.g., empty list) or
re-raise a new Exception with contextual details (include GITHUB_API_BASE,
params, response.status_code and response.text where available); ensure the
changes are applied around the call to client.get(...) and the loop that builds
GitHubRepo so failures don’t propagate as unhandled exceptions.
🧹 Nitpick comments (1)
frontend/src/components/dashboard/DashboardHome.tsx (1)

123-126: Consider moving setLoading(true) outside the loop.

Currently setLoading(true) is called on every iteration, but it only needs to be set once before the loop starts. Minor optimization for clarity.

♻️ Suggested refactor
   const handleGitHubImport = async (githubRepos: GitHubRepo[]) => {
     if (githubRepos.length === 0) return
     
+    setLoading(true)
+    
     // Import repos one at a time
     for (const repo of githubRepos) {
       try {
-        setLoading(true)
         const response = await fetch(`${API_URL}/repos`, {

Comment thread backend/services/github.py Outdated
Comment thread backend/services/github.py Outdated
…ination

get_repos:
- Wrap HTTP request and JSON parsing in try/except
- Return empty list on network errors instead of raising
- Use .get() for all fields to handle missing keys gracefully
- Log errors with context (page number, error details)

get_all_repos:
- Add configurable max_pages parameter (default 10, None for no limit)
- Log warning when pagination is stopped due to limit
- Include total repos fetched and stopped page in warning

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@backend/services/github.py`:
- Around line 152-187: The pagination warning in get_all_repos logs
stopped_at_page as the incremented page (the next page to fetch) rather than the
last successfully fetched page; change the logged value to the actual last
fetched page (e.g., use page - 1 or compute last_page before increment) when
building the logger.warning call so stopped_at_page reflects the last page
retrieved; update the logger.warning call in get_all_repos to reference that
corrected value (and keep max_pages and total_repos_fetched as-is).

Comment thread backend/services/github.py
Changed stopped_at_page to last_page_fetched (page - 1) since the
logged value was the next page to fetch, not the last one retrieved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@backend/services/github.py`:
- Around line 86-150: The get_repos function currently swallows
API/network/parse failures and returns [] (masking errors) and get_all_repos
uses "if not repos: break" which conflates an actual empty final page with an
error; update get_repos to let failures propagate by removing/rewriting the
except blocks so they re-raise (or raise a specific exception) instead of
returning [] (preserve logging but re-raise), and update get_all_repos's
pagination logic to stop based on page size (e.g., break when len(repos_page) <
per_page) or explicit API signals rather than "if not repos", so errors bubble
up to the route handler for proper 4xx/5xx handling; refer to get_repos and
get_all_repos to locate the changes.
🧹 Nitpick comments (1)
backend/services/github.py (1)

51-62: Add lightweight logging for token validation failures.
Returning False on non‑200 or exceptions is fine, but without logging it’s hard to debug OAuth issues.

🛠️ Suggested change
-                return response.status_code == 200
+                if response.status_code != 200:
+                    logger.warning(
+                        "GitHub token validation failed",
+                        status_code=response.status_code
+                    )
+                    return False
+                return True
-        except Exception:
-            return False
+        except Exception as e:
+            logger.error("GitHub token validation error", error=str(e))
+            return False

Comment thread backend/services/github.py Outdated
get_repos:
- Raise RuntimeError on API errors instead of returning []
- Re-raise network/parse exceptions after logging
- Allows route handler to return proper 4xx/5xx

get_all_repos:
- Remove 'if not repos: break' which conflated errors with empty pages
- Use 'len(repos) < per_page' to detect last page
- Errors now bubble up instead of silently truncating results

Fixes silent data loss bug where API failures returned HTTP 200 with partial results.
GitHubCallbackPage:
- Check sessionStorage before attempting exchange (prevents double-execution errors)
- Navigate with ?openGitHubImport=true param to auto-open modal
- Handle edge case where state already cleared (fast redirect)

DashboardHome:
- Read openGitHubImport param and auto-open GitHub import modal
- Clear param from URL after opening modal

GitHubRepoSelector:
- Add hasFetchedReposRef to prevent repeated fetch calls
- Only fetch repos once per modal open

Fixes: double callback errors, manual modal reopen after OAuth, repeated API calls
@vercel

vercel Bot commented Jan 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
opencodeintel Ready Ready Preview, Comment Jan 28, 2026 11:50pm

@DevanshuNEU DevanshuNEU merged commit d1db8c3 into OpenCodeIntel:main Jan 28, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant