Skip to content

fix: complete GitHub OAuth flow for non-GitHub-App setups#65

Open
dmartinezh97 wants to merge 3 commits intolak7:mainfrom
dmartinezh97:fix/github-oauth-flow
Open

fix: complete GitHub OAuth flow for non-GitHub-App setups#65
dmartinezh97 wants to merge 3 commits intolak7:mainfrom
dmartinezh97:fix/github-oauth-flow

Conversation

@dmartinezh97
Copy link
Contributor

@dmartinezh97 dmartinezh97 commented Feb 26, 2026

Summary

Fixes the GitHub OAuth integration flow that was broken for setups not using GitHub App (e.g., local development with standard OAuth credentials). Three independent bugs prevented users from completing the "Connect GitHub Account" → "Import Repository" flow.

  • Auth route fallback/api/github/auth returned no response when GITHUB_APP_NEW_USERS=false. Now redirects to GitHub OAuth authorize endpoint as fallback.
  • User record creation — OAuth callback failed with db.user.update() when the user didn't exist in PostgreSQL (no Clerk webhook). Added ensureUserExists() that creates the user from Clerk API if missing.
  • Import component OAuth supportImportGitRepository only worked with GitHub App installationId. Added isOAuthConnected state so OAuth-connected users can also browse and import repos. Conditioned the "OAuth Deprecated" notice to only show when GITHUB_APP_FLOW_ENABLED=true.

Closes #62
Closes #63
Closes #64

Changes

Commit 1: Auth route OAuth fallback (#62)

  • src/app/api/github/auth/route.ts — Add redirect to github.com/login/oauth/authorize when GitHub App flow is not active

Commit 2: ensureUserExists (#63)

  • src/lib/ensureUser.ts — New utility that checks DB and creates user from Clerk API if missing
  • src/app/api/github/callback/route.ts — Call ensureUserExists() before db.user.update()
  • actions/user.ts — Call ensureUserExists() in getCurrentUserProfile

Commit 3: ImportGitRepository OAuth support (#64)

  • src/components/ImportGitRepository.tsx — Add isOAuthConnected state, update guards to allow OAuth users
  • src/app/api/github/status/route.ts — Add oauthDeprecated field based on GITHUB_APP_FLOW_ENABLED
  • src/components/GithubOAuthDeprecatedNotice.tsx — Use oauthDeprecated instead of isConnected

Test plan

  • With GITHUB_APP_NEW_USERS=false and valid GITHUB_OAUTH_CLIENT_ID/SECRET, verify /api/github/auth redirects to GitHub OAuth
  • Without Clerk webhook configured, verify OAuth callback creates the user and saves GitHub connection
  • After OAuth connection, verify ImportGitRepository shows the user's repositories
  • With GITHUB_APP_FLOW_ENABLED=true, verify the deprecation notice appears for OAuth-connected users
  • With GITHUB_APP_FLOW_ENABLED=false (or unset), verify the deprecation notice does NOT appear

Summary by CodeRabbit

  • New Features

    • GitHub OAuth authentication is now available as an alternative authentication method, providing flexibility beyond GitHub App installations.
    • Added OAuth deprecation status tracking to inform users about authentication method availability.
  • Improvements

    • Enhanced synchronization of user data between authentication systems to ensure consistent profile information.

When GITHUB_APP_NEW_USERS is false (common in local dev setups without
GitHub App), the /api/github/auth route ended without returning any
response. Add standard GitHub OAuth redirect as fallback so users can
still connect their GitHub account.
When the Clerk webhook is not configured (common in local development),
users are never created in PostgreSQL. This causes the OAuth callback to
fail with "Failed to save GitHub connection" because db.user.update()
expects the user to already exist.

Add ensureUserExists() helper that checks the DB and creates the user
from Clerk API data if missing. Call it in the OAuth callback and in
getCurrentUserProfile.
The ImportGitRepository component only showed repos when an
installationId (GitHub App) was present, even though /api/github/repos
already supports OAuth fallback. Users who connected via OAuth saw no
repos to import.

Add isOAuthConnected state so the component also fetches repos for
OAuth-connected users. Also condition the "OAuth is Deprecated" notice
to only show when GITHUB_APP_FLOW_ENABLED=true, since it makes no
sense when GitHub App is not configured.
@vercel
Copy link

vercel bot commented Feb 26, 2026

@dmartinezh97 is attempting to deploy a commit to the lak7's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

This PR addresses three bugs by implementing GitHub OAuth fallback when GitHub App flow is disabled, ensuring users exist in the database before operations (with Clerk fallback), and enabling the import repository component to support OAuth-connected users alongside GitHub App installations.

Changes

Cohort / File(s) Summary
User Existence Verification
src/lib/ensureUser.ts, actions/user.ts, src/app/api/github/callback/route.ts
New utility function ensureUserExists() that checks if a user exists in the database and creates one from Clerk data if absent. Integrated into user profile retrieval and GitHub OAuth callback to prevent database operation failures when Clerk webhooks are unconfigured.
GitHub OAuth Fallback
src/app/api/github/auth/route.ts
Added fallback GitHub OAuth redirect flow when GitHub App integration is disabled. Validates OAuth client ID configuration and redirects users to GitHub's authorize endpoint with proper state parameter encoding user ID.
GitHub Status Endpoint
src/app/api/github/status/route.ts
Added oauthDeprecated boolean field to response payload, conditionally set to true when GitHub App flow is enabled and user is GitHub-connected, addressing OAuth deprecation messaging logic.
Frontend OAuth Support
src/components/GithubOAuthDeprecatedNotice.tsx, src/components/ImportGitRepository.tsx
Updated components to use oauthDeprecated field for correct deprecation notice display. Enhanced ImportGitRepository with isOAuthConnected state to support OAuth-connected users alongside GitHub App installations for repository imports.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Client as Web Client
    participant AuthEndpoint as /api/github/auth
    participant GitHub as GitHub OAuth Server
    participant CallbackEndpoint as /api/github/callback
    participant Clerk as Clerk API
    participant DB as Database

    User->>Client: Click "Connect GitHub"
    Client->>AuthEndpoint: GET /api/github/auth
    
    rect rgba(100, 200, 255, 0.5)
        Note over AuthEndpoint: Check GITHUB_APP_FLOW_ENABLED
        alt GitHub App Enabled
            AuthEndpoint->>GitHub: Redirect to GitHub App Installation
        else OAuth Fallback
            AuthEndpoint->>GitHub: Redirect to OAuth Authorize<br/>(client_id + state with userId)
        end
    end
    
    AuthEndpoint-->>Client: 302 Redirect
    Client->>GitHub: Follow redirect
    GitHub-->>User: GitHub authorization prompt
    User->>GitHub: Authorize
    GitHub->>CallbackEndpoint: Redirect with code & state
    
    rect rgba(150, 200, 100, 0.5)
        CallbackEndpoint->>CallbackEndpoint: Extract userId from state
        CallbackEndpoint->>DB: Check if user exists
        alt User not in DB
            CallbackEndpoint->>Clerk: Fetch user info
            Clerk-->>CallbackEndpoint: User details
            CallbackEndpoint->>DB: Create user record
        end
        CallbackEndpoint->>DB: Update user with GitHub token
    end
    
    CallbackEndpoint-->>Client: Redirect to app
    Client->>User: Show confirmation
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 wiggles nose happily

A fallback path for OAuth's grace,
Users synced from Clerk's embrace,
Connections now both App and standard flow,
Repository imports put on show! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: complete GitHub OAuth flow for non-GitHub-App setups' directly summarizes the main changes, which implement fallback OAuth flow, user creation handling, and OAuth support in the import component.
Linked Issues check ✅ Passed All three linked issues are addressed: OAuth redirect fallback added to /api/github/auth [#62], ensureUserExists() implemented to create missing users [#63], and ImportGitRepository updated to support OAuth-connected users with oauthDeprecated field [#64].
Out of Scope Changes check ✅ Passed All code changes are directly related to addressing GitHub OAuth flow issues as specified in the linked issues; no extraneous modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/lib/ensureUser.ts (1)

10-36: Consider handling race conditions on user creation.

If two concurrent requests call ensureUserExists for the same userId simultaneously, both may pass the findUnique check and attempt to create the user, resulting in a unique constraint violation on db.user.create.

This is a minor edge case, but you could handle it gracefully by catching the Prisma unique constraint error (P2002) and returning the existing user:

♻️ Suggested improvement
 export async function ensureUserExists(userId: string) {
   const existing = await db.user.findUnique({ where: { id: userId } });
   if (existing) return existing;

   const clerk = await clerkClient();
   const clerkUser = await clerk.users.getUser(userId);

   const primaryEmail = clerkUser.emailAddresses.find(
     (e) => e.id === clerkUser.primaryEmailAddressId,
   );

   if (!primaryEmail) {
     throw new Error(`No primary email found for Clerk user ${userId}`);
   }

-  return db.user.create({
-    data: {
-      id: userId,
-      email: primaryEmail.emailAddress,
-      name:
-        clerkUser.firstName && clerkUser.lastName
-          ? `${clerkUser.firstName} ${clerkUser.lastName}`
-          : clerkUser.firstName || clerkUser.lastName || null,
-      username: clerkUser.username || null,
-      credits: signUpInitialSouls,
-    },
-  });
+  try {
+    return await db.user.create({
+      data: {
+        id: userId,
+        email: primaryEmail.emailAddress,
+        name:
+          clerkUser.firstName && clerkUser.lastName
+            ? `${clerkUser.firstName} ${clerkUser.lastName}`
+            : clerkUser.firstName || clerkUser.lastName || null,
+        username: clerkUser.username || null,
+        credits: signUpInitialSouls,
+      },
+    });
+  } catch (error: any) {
+    // Handle race condition: user was created by concurrent request
+    if (error?.code === 'P2002') {
+      const user = await db.user.findUnique({ where: { id: userId } });
+      if (user) return user;
+    }
+    throw error;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ensureUser.ts` around lines 10 - 36, ensureUserExists currently
races: two requests can both pass db.user.findUnique and call db.user.create
causing a Prisma P2002 unique constraint error; wrap the create call in a
try/catch and on catching Prisma's P2002 error (or checking error.code ===
'P2002') re-query db.user.findUnique and return the existing record instead of
throwing; keep existing logic for other errors and still use clerkClient() /
clerk.users.getUser to build create payload when needed.
src/app/api/github/auth/route.ts (1)

8-8: Remove stray empty statements.

There are several empty statements (; on their own lines) that appear to be debug artifacts or accidental code. These should be removed for code cleanliness.

♻️ Lines to remove
   try {
-    ;
     // Check if user is authenticated with Clerk
     const { userId } = await auth();
     
     if (!userId) {
       return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
     } 
-    ;

     // ...

       select: { isGithubConnected: true, githubAccessToken: true, isGithubAppConnected: true },
       });
-      ;
       const state = crypto.randomUUID();
  
-        ;
-        ;
         // Store the state mapping in database for webhook lookup

     // ...

-        ;
-        // ;
         
         const installUrl = new URL(`https://github.com/apps/${appSlug}/installations/new`);

Also applies to: 15-15, 26-26, 29-30, 42-43

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/github/auth/route.ts` at line 8, Remove the stray standalone
semicolon statements in src/app/api/github/auth/route.ts (they appear as lone
";" on their own lines—e.g., the instances called out at lines 8, 15, 26, 29-30,
42-43 in the review); simply delete those empty statements so the file contains
only meaningful expressions/exports (verify surrounding functions/exports like
the route handler(s) are unaffected) and run the linter to confirm no extra
semicolons remain.
src/components/GithubOAuthDeprecatedNotice.tsx (1)

22-30: Stale localStorage cache may cause incorrect notice display.

The current logic returns early if localStorage.getItem('githubOAuthConnected') has any value ('true' or 'false'), never refreshing from the API. This could cause issues:

  1. User disconnects GitHub OAuth → server returns oauthDeprecated: false, but localStorage still has 'true'
  2. Admin disables GITHUB_APP_FLOW_ENABLED → server returns oauthDeprecated: false, but localStorage still has 'true'

Consider either removing the localStorage cache, using a time-based expiry, or clearing it when relevant user actions occur (e.g., after disconnecting GitHub).

♻️ Suggested approach - add expiry or always fetch

One option is to always fetch and update the cache:

   const checkGithubOAuthStatus = async () => {
-    if(localStorage.getItem('githubOAuthConnected') === 'true'){
-      setGithubOAuthConnected(true);
-      setIsGithubStatusLoading(false);
-      return;
-    }else if(localStorage.getItem('githubOAuthConnected') === 'false'){
-      setGithubOAuthConnected(false);
-      setIsGithubStatusLoading(false);
-      return;
-    }
     setIsGithubStatusLoading(true);
     try {
       const response = await fetch('/api/github/status');

Or use session storage instead of local storage so it refreshes each session.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/GithubOAuthDeprecatedNotice.tsx` around lines 22 - 30, The
localStorage short-circuits in the GithubOAuthDeprecatedNotice component (checks
via localStorage.getItem('githubOAuthConnected') and returns early) so stale
values can prevent re-fetching the server status; remove the early return and
instead always call the API to determine oauthDeprecated then call
setGithubOAuthConnected(...) and setIsGithubStatusLoading(false), and update or
clear the local cache (localStorage.setItem(...) or removeItem) based on that
fresh response — alternatively replace localStorage usage with sessionStorage or
add a timestamp-based expiry when reading/writing the 'githubOAuthConnected' key
so the component (and functions like the GitHub disconnect handler) refresh the
cached value after relevant user actions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/api/github/auth/route.ts`:
- Line 8: Remove the stray standalone semicolon statements in
src/app/api/github/auth/route.ts (they appear as lone ";" on their own
lines—e.g., the instances called out at lines 8, 15, 26, 29-30, 42-43 in the
review); simply delete those empty statements so the file contains only
meaningful expressions/exports (verify surrounding functions/exports like the
route handler(s) are unaffected) and run the linter to confirm no extra
semicolons remain.

In `@src/components/GithubOAuthDeprecatedNotice.tsx`:
- Around line 22-30: The localStorage short-circuits in the
GithubOAuthDeprecatedNotice component (checks via
localStorage.getItem('githubOAuthConnected') and returns early) so stale values
can prevent re-fetching the server status; remove the early return and instead
always call the API to determine oauthDeprecated then call
setGithubOAuthConnected(...) and setIsGithubStatusLoading(false), and update or
clear the local cache (localStorage.setItem(...) or removeItem) based on that
fresh response — alternatively replace localStorage usage with sessionStorage or
add a timestamp-based expiry when reading/writing the 'githubOAuthConnected' key
so the component (and functions like the GitHub disconnect handler) refresh the
cached value after relevant user actions.

In `@src/lib/ensureUser.ts`:
- Around line 10-36: ensureUserExists currently races: two requests can both
pass db.user.findUnique and call db.user.create causing a Prisma P2002 unique
constraint error; wrap the create call in a try/catch and on catching Prisma's
P2002 error (or checking error.code === 'P2002') re-query db.user.findUnique and
return the existing record instead of throwing; keep existing logic for other
errors and still use clerkClient() / clerk.users.getUser to build create payload
when needed.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81a89fb and d1580ce.

📒 Files selected for processing (7)
  • actions/user.ts
  • src/app/api/github/auth/route.ts
  • src/app/api/github/callback/route.ts
  • src/app/api/github/status/route.ts
  • src/components/GithubOAuthDeprecatedNotice.tsx
  • src/components/ImportGitRepository.tsx
  • src/lib/ensureUser.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant