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:
Stijnus
2025-03-29 21:12:47 +01:00
committed by GitHub
parent 1c561a0615
commit 0487ed1438
23 changed files with 3321 additions and 778 deletions

View File

@@ -117,6 +117,7 @@ async function handleProxyRequest(request: Request, path: string | undefined) {
// Add body for non-GET/HEAD requests
if (!['GET', 'HEAD'].includes(request.method)) {
fetchOptions.body = request.body;
fetchOptions.duplex = 'half';
/*
* Note: duplex property is removed to ensure TypeScript compatibility

View File

@@ -0,0 +1,142 @@
import { json, type LoaderFunction, type LoaderFunctionArgs } from '@remix-run/cloudflare';
/**
* Diagnostic API for troubleshooting connection issues
*/
interface AppContext {
env?: {
GITHUB_ACCESS_TOKEN?: string;
NETLIFY_TOKEN?: string;
};
}
export const loader: LoaderFunction = async ({ request, context }: LoaderFunctionArgs & { context: AppContext }) => {
// Get environment variables
const envVars = {
hasGithubToken: Boolean(process.env.GITHUB_ACCESS_TOKEN || context.env?.GITHUB_ACCESS_TOKEN),
hasNetlifyToken: Boolean(process.env.NETLIFY_TOKEN || context.env?.NETLIFY_TOKEN),
nodeEnv: process.env.NODE_ENV,
};
// Check cookies
const cookieHeader = request.headers.get('Cookie') || '';
const cookies = cookieHeader.split(';').reduce(
(acc, cookie) => {
const [key, value] = cookie.trim().split('=');
if (key) {
acc[key] = value;
}
return acc;
},
{} as Record<string, string>,
);
const hasGithubTokenCookie = Boolean(cookies.githubToken);
const hasGithubUsernameCookie = Boolean(cookies.githubUsername);
const hasNetlifyCookie = Boolean(cookies.netlifyToken);
// Get local storage status (this can only be checked client-side)
const localStorageStatus = {
explanation: 'Local storage can only be checked on the client side. Use browser devtools to check.',
githubKeysToCheck: ['github_connection'],
netlifyKeysToCheck: ['netlify_connection'],
};
// Check if CORS might be an issue
const corsStatus = {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
};
// Check if API endpoints are reachable
const apiEndpoints = {
githubUser: '/api/system/git-info?action=getUser',
githubRepos: '/api/system/git-info?action=getRepos',
githubOrgs: '/api/system/git-info?action=getOrgs',
githubActivity: '/api/system/git-info?action=getActivity',
gitInfo: '/api/system/git-info',
};
// Test GitHub API connectivity
let githubApiStatus;
try {
const githubResponse = await fetch('https://api.github.com/zen', {
method: 'GET',
headers: {
Accept: 'application/vnd.github.v3+json',
},
});
githubApiStatus = {
isReachable: githubResponse.ok,
status: githubResponse.status,
statusText: githubResponse.statusText,
};
} catch (error) {
githubApiStatus = {
isReachable: false,
error: error instanceof Error ? error.message : String(error),
};
}
// Test Netlify API connectivity
let netlifyApiStatus;
try {
const netlifyResponse = await fetch('https://api.netlify.com/api/v1/', {
method: 'GET',
});
netlifyApiStatus = {
isReachable: netlifyResponse.ok,
status: netlifyResponse.status,
statusText: netlifyResponse.statusText,
};
} catch (error) {
netlifyApiStatus = {
isReachable: false,
error: error instanceof Error ? error.message : String(error),
};
}
// Provide technical details about the environment
const technicalDetails = {
serverTimestamp: new Date().toISOString(),
userAgent: request.headers.get('User-Agent'),
referrer: request.headers.get('Referer'),
host: request.headers.get('Host'),
method: request.method,
url: request.url,
};
// Return diagnostics
return json(
{
status: 'success',
environment: envVars,
cookies: {
hasGithubTokenCookie,
hasGithubUsernameCookie,
hasNetlifyCookie,
},
localStorage: localStorageStatus,
apiEndpoints,
externalApis: {
github: githubApiStatus,
netlify: netlifyApiStatus,
},
corsStatus,
technicalDetails,
},
{
headers: corsStatus.headers,
},
);
};

View File

@@ -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',
},
});
};