VIBN Frontend for Coolify deployment
This commit is contained in:
151
app/api/github/connect/route.ts
Normal file
151
app/api/github/connect/route.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin';
|
||||
import { FieldValue } from 'firebase-admin/firestore';
|
||||
|
||||
/**
|
||||
* Store GitHub connection for authenticated user
|
||||
* Encrypts and stores the access token securely
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const idToken = authHeader.split('Bearer ')[1];
|
||||
const adminAuth = getAdminAuth();
|
||||
const adminDb = getAdminDb();
|
||||
|
||||
let userId: string;
|
||||
try {
|
||||
const decodedToken = await adminAuth.verifyIdToken(idToken);
|
||||
userId = decodedToken.uid;
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { accessToken, githubUser } = await request.json();
|
||||
|
||||
if (!accessToken || !githubUser) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Encrypt the access token before storing
|
||||
// For now, we'll store it directly (should use crypto.subtle or a library)
|
||||
const encryptedToken = accessToken; // PLACEHOLDER
|
||||
|
||||
// Store GitHub connection
|
||||
const connectionRef = adminDb.collection('githubConnections').doc(userId);
|
||||
await connectionRef.set({
|
||||
userId,
|
||||
githubUserId: githubUser.id,
|
||||
githubUsername: githubUser.login,
|
||||
githubName: githubUser.name,
|
||||
githubEmail: githubUser.email,
|
||||
githubAvatarUrl: githubUser.avatar_url,
|
||||
accessToken: encryptedToken,
|
||||
connectedAt: FieldValue.serverTimestamp(),
|
||||
lastSyncedAt: null,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
githubUsername: githubUser.login,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[GitHub Connect] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to store GitHub connection' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GitHub connection status for authenticated user
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const idToken = authHeader.split('Bearer ')[1];
|
||||
const adminAuth = getAdminAuth();
|
||||
const adminDb = getAdminDb();
|
||||
|
||||
let userId: string;
|
||||
try {
|
||||
const decodedToken = await adminAuth.verifyIdToken(idToken);
|
||||
userId = decodedToken.uid;
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
|
||||
const connectionDoc = await adminDb
|
||||
.collection('githubConnections')
|
||||
.doc(userId)
|
||||
.get();
|
||||
|
||||
if (!connectionDoc.exists) {
|
||||
return NextResponse.json({ connected: false });
|
||||
}
|
||||
|
||||
const data = connectionDoc.data()!;
|
||||
|
||||
return NextResponse.json({
|
||||
connected: true,
|
||||
githubUsername: data.githubUsername,
|
||||
githubName: data.githubName,
|
||||
githubAvatarUrl: data.githubAvatarUrl,
|
||||
connectedAt: data.connectedAt,
|
||||
lastSyncedAt: data.lastSyncedAt,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[GitHub Connect] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch GitHub connection' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect GitHub account
|
||||
*/
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const idToken = authHeader.split('Bearer ')[1];
|
||||
const adminAuth = getAdminAuth();
|
||||
const adminDb = getAdminDb();
|
||||
|
||||
let userId: string;
|
||||
try {
|
||||
const decodedToken = await adminAuth.verifyIdToken(idToken);
|
||||
userId = decodedToken.uid;
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
|
||||
await adminDb.collection('githubConnections').doc(userId).delete();
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[GitHub Disconnect] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to disconnect GitHub' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
99
app/api/github/file-content/route.ts
Normal file
99
app/api/github/file-content/route.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin';
|
||||
|
||||
/**
|
||||
* Fetch file content from GitHub
|
||||
* GET /api/github/file-content?owner=X&repo=Y&path=Z
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const idToken = authHeader.split('Bearer ')[1];
|
||||
const adminAuth = getAdminAuth();
|
||||
const adminDb = getAdminDb();
|
||||
|
||||
let userId: string;
|
||||
try {
|
||||
const decodedToken = await adminAuth.verifyIdToken(idToken);
|
||||
userId = decodedToken.uid;
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const owner = url.searchParams.get('owner');
|
||||
const repo = url.searchParams.get('repo');
|
||||
const path = url.searchParams.get('path');
|
||||
const branch = url.searchParams.get('branch') || 'main';
|
||||
|
||||
if (!owner || !repo || !path) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing owner, repo, or path' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get GitHub connection
|
||||
const connectionDoc = await adminDb
|
||||
.collection('githubConnections')
|
||||
.doc(userId)
|
||||
.get();
|
||||
|
||||
if (!connectionDoc.exists) {
|
||||
return NextResponse.json(
|
||||
{ error: 'GitHub not connected' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const connection = connectionDoc.data()!;
|
||||
const accessToken = connection.accessToken; // TODO: Decrypt
|
||||
|
||||
// Fetch file content from GitHub API
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}?ref=${branch}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return NextResponse.json({ error: 'File not found' }, { status: 404 });
|
||||
}
|
||||
const error = await response.json();
|
||||
throw new Error(`GitHub API error: ${error.message || response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// GitHub returns base64-encoded content
|
||||
const content = Buffer.from(data.content, 'base64').toString('utf-8');
|
||||
|
||||
return NextResponse.json({
|
||||
path: data.path,
|
||||
name: data.name,
|
||||
size: data.size,
|
||||
sha: data.sha,
|
||||
content,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[GitHub File Content] Error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch file content',
|
||||
details: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
140
app/api/github/oauth/callback/page.tsx
Normal file
140
app/api/github/oauth/callback/page.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, Suspense } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { auth } from '@/lib/firebase/config';
|
||||
import { exchangeCodeForToken, getGitHubUser } from '@/lib/github/oauth';
|
||||
import { Loader2, CheckCircle2, XCircle } from 'lucide-react';
|
||||
|
||||
function GitHubCallbackContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function handleCallback() {
|
||||
try {
|
||||
const code = searchParams.get('code');
|
||||
const state = searchParams.get('state');
|
||||
const error = searchParams.get('error');
|
||||
|
||||
if (error) {
|
||||
throw new Error(`GitHub OAuth error: ${error}`);
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error('No authorization code received');
|
||||
}
|
||||
|
||||
// Verify state (CSRF protection)
|
||||
const storedState = sessionStorage.getItem('github_oauth_state');
|
||||
if (state !== storedState) {
|
||||
throw new Error('Invalid state parameter');
|
||||
}
|
||||
sessionStorage.removeItem('github_oauth_state');
|
||||
|
||||
// Exchange code for token
|
||||
const tokenData = await exchangeCodeForToken(code);
|
||||
|
||||
// Get GitHub user info
|
||||
const githubUser = await getGitHubUser(tokenData.access_token);
|
||||
|
||||
// Store connection in Firebase
|
||||
const user = auth.currentUser;
|
||||
if (!user) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const idToken = await user.getIdToken();
|
||||
const response = await fetch('/api/github/connect', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${idToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accessToken: tokenData.access_token,
|
||||
githubUser,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to store GitHub connection');
|
||||
}
|
||||
|
||||
setStatus('success');
|
||||
|
||||
// Redirect back to connections page after 2 seconds
|
||||
setTimeout(() => {
|
||||
const workspace = user.displayName || 'workspace';
|
||||
router.push(`/${workspace}/connections`);
|
||||
}, 2000);
|
||||
} catch (err: any) {
|
||||
console.error('[GitHub Callback] Error:', err);
|
||||
setError(err.message);
|
||||
setStatus('error');
|
||||
}
|
||||
}
|
||||
|
||||
handleCallback();
|
||||
}, [searchParams, router]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-6">
|
||||
<div className="w-full max-w-md space-y-6 text-center">
|
||||
{status === 'loading' && (
|
||||
<>
|
||||
<Loader2 className="mx-auto h-12 w-12 animate-spin text-primary" />
|
||||
<h1 className="text-2xl font-bold">Connecting to GitHub...</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Please wait while we complete the connection.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<>
|
||||
<CheckCircle2 className="mx-auto h-12 w-12 text-green-500" />
|
||||
<h1 className="text-2xl font-bold">Successfully Connected!</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Your GitHub account has been connected. Redirecting...
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<>
|
||||
<XCircle className="mx-auto h-12 w-12 text-red-500" />
|
||||
<h1 className="text-2xl font-bold">Connection Failed</h1>
|
||||
<p className="text-muted-foreground">{error}</p>
|
||||
<button
|
||||
onClick={() => router.push('/connections')}
|
||||
className="mt-4 rounded-lg bg-primary px-6 py-2 text-white hover:bg-primary/90"
|
||||
>
|
||||
Back to Connections
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GitHubCallbackPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-6">
|
||||
<div className="w-full max-w-md space-y-6 text-center">
|
||||
<Loader2 className="mx-auto h-12 w-12 animate-spin text-primary" />
|
||||
<h1 className="text-2xl font-bold">Loading...</h1>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GitHubCallbackContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
68
app/api/github/oauth/token/route.ts
Normal file
68
app/api/github/oauth/token/route.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
/**
|
||||
* Exchange GitHub OAuth code for access token
|
||||
* This must be done server-side to keep client secret secure
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { code } = await request.json();
|
||||
|
||||
if (!code) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Authorization code is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const clientId = process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID;
|
||||
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
return NextResponse.json(
|
||||
{ error: 'GitHub OAuth not configured' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Exchange code for token
|
||||
const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error('Failed to exchange code for token');
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
|
||||
if (tokenData.error) {
|
||||
return NextResponse.json(
|
||||
{ error: tokenData.error_description || tokenData.error },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
access_token: tokenData.access_token,
|
||||
token_type: tokenData.token_type,
|
||||
scope: tokenData.scope,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[GitHub OAuth] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to exchange code for token' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
149
app/api/github/repo-tree/route.ts
Normal file
149
app/api/github/repo-tree/route.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin';
|
||||
|
||||
/**
|
||||
* Fetch repository file tree from GitHub
|
||||
* GET /api/github/repo-tree?owner=X&repo=Y
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const idToken = authHeader.split('Bearer ')[1];
|
||||
const adminAuth = getAdminAuth();
|
||||
const adminDb = getAdminDb();
|
||||
|
||||
let userId: string;
|
||||
try {
|
||||
const decodedToken = await adminAuth.verifyIdToken(idToken);
|
||||
userId = decodedToken.uid;
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const owner = url.searchParams.get('owner');
|
||||
const repo = url.searchParams.get('repo');
|
||||
const branch = url.searchParams.get('branch') || 'main';
|
||||
|
||||
if (!owner || !repo) {
|
||||
return NextResponse.json({ error: 'Missing owner or repo' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get GitHub connection
|
||||
const connectionDoc = await adminDb
|
||||
.collection('githubConnections')
|
||||
.doc(userId)
|
||||
.get();
|
||||
|
||||
if (!connectionDoc.exists) {
|
||||
return NextResponse.json(
|
||||
{ error: 'GitHub not connected' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const connection = connectionDoc.data()!;
|
||||
const accessToken = connection.accessToken; // TODO: Decrypt
|
||||
|
||||
// Fetch repository tree from GitHub API (recursive)
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(`GitHub API error: ${error.message || response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Filter to only include files (not directories)
|
||||
// and exclude common non-code files
|
||||
const excludePatterns = [
|
||||
/node_modules\//,
|
||||
/\.git\//,
|
||||
/dist\//,
|
||||
/build\//,
|
||||
/\.next\//,
|
||||
/coverage\//,
|
||||
/\.cache\//,
|
||||
/\.env/,
|
||||
/package-lock\.json$/,
|
||||
/yarn\.lock$/,
|
||||
/pnpm-lock\.yaml$/,
|
||||
/\.png$/,
|
||||
/\.jpg$/,
|
||||
/\.jpeg$/,
|
||||
/\.gif$/,
|
||||
/\.svg$/,
|
||||
/\.ico$/,
|
||||
/\.woff$/,
|
||||
/\.woff2$/,
|
||||
/\.ttf$/,
|
||||
/\.eot$/,
|
||||
/\.min\.js$/,
|
||||
/\.min\.css$/,
|
||||
/\.map$/,
|
||||
];
|
||||
|
||||
// Include common code file extensions
|
||||
const includePatterns = [
|
||||
/\.(ts|tsx|js|jsx|py|java|go|rs|cpp|c|h|cs|rb|php|swift|kt)$/,
|
||||
/\.(json|yaml|yml|toml|xml)$/,
|
||||
/\.(md|txt)$/,
|
||||
/\.(sql|graphql|proto)$/,
|
||||
/\.(css|scss|sass|less)$/,
|
||||
/\.(html|htm)$/,
|
||||
/Dockerfile$/,
|
||||
/Makefile$/,
|
||||
/README$/,
|
||||
];
|
||||
|
||||
const files = data.tree
|
||||
.filter((item: any) => item.type === 'blob')
|
||||
.filter((item: any) => {
|
||||
// Exclude patterns
|
||||
if (excludePatterns.some(pattern => pattern.test(item.path))) {
|
||||
return false;
|
||||
}
|
||||
// Include patterns
|
||||
return includePatterns.some(pattern => pattern.test(item.path));
|
||||
})
|
||||
.map((item: any) => ({
|
||||
path: item.path,
|
||||
sha: item.sha,
|
||||
size: item.size,
|
||||
url: item.url,
|
||||
}));
|
||||
|
||||
console.log(`[GitHub Tree] Found ${files.length} code files in ${owner}/${repo}`);
|
||||
|
||||
return NextResponse.json({
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
totalFiles: files.length,
|
||||
files,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[GitHub Tree] Error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch repository tree',
|
||||
details: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
79
app/api/github/repos/route.ts
Normal file
79
app/api/github/repos/route.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin';
|
||||
|
||||
/**
|
||||
* Fetch user's GitHub repositories
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const idToken = authHeader.split('Bearer ')[1];
|
||||
const adminAuth = getAdminAuth();
|
||||
const adminDb = getAdminDb();
|
||||
|
||||
let userId: string;
|
||||
try {
|
||||
const decodedToken = await adminAuth.verifyIdToken(idToken);
|
||||
userId = decodedToken.uid;
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get GitHub connection
|
||||
const connectionDoc = await adminDb
|
||||
.collection('githubConnections')
|
||||
.doc(userId)
|
||||
.get();
|
||||
|
||||
if (!connectionDoc.exists) {
|
||||
return NextResponse.json(
|
||||
{ error: 'GitHub not connected' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const connection = connectionDoc.data()!;
|
||||
const accessToken = connection.accessToken; // TODO: Decrypt
|
||||
|
||||
// Fetch repos from GitHub API
|
||||
const response = await fetch('https://api.github.com/user/repos?sort=updated&per_page=100', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch repositories from GitHub');
|
||||
}
|
||||
|
||||
const repos = await response.json();
|
||||
|
||||
// Return simplified repo data
|
||||
return NextResponse.json(
|
||||
repos.map((repo: any) => ({
|
||||
id: repo.id,
|
||||
name: repo.name,
|
||||
full_name: repo.full_name,
|
||||
description: repo.description,
|
||||
html_url: repo.html_url,
|
||||
language: repo.language,
|
||||
default_branch: repo.default_branch,
|
||||
private: repo.private,
|
||||
topics: repo.topics || [],
|
||||
updated_at: repo.updated_at,
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[GitHub Repos] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch repositories' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user