From 4b9677343df76983b172a61d142e666fbc1a9389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20mart=C3=ADnez?= Date: Thu, 26 Feb 2026 04:26:06 +0100 Subject: [PATCH 1/3] fix: add OAuth fallback when GitHub App flow is disabled 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. --- src/app/api/github/auth/route.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/app/api/github/auth/route.ts b/src/app/api/github/auth/route.ts index 0b2acf8..b18f099 100644 --- a/src/app/api/github/auth/route.ts +++ b/src/app/api/github/auth/route.ts @@ -45,11 +45,26 @@ export async function GET(request: NextRequest) { const installUrl = new URL(`https://github.com/apps/${appSlug}/installations/new`); installUrl.searchParams.set('state', state); // Just the state, not userId installUrl.searchParams.set('setup_action', 'install'); - return NextResponse.redirect(installUrl.toString()); + return NextResponse.redirect(installUrl.toString()); } - - + // Fallback: Standard GitHub OAuth flow + const clientId = process.env.GITHUB_OAUTH_CLIENT_ID; + if (!clientId) { + return NextResponse.json({ error: 'GitHub OAuth not configured' }, { status: 500 }); + } + + const state = `${crypto.randomUUID()}:${userId}`; + const redirectUri = `${process.env.NEXT_PUBLIC_BASE_URL}/api/github/callback`; + + const githubAuthUrl = new URL('https://github.com/login/oauth/authorize'); + githubAuthUrl.searchParams.set('client_id', clientId); + githubAuthUrl.searchParams.set('redirect_uri', redirectUri); + githubAuthUrl.searchParams.set('scope', 'repo user:email'); + githubAuthUrl.searchParams.set('state', state); + + return NextResponse.redirect(githubAuthUrl.toString()); + } catch (error) { console.error('Error initiating GitHub OAuth:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); From a78579b4bed41a8b449a27c82ecbe21bc8446788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20mart=C3=ADnez?= Date: Thu, 26 Feb 2026 04:26:14 +0100 Subject: [PATCH 2/3] fix: create user record from Clerk if missing during OAuth callback 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. --- actions/user.ts | 2 ++ src/app/api/github/callback/route.ts | 6 +++-- src/lib/ensureUser.ts | 37 ++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/lib/ensureUser.ts diff --git a/actions/user.ts b/actions/user.ts index 82e4f58..ae4aca5 100644 --- a/actions/user.ts +++ b/actions/user.ts @@ -2,6 +2,7 @@ import { db } from "@/lib/db"; import { z } from "zod"; import { auth } from "@clerk/nextjs/server"; +import { ensureUserExists } from "@/lib/ensureUser"; export async function joinWaitlist(email: string) { try { @@ -224,6 +225,7 @@ export async function getCurrentUserProfile() { if (!userId) { return { error: 'Not authenticated' } as const; } + await ensureUserExists(userId); const user = await db.user.findUnique({ where: { id: userId }, select: { diff --git a/src/app/api/github/callback/route.ts b/src/app/api/github/callback/route.ts index 25561be..295f8e8 100644 --- a/src/app/api/github/callback/route.ts +++ b/src/app/api/github/callback/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; +import { ensureUserExists } from '@/lib/ensureUser'; export async function GET(request: NextRequest) { try { @@ -79,8 +80,9 @@ export async function GET(request: NextRequest) { } } - // Update user in database with GitHub information + // Ensure user exists in DB (creates from Clerk if missing), then update GitHub info try { + await ensureUserExists(userId); await db.user.update({ where: { id: userId }, data: { @@ -88,7 +90,7 @@ export async function GET(request: NextRequest) { githubUsername: githubUser.login, githubEmail: primaryEmail, githubAvatarUrl: githubUser.avatar_url, - githubAccessToken: accessToken, // Note: In production, you should encrypt this + githubAccessToken: accessToken, githubConnectedAt: new Date(), isGithubConnected: true, }, diff --git a/src/lib/ensureUser.ts b/src/lib/ensureUser.ts new file mode 100644 index 0000000..447d3fa --- /dev/null +++ b/src/lib/ensureUser.ts @@ -0,0 +1,37 @@ +import { clerkClient } from '@clerk/nextjs/server'; +import { db } from '@/lib/db'; +import { signUpInitialSouls } from '../../Limits'; + +/** + * Ensures that a Clerk user exists in the local database. + * If the user doesn't exist, fetches their info from Clerk and creates the record. + * Returns the user record. + */ +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, + }, + }); +} From d1580ceaf0dd96b1121a4bbd22513e25da6b5dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20mart=C3=ADnez?= Date: Thu, 26 Feb 2026 04:26:24 +0100 Subject: [PATCH 3/3] fix: allow ImportGitRepository to work with OAuth, not just GitHub App 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. --- src/app/api/github/status/route.ts | 3 +++ src/components/GithubOAuthDeprecatedNotice.tsx | 5 +++-- src/components/ImportGitRepository.tsx | 16 ++++++++++------ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/app/api/github/status/route.ts b/src/app/api/github/status/route.ts index 1bceef2..3265261 100644 --- a/src/app/api/github/status/route.ts +++ b/src/app/api/github/status/route.ts @@ -24,8 +24,11 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'User not found' }, { status: 404 }); } + const appFlowEnabled = process.env.GITHUB_APP_FLOW_ENABLED === 'true'; + return NextResponse.json({ isConnected: user.isGithubConnected, + oauthDeprecated: appFlowEnabled && user.isGithubConnected, githubUsername: user.githubUsername, githubAvatarUrl: user.githubAvatarUrl, connectedAt: user.githubConnectedAt, diff --git a/src/components/GithubOAuthDeprecatedNotice.tsx b/src/components/GithubOAuthDeprecatedNotice.tsx index a454683..480554a 100644 --- a/src/components/GithubOAuthDeprecatedNotice.tsx +++ b/src/components/GithubOAuthDeprecatedNotice.tsx @@ -32,8 +32,9 @@ export default function GithubOAuthDeprecatedNotice() { try { const response = await fetch('/api/github/status'); const data = await response.json(); - setGithubOAuthConnected(data.isConnected); - localStorage.setItem('githubOAuthConnected', data.isConnected.toString()); + const deprecated = data.oauthDeprecated ?? false; + setGithubOAuthConnected(deprecated); + localStorage.setItem('githubOAuthConnected', deprecated.toString()); setIsGithubStatusLoading(false); } catch (error) { console.error('Error checking GitHub status:', error); diff --git a/src/components/ImportGitRepository.tsx b/src/components/ImportGitRepository.tsx index 654855d..0c7fc85 100644 --- a/src/components/ImportGitRepository.tsx +++ b/src/components/ImportGitRepository.tsx @@ -103,6 +103,7 @@ export default function ImportGitRepository({ onImport }: ImportGitRepositoryPro const [searchTerm, setSearchTerm] = useState(''); const [searchLoading, setSearchLoading] = useState(false); const [installationId, setInstallationId] = useState(null); + const [isOAuthConnected, setIsOAuthConnected] = useState(false); const [importing, setImporting] = useState(null); const [isGithubStatusLoading, setIsGithubStatusLoading] = useState(true); const [userProjects, setUserProjects] = useState([]); @@ -143,10 +144,10 @@ export default function ImportGitRepository({ onImport }: ImportGitRepositoryPro }, []); useEffect(() => { - if (installationId) { + if (installationId || isOAuthConnected) { fetchRepos(); } - }, [installationId]); + }, [installationId, isOAuthConnected]); // Debounced search effect useEffect(() => { @@ -172,8 +173,11 @@ export default function ImportGitRepository({ onImport }: ImportGitRepositoryPro if ((result as any).projects) { setUserProjects((result as any).projects); } - setUserProfile((result as any).user ?? null); - + const usr = (result as any).user ?? null; + setUserProfile(usr); + if (usr?.isGithubConnected) { + setIsOAuthConnected(true); + } } else { setInstallationId(null); } @@ -222,7 +226,7 @@ export default function ImportGitRepository({ onImport }: ImportGitRepositoryPro const fetchRepos = async (search?: string) => { // Only fetch when the user is connected to GitHub or we have an installationId - if (!installationId) { + if (!installationId && !isOAuthConnected) { return; } try { @@ -397,7 +401,7 @@ export default function ImportGitRepository({ onImport }: ImportGitRepositoryPro ); } - if (!installationId) { + if (!installationId && !isOAuthConnected) { return (