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/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 }); 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/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 (
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, + }, + }); +}