mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
feat: new improvement for the GitHub API Authentication Fix (#1537)
* Add environment variables section to ConnectionsTab and fallback token to git-info * Add remaining code from original branch * Import Repo Fix * refactor the UI * add a rate limit counter * Update GithubConnection.tsx * Update NetlifyConnection.tsx * fix: ui style * Sync with upstream and preserve GitHub connection and DataTab fixes * fix disconnect buttons * revert commits * Update api.git-proxy.$.ts * Update api.git-proxy.$.ts
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { json, type LoaderFunction } from '@remix-run/cloudflare';
|
||||
import { json, type LoaderFunction, type LoaderFunctionArgs } from '@remix-run/cloudflare';
|
||||
|
||||
interface GitInfo {
|
||||
local: {
|
||||
@@ -20,6 +20,31 @@ interface GitInfo {
|
||||
};
|
||||
};
|
||||
isForked?: boolean;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
// Define context type
|
||||
interface AppContext {
|
||||
env?: {
|
||||
GITHUB_ACCESS_TOKEN?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GitHubRepo {
|
||||
name: string;
|
||||
full_name: string;
|
||||
html_url: string;
|
||||
description: string;
|
||||
stargazers_count: number;
|
||||
forks_count: number;
|
||||
language: string | null;
|
||||
languages_url: string;
|
||||
}
|
||||
|
||||
interface GitHubGist {
|
||||
id: string;
|
||||
html_url: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// These values will be replaced at build time
|
||||
@@ -31,7 +56,260 @@ declare const __GIT_EMAIL: string;
|
||||
declare const __GIT_REMOTE_URL: string;
|
||||
declare const __GIT_REPO_NAME: string;
|
||||
|
||||
export const loader: LoaderFunction = async () => {
|
||||
/*
|
||||
* Remove unused variable to fix linter error
|
||||
* declare const __GIT_REPO_URL: string;
|
||||
*/
|
||||
|
||||
export const loader: LoaderFunction = async ({ request, context }: LoaderFunctionArgs & { context: AppContext }) => {
|
||||
console.log('Git info API called with URL:', request.url);
|
||||
|
||||
// Handle CORS preflight requests
|
||||
if (request.method === 'OPTIONS') {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const action = searchParams.get('action');
|
||||
|
||||
console.log('Git info action:', action);
|
||||
|
||||
if (action === 'getUser' || action === 'getRepos' || action === 'getOrgs' || action === 'getActivity') {
|
||||
// Use server-side token instead of client-side token
|
||||
const serverGithubToken = process.env.GITHUB_ACCESS_TOKEN || context.env?.GITHUB_ACCESS_TOKEN;
|
||||
const cookieToken = request.headers
|
||||
.get('Cookie')
|
||||
?.split(';')
|
||||
.find((cookie) => cookie.trim().startsWith('githubToken='))
|
||||
?.split('=')[1];
|
||||
|
||||
// Also check for token in Authorization header
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
const headerToken = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
|
||||
|
||||
const token = serverGithubToken || headerToken || cookieToken;
|
||||
|
||||
console.log(
|
||||
'Using GitHub token from:',
|
||||
serverGithubToken ? 'server env' : headerToken ? 'auth header' : cookieToken ? 'cookie' : 'none',
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
console.error('No GitHub token available');
|
||||
return json(
|
||||
{ error: 'No GitHub token available' },
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (action === 'getUser') {
|
||||
const response = await fetch('https://api.github.com/user', {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('GitHub user API error:', response.status);
|
||||
throw new Error(`GitHub API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const userData = await response.json();
|
||||
|
||||
return json(
|
||||
{ user: userData },
|
||||
{
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (action === 'getRepos') {
|
||||
const reposResponse = await fetch('https://api.github.com/user/repos?per_page=100&sort=updated', {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!reposResponse.ok) {
|
||||
console.error('GitHub repos API error:', reposResponse.status);
|
||||
throw new Error(`GitHub API error: ${reposResponse.status}`);
|
||||
}
|
||||
|
||||
const repos = (await reposResponse.json()) as GitHubRepo[];
|
||||
|
||||
// Get user's gists
|
||||
const gistsResponse = await fetch('https://api.github.com/gists', {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const gists = gistsResponse.ok ? ((await gistsResponse.json()) as GitHubGist[]) : [];
|
||||
|
||||
// Calculate language statistics
|
||||
const languageStats: Record<string, number> = {};
|
||||
let totalStars = 0;
|
||||
let totalForks = 0;
|
||||
|
||||
for (const repo of repos) {
|
||||
totalStars += repo.stargazers_count || 0;
|
||||
totalForks += repo.forks_count || 0;
|
||||
|
||||
if (repo.language && repo.language !== 'null') {
|
||||
languageStats[repo.language] = (languageStats[repo.language] || 0) + 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Optionally fetch languages for each repo for more accurate stats
|
||||
* This is commented out to avoid rate limiting
|
||||
*
|
||||
* if (repo.languages_url) {
|
||||
* try {
|
||||
* const langResponse = await fetch(repo.languages_url, {
|
||||
* headers: {
|
||||
* Accept: 'application/vnd.github.v3+json',
|
||||
* Authorization: `Bearer ${token}`,
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* if (langResponse.ok) {
|
||||
* const languages = await langResponse.json();
|
||||
* Object.keys(languages).forEach(lang => {
|
||||
* languageStats[lang] = (languageStats[lang] || 0) + languages[lang];
|
||||
* });
|
||||
* }
|
||||
* } catch (error) {
|
||||
* console.error(`Error fetching languages for ${repo.name}:`, error);
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
}
|
||||
|
||||
return json(
|
||||
{
|
||||
repos,
|
||||
stats: {
|
||||
totalStars,
|
||||
totalForks,
|
||||
languages: languageStats,
|
||||
totalGists: gists.length,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (action === 'getOrgs') {
|
||||
const response = await fetch('https://api.github.com/user/orgs', {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('GitHub orgs API error:', response.status);
|
||||
throw new Error(`GitHub API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const orgs = await response.json();
|
||||
|
||||
return json(
|
||||
{ organizations: orgs },
|
||||
{
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (action === 'getActivity') {
|
||||
const username = request.headers
|
||||
.get('Cookie')
|
||||
?.split(';')
|
||||
.find((cookie) => cookie.trim().startsWith('githubUsername='))
|
||||
?.split('=')[1];
|
||||
|
||||
if (!username) {
|
||||
console.error('GitHub username not found in cookies');
|
||||
return json(
|
||||
{ error: 'GitHub username not found in cookies' },
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(`https://api.github.com/users/${username}/events?per_page=30`, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('GitHub activity API error:', response.status);
|
||||
throw new Error(`GitHub API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const events = await response.json();
|
||||
|
||||
return json(
|
||||
{ recentActivity: events },
|
||||
{
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('GitHub API error:', error);
|
||||
return json(
|
||||
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const gitInfo: GitInfo = {
|
||||
local: {
|
||||
commitHash: typeof __COMMIT_HASH !== 'undefined' ? __COMMIT_HASH : 'development',
|
||||
@@ -42,7 +320,13 @@ export const loader: LoaderFunction = async () => {
|
||||
remoteUrl: typeof __GIT_REMOTE_URL !== 'undefined' ? __GIT_REMOTE_URL : 'local',
|
||||
repoName: typeof __GIT_REPO_NAME !== 'undefined' ? __GIT_REPO_NAME : 'bolt.diy',
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return json(gitInfo);
|
||||
return json(gitInfo, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user