VIBN Frontend for Coolify deployment

This commit is contained in:
2026-02-15 19:25:52 -08:00
commit 40bf8428cd
398 changed files with 76513 additions and 0 deletions

View 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 }
);
}
}

View 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 }
);
}
}

View 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>
);
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}