feat: GitHub OAuth one-click repo import#230
Conversation
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
|
@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. |
📝 WalkthroughWalkthroughAdds GitHub OAuth-based repository import: environment updates, backend GitHub router and service, DB migration for Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
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 onuser_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 resettingloading. 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
reposfrom 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
reposfrom theuseCallbackdependency 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 exportsAPI_URLthat already includes the/api/v1prefix (evaluates tohttp://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'sAPI_URLalready 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 providedbuildApiUrlhelper:-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.
_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
There was a problem hiding this comment.
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 nestedownerfield to avoid potentialKeyError/TypeError.Direct access to
repo["owner"]["login"]andrepo["owner"]["avatar_url"]will raise an exception ifownerisNoneor 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_IDandGITHUB_CLIENT_SECRETare 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. IfGitHubService.get_reposis updated to raise a more specific exception type, this check would be easier to maintain.♻️ Suggested approach
Consider having
GitHubService.get_reposraise 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")
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.
There was a problem hiding this comment.
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 movingsetLoading(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`, {
…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
There was a problem hiding this comment.
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).
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.
There was a problem hiding this comment.
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.
ReturningFalseon 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
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary
Add one-click GitHub repository import feature with secure server-side OAuth token storage.
Features
reposcope for accessing private repositoriesScreenshots
Implementation Details
Backend
routes/github.py- OAuth endpoints (/connect,/callback,/status,/repos,/disconnect)services/github.py- GitHub API wrapper with pagination supportmigrations/003_github_connections.sql- Secure token storage table with RLSFrontend
GitHubRepoSelector.tsx- Modal for browsing/selecting reposGitHubCallbackPage.tsx- OAuth redirect handleruseGitHubRepos.ts- React hook for GitHub API integrationSecurity
Setup Required
.env:supabase/migrations/003_github_connections.sqlTesting
Related Issues
Closes #XXX (if applicable)
Note: For production, create separate OAuth app with production callback URL.
Summary by CodeRabbit
New Features
Improvements
Chores
✏️ Tip: You can customize this high-level summary in your review settings.