Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions actions/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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: {
Expand Down
21 changes: 18 additions & 3 deletions src/app/api/github/auth/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
6 changes: 4 additions & 2 deletions src/app/api/github/callback/route.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -79,16 +80,17 @@ 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: {
githubId: githubUser.id.toString(),
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,
},
Expand Down
3 changes: 3 additions & 0 deletions src/app/api/github/status/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions src/components/GithubOAuthDeprecatedNotice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 10 additions & 6 deletions src/components/ImportGitRepository.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export default function ImportGitRepository({ onImport }: ImportGitRepositoryPro
const [searchTerm, setSearchTerm] = useState('');
const [searchLoading, setSearchLoading] = useState(false);
const [installationId, setInstallationId] = useState<string | null>(null);
const [isOAuthConnected, setIsOAuthConnected] = useState(false);
const [importing, setImporting] = useState<number | null>(null);
const [isGithubStatusLoading, setIsGithubStatusLoading] = useState(true);
const [userProjects, setUserProjects] = useState<UserProject[]>([]);
Expand Down Expand Up @@ -143,10 +144,10 @@ export default function ImportGitRepository({ onImport }: ImportGitRepositoryPro
}, []);

useEffect(() => {
if (installationId) {
if (installationId || isOAuthConnected) {
fetchRepos();
}
}, [installationId]);
}, [installationId, isOAuthConnected]);

// Debounced search effect
useEffect(() => {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -397,7 +401,7 @@ export default function ImportGitRepository({ onImport }: ImportGitRepositoryPro
);
}

if (!installationId) {
if (!installationId && !isOAuthConnected) {
return (
<div className="flex-1 flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div className="w-full h-full max-w-4xl">
Expand Down
37 changes: 37 additions & 0 deletions src/lib/ensureUser.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
}