done the redirection issue
This commit is contained in:
parent
ab8b8942e8
commit
5e39839d42
@ -256,7 +256,7 @@ services:
|
||||
# - JWT_ACCESS_SECRET=access-secret-key-2024-tech4biz-secure_pipeline_2024
|
||||
# - JWT_REFRESH_SECRET=refresh-secret-key-2024-tech4biz-secure_pipeline_2024
|
||||
# Service URLs
|
||||
- USER_AUTH_URL=http://user-auth:8011
|
||||
- USER_AUTH_URL=http://pipeline_user_auth:8011
|
||||
- TEMPLATE_MANAGER_URL=http://template-manager:8009
|
||||
- GIT_INTEGRATION_URL=http://git-integration:8012
|
||||
- REQUIREMENT_PROCESSOR_URL=http://requirement-processor:8001
|
||||
|
||||
@ -1814,7 +1814,10 @@ app.use('/api/vcs',
|
||||
},
|
||||
timeout: 45000,
|
||||
validateStatus: () => true,
|
||||
maxRedirects: 5 // Allow following redirects for OAuth flows
|
||||
maxRedirects: 0, // Don't follow redirects - handle them manually
|
||||
decompress: true,
|
||||
httpAgent: new http.Agent({ keepAlive: true, maxSockets: 100 }),
|
||||
httpsAgent: new https.Agent({ keepAlive: true, maxSockets: 100 })
|
||||
};
|
||||
|
||||
// Always include request body for POST/PUT/PATCH requests
|
||||
|
||||
@ -64,7 +64,13 @@ router.get('/auth/github/callback', async (req, res) => {
|
||||
(req.session && req.session.user && (req.session.user.id || req.session.user.userId)) ||
|
||||
(req.user && (req.user.id || req.user.userId));
|
||||
if (!user_id && typeof state === 'string' && state.includes('|uid=')) {
|
||||
try { user_id = state.split('|uid=')[1]; } catch {}
|
||||
try {
|
||||
const stateParts = state.split('|');
|
||||
const uidPart = stateParts.find(part => part.startsWith('uid='));
|
||||
if (uidPart) {
|
||||
user_id = uidPart.split('=')[1];
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!user_id) {
|
||||
@ -113,9 +119,18 @@ router.get('/auth/github/callback', async (req, res) => {
|
||||
}
|
||||
|
||||
// Redirect back to frontend IMMEDIATELY (before heavy cloning operation)
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'https://dashboard.codenuk.com';
|
||||
const frontendUrl = 'http://localhost:3001';
|
||||
try {
|
||||
const redirectUrl = `${frontendUrl}?github_connected=1&user=${encodeURIComponent(githubUser.login)}&processing=1`;
|
||||
// Check if this is a private repository OAuth flow
|
||||
const isPrivateRepo = typeof state === 'string' && state.includes('private_repo=true');
|
||||
let redirectUrl = `${frontendUrl}/project-builder?oauth_success=true&provider=github&user_id=${encodeURIComponent(user_id)}`;
|
||||
|
||||
if (isPrivateRepo && repoContext) {
|
||||
// Add private repo sync parameters
|
||||
redirectUrl += `&sync_private_repo=true&repository_url=${encodeURIComponent(repoContext.repoUrl)}&branch_name=${encodeURIComponent(repoContext.branchName || 'main')}&sync_status=authenticating`;
|
||||
console.log('[GitHub OAuth] Private repository OAuth completed, starting sync:', repoContext.repoUrl);
|
||||
}
|
||||
|
||||
console.log('[GitHub OAuth] Redirecting to:', redirectUrl);
|
||||
|
||||
// Send redirect response immediately
|
||||
|
||||
@ -40,6 +40,109 @@ function extractEventType(providerKey, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
// Background sync method for private repositories
|
||||
async function startPrivateRepoSync(providerKey, repoUrl, branchName, userId) {
|
||||
try {
|
||||
console.log(`🔄 [Private Repo Sync] Starting background sync for ${providerKey}: ${repoUrl}`);
|
||||
|
||||
const provider = getProvider({ params: { provider: providerKey } });
|
||||
const { owner, repo, branch } = provider.parseRepoUrl(repoUrl);
|
||||
|
||||
// Update sync status to syncing
|
||||
const database = require('../config/database');
|
||||
await database.query(`
|
||||
UPDATE all_repositories
|
||||
SET sync_status = 'syncing', updated_at = NOW()
|
||||
WHERE repository_url = $1 AND user_id = $2
|
||||
`, [repoUrl, userId]);
|
||||
|
||||
// Fetch repository metadata with authentication
|
||||
const repositoryData = await provider.fetchRepositoryMetadata(owner, repo);
|
||||
let actualBranch = branch || branchName || repositoryData.default_branch || 'main';
|
||||
|
||||
// Analyze codebase
|
||||
const codebaseAnalysis = await provider.analyzeCodebase(owner, repo, actualBranch, false);
|
||||
|
||||
// Prepare insert values
|
||||
const insertValues = [
|
||||
repoUrl,
|
||||
repo,
|
||||
owner,
|
||||
actualBranch,
|
||||
repositoryData.visibility !== 'private',
|
||||
JSON.stringify(repositoryData),
|
||||
JSON.stringify(codebaseAnalysis),
|
||||
'synced',
|
||||
repositoryData.visibility === 'private',
|
||||
userId
|
||||
];
|
||||
|
||||
// Check if repository already exists
|
||||
const existingRepo = await database.query(
|
||||
'SELECT id FROM all_repositories WHERE repository_url = $1 AND user_id = $2',
|
||||
[repoUrl, userId]
|
||||
);
|
||||
|
||||
let repositoryRecord;
|
||||
if (existingRepo.rows.length > 0) {
|
||||
// Update existing repository
|
||||
const updateQuery = `
|
||||
UPDATE all_repositories SET
|
||||
metadata = $1,
|
||||
codebase_analysis = $2,
|
||||
sync_status = $3,
|
||||
updated_at = NOW()
|
||||
WHERE id = $4
|
||||
RETURNING *
|
||||
`;
|
||||
const updateResult = await database.query(updateQuery, [
|
||||
JSON.stringify(repositoryData),
|
||||
JSON.stringify(codebaseAnalysis),
|
||||
'synced',
|
||||
existingRepo.rows[0].id
|
||||
]);
|
||||
repositoryRecord = updateResult.rows[0];
|
||||
} else {
|
||||
// Insert new repository
|
||||
const insertQuery = `
|
||||
INSERT INTO all_repositories (
|
||||
repository_url, repository_name, owner_name,
|
||||
branch_name, is_public, metadata, codebase_analysis, sync_status,
|
||||
requires_auth, user_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING *
|
||||
`;
|
||||
const insertResult = await database.query(insertQuery, insertValues);
|
||||
repositoryRecord = insertResult.rows[0];
|
||||
}
|
||||
|
||||
// Sync repository files (clone/download)
|
||||
const downloadResult = await provider.syncRepositoryWithFallback(owner, repo, actualBranch, repositoryRecord.id, repositoryData.visibility !== 'private');
|
||||
|
||||
// Update final sync status
|
||||
const finalSyncStatus = downloadResult.success ? 'synced' : 'error';
|
||||
await database.query('UPDATE all_repositories SET sync_status = $1, updated_at = NOW() WHERE id = $2', [finalSyncStatus, repositoryRecord.id]);
|
||||
|
||||
// Set up webhook if possible
|
||||
const publicBaseUrl = process.env.PUBLIC_BASE_URL || null;
|
||||
const callbackUrl = publicBaseUrl ? `${publicBaseUrl}/api/vcs/${providerKey}/webhook` : null;
|
||||
try { await provider.ensureRepositoryWebhook(owner, repo, callbackUrl); } catch (_) {}
|
||||
|
||||
console.log(`✅ [Private Repo Sync] Completed sync for ${repoUrl} - Status: ${finalSyncStatus}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ [Private Repo Sync] Failed for ${repoUrl}:`, error);
|
||||
|
||||
// Update status to error
|
||||
const database = require('../config/database');
|
||||
await database.query(`
|
||||
UPDATE all_repositories
|
||||
SET sync_status = 'error', updated_at = NOW()
|
||||
WHERE repository_url = $1 AND user_id = $2
|
||||
`, [repoUrl, userId]);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach repository (provider-agnostic)
|
||||
router.post('/:provider/attach-repository', async (req, res) => {
|
||||
try {
|
||||
@ -53,12 +156,50 @@ router.post('/:provider/attach-repository', async (req, res) => {
|
||||
}
|
||||
|
||||
const { owner, repo, branch } = provider.parseRepoUrl(repository_url);
|
||||
const accessCheck = await provider.checkRepositoryAccess(owner, repo, userId);
|
||||
|
||||
if (!accessCheck.hasAccess) {
|
||||
if (accessCheck.requiresAuth) {
|
||||
// Check if we have OAuth token for this provider
|
||||
// Enhanced flow: Detect private repos and redirect to OAuth immediately
|
||||
const providerKey = (req.params.provider || '').toLowerCase();
|
||||
console.log(`[VCS Attach] Processing ${providerKey} repository: ${repository_url}`);
|
||||
console.log(`[VCS Attach] Parsed - owner: ${owner}, repo: ${repo}`);
|
||||
|
||||
// First, try to determine if this is a private repository without authentication
|
||||
let isPrivateRepo = false;
|
||||
let visibilityError = null;
|
||||
|
||||
try {
|
||||
console.log(`[VCS Attach] Fetching metadata without auth for ${providerKey}/${owner}/${repo}`);
|
||||
console.log(`[VCS Attach] Provider method:`, typeof provider.fetchRepositoryMetadata);
|
||||
console.log(`[VCS Attach] Calling with skipAuth=true`);
|
||||
// Try to fetch repo metadata without authentication to check visibility
|
||||
const repositoryData = await provider.fetchRepositoryMetadata(owner, repo, true);
|
||||
console.log(`[VCS Attach] Metadata response:`, repositoryData);
|
||||
isPrivateRepo = repositoryData.visibility === 'private';
|
||||
console.log(`[VCS Attach] Repository visibility: ${repositoryData.visibility}, isPrivate: ${isPrivateRepo}`);
|
||||
} catch (error) {
|
||||
console.log(`[VCS Attach] Error fetching metadata:`, error.message);
|
||||
// If we can't determine visibility, check error type
|
||||
if (error.message && (
|
||||
error.message.includes('Authentication required') ||
|
||||
error.message.includes('404') ||
|
||||
error.message.includes('Not Found') ||
|
||||
error.message.includes('Repository not found') ||
|
||||
error.message.includes('No GitHub token found') ||
|
||||
error.message.includes('Please authenticate') ||
|
||||
error.message.includes('authentication')
|
||||
)) {
|
||||
console.log(`[VCS Attach] Detected as private repository requiring auth`);
|
||||
// Could be a private repository (404/Not Found/Repository not found) or auth required
|
||||
// In either case, we need authentication to proceed
|
||||
isPrivateRepo = true;
|
||||
visibilityError = 'Authentication required to access repository';
|
||||
} else {
|
||||
console.log(`[VCS Attach] Non-auth error, returning 404:`, error.message);
|
||||
return res.status(404).json({ success: false, message: error.message || 'Repository not accessible' });
|
||||
}
|
||||
}
|
||||
|
||||
// If private repo detected, check authentication
|
||||
if (isPrivateRepo) {
|
||||
const oauthService = getOAuthService(providerKey);
|
||||
if (oauthService) {
|
||||
let tokenRecord = null;
|
||||
@ -70,16 +211,31 @@ router.post('/:provider/attach-repository', async (req, res) => {
|
||||
}
|
||||
|
||||
if (!tokenRecord) {
|
||||
return res.status(401).json({
|
||||
// Redirect to OAuth for private repo authentication
|
||||
console.log(`🔒 [VCS Attach] Private ${providerKey} repository detected, redirecting to OAuth: ${repository_url}`);
|
||||
|
||||
// Generate OAuth URL with repository context in state
|
||||
const stateBase = Math.random().toString(36).substring(7);
|
||||
const state = `${stateBase}|uid=${userId}|repo=${encodeURIComponent(repository_url)}|branch=${encodeURIComponent(branch_name || 'main')}|private_repo=true`;
|
||||
|
||||
const authUrl = oauthService.getAuthUrl(state, userId);
|
||||
|
||||
return res.json({
|
||||
success: false,
|
||||
message: `${providerKey.charAt(0).toUpperCase() + providerKey.slice(1)} authentication required for this repository`,
|
||||
message: `${providerKey.charAt(0).toUpperCase() + providerKey.slice(1)} authentication required for private repository`,
|
||||
requires_auth: true,
|
||||
auth_url: `/api/vcs/${providerKey}/auth/start?user_id=${encodeURIComponent(userId)}`
|
||||
is_private_repo: true,
|
||||
auth_url: authUrl,
|
||||
state: state
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For public repos or authenticated private repos, proceed with normal flow
|
||||
const accessCheck = await provider.checkRepositoryAccess(owner, repo, userId);
|
||||
|
||||
if (!accessCheck.hasAccess) {
|
||||
return res.status(404).json({ success: false, message: accessCheck.error || 'Repository not accessible' });
|
||||
}
|
||||
|
||||
@ -484,11 +640,14 @@ router.get('/:provider/repositories', async (req, res) => {
|
||||
}
|
||||
|
||||
if (!tokenRecord) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: `${providerKey.charAt(0).toUpperCase() + providerKey.slice(1)} authentication required`,
|
||||
// Return empty list instead of 401 error for better UX
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: [],
|
||||
message: `${providerKey.charAt(0).toUpperCase() + providerKey.slice(1)} authentication required to view repositories`,
|
||||
requires_auth: true,
|
||||
auth_url: `/api/vcs/${providerKey}/auth/start?user_id=${encodeURIComponent(userId)}`
|
||||
auth_url: `/api/vcs/${providerKey}/auth/start?user_id=${encodeURIComponent(userId)}`,
|
||||
count: 0
|
||||
});
|
||||
}
|
||||
|
||||
@ -570,48 +729,110 @@ router.get('/:provider/auth/callback', (req, res) => {
|
||||
}
|
||||
const accessToken = await oauth.exchangeCodeForToken(code);
|
||||
const user = await oauth.getUserInfo(accessToken);
|
||||
const userId =
|
||||
req.query.user_id ||
|
||||
|
||||
// Extract userId from state parameter (embedded in OAuth state)
|
||||
let userId = null;
|
||||
const state = req.query.state;
|
||||
if (state && state.includes('|uid=')) {
|
||||
const stateParts = state.split('|');
|
||||
const uidPart = stateParts.find(part => part.startsWith('uid='));
|
||||
if (uidPart) {
|
||||
userId = uidPart.split('=')[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to other sources if not found in state
|
||||
if (!userId) {
|
||||
userId = req.query.user_id ||
|
||||
(req.body && req.body.user_id) ||
|
||||
req.headers['x-user-id'] ||
|
||||
(req.cookies && (req.cookies.user_id || req.cookies.uid)) ||
|
||||
(req.session && req.session.user && (req.session.user.id || req.session.user.userId)) ||
|
||||
(req.user && (req.user.id || req.user.userId));
|
||||
}
|
||||
if (providerKey === 'github' && !userId) {
|
||||
return res.status(400).json({ success: false, message: 'user_id is required to complete GitHub authentication' });
|
||||
}
|
||||
console.log('[VCS OAuth] callback provider=%s resolved user_id = %s', providerKey, userId || null);
|
||||
const tokenRecord = await oauth.storeToken(accessToken, user, userId || null);
|
||||
res.json({ success: true, provider: providerKey, user, token: { id: tokenRecord.id || null } });
|
||||
|
||||
// Enhanced redirect logic for private repository flow
|
||||
const frontendUrl = 'http://localhost:3001';
|
||||
|
||||
// Check if there was a pending repository attachment from private repo flow
|
||||
let redirectUrl = `${frontendUrl}/project-builder?oauth_success=true&provider=${providerKey}&user_id=${encodeURIComponent(userId || '')}`;
|
||||
|
||||
// Extract repository context from OAuth state
|
||||
if (state && (state.includes('|repo=') || state.includes('|private_repo=true'))) {
|
||||
const stateParts = state.split('|');
|
||||
const repoUrlPart = stateParts.find(part => part.startsWith('repo='));
|
||||
const branchPart = stateParts.find(part => part.startsWith('branch='));
|
||||
const privateRepoPart = stateParts.find(part => part.startsWith('private_repo='));
|
||||
|
||||
if (repoUrlPart) {
|
||||
const repoUrl = decodeURIComponent(repoUrlPart.split('=')[1]);
|
||||
const branchName = branchPart ? decodeURIComponent(branchPart.split('=')[1]) : 'main';
|
||||
const isPrivateRepo = privateRepoPart ? privateRepoPart.split('=')[1] === 'true' : false;
|
||||
|
||||
if (isPrivateRepo) {
|
||||
// For private repos, redirect to project-builder with sync progress tracking
|
||||
redirectUrl += `&sync_private_repo=true&repository_url=${encodeURIComponent(repoUrl)}&branch_name=${encodeURIComponent(branchName)}&sync_status=authenticating`;
|
||||
|
||||
console.log(`🔒 [VCS OAUTH] Private repository OAuth completed, starting sync: ${repoUrl}`);
|
||||
|
||||
// Start background sync process
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await startPrivateRepoSync(providerKey, repoUrl, branchName, userId);
|
||||
} catch (error) {
|
||||
console.error(`❌ [VCS OAUTH] Background sync failed for ${repoUrl}:`, error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// For regular repos, include attachment parameters
|
||||
redirectUrl += `&attach_repo=true&repository_url=${encodeURIComponent(repoUrl)}&branch_name=${encodeURIComponent(branchName)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ [VCS OAUTH] Redirecting to frontend: ${redirectUrl}`);
|
||||
res.redirect(redirectUrl);
|
||||
} catch (e) {
|
||||
|
||||
console.error(`❌ [VCS OAUTH] Callback error for ${req.params.provider}:`, e);
|
||||
|
||||
// Always redirect to frontend even on errors, so user sees proper error message
|
||||
const frontendUrl = 'http://localhost:3001';
|
||||
const providerKey = (req.params.provider || '').toLowerCase();
|
||||
|
||||
// Extract userId from state if available
|
||||
let userId = null;
|
||||
const state = req.query.state;
|
||||
if (state && state.includes('|uid=')) {
|
||||
const stateParts = state.split('|');
|
||||
const uidPart = stateParts.find(part => part.startsWith('uid='));
|
||||
if (uidPart) {
|
||||
userId = uidPart.split('=')[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Provide more specific error messages
|
||||
let errorMessage = e.message || 'OAuth callback failed';
|
||||
let statusCode = 500;
|
||||
|
||||
if (e.message.includes('not configured')) {
|
||||
statusCode = 500;
|
||||
errorMessage = `OAuth configuration error: ${e.message}`;
|
||||
} else if (e.message.includes('timeout')) {
|
||||
statusCode = 504;
|
||||
errorMessage = `OAuth timeout: ${e.message}`;
|
||||
} else if (e.message.includes('network error') || e.message.includes('Cannot connect')) {
|
||||
statusCode = 502;
|
||||
errorMessage = `Network error: ${e.message}`;
|
||||
} else if (e.message.includes('HTTP error')) {
|
||||
statusCode = 502;
|
||||
errorMessage = `OAuth provider error: ${e.message}`;
|
||||
}
|
||||
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
provider: req.params.provider,
|
||||
error: e.message,
|
||||
details: process.env.NODE_ENV === 'development' ? e.stack : undefined
|
||||
});
|
||||
// Redirect to frontend with error parameters
|
||||
const redirectUrl = `${frontendUrl}/project-builder?oauth_error=true&provider=${providerKey}&error_message=${encodeURIComponent(errorMessage)}&user_id=${encodeURIComponent(userId || '')}`;
|
||||
console.log(`❌ [VCS OAUTH] Redirecting to frontend with error: ${redirectUrl}`);
|
||||
res.redirect(redirectUrl);
|
||||
}
|
||||
})();
|
||||
});
|
||||
@ -1394,6 +1615,3 @@ async function getMinimalFileTree(repositoryId, filePath) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@ -18,12 +18,8 @@ class BitbucketOAuthService {
|
||||
throw new Error('Bitbucket OAuth not configured');
|
||||
}
|
||||
|
||||
// If a userId is provided, append it to the redirect_uri
|
||||
let redirectUri = this.redirectUri;
|
||||
if (userId) {
|
||||
const hasQuery = redirectUri.includes('?');
|
||||
redirectUri = `${redirectUri}${hasQuery ? '&' : '?'}user_id=${encodeURIComponent(userId)}`;
|
||||
}
|
||||
// Use the base redirect URI without user_id parameter
|
||||
const redirectUri = this.redirectUri;
|
||||
|
||||
// Embed userId into the OAuth state for fallback extraction
|
||||
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
||||
|
||||
@ -21,12 +21,8 @@ class GiteaOAuthService {
|
||||
throw new Error('Gitea OAuth not configured');
|
||||
}
|
||||
|
||||
// If a userId is provided, append it to the redirect_uri
|
||||
let redirectUri = this.redirectUri;
|
||||
if (userId) {
|
||||
const hasQuery = redirectUri.includes('?');
|
||||
redirectUri = `${redirectUri}${hasQuery ? '&' : '?'}user_id=${encodeURIComponent(userId)}`;
|
||||
}
|
||||
// Use the base redirect URI without user_id parameter
|
||||
const redirectUri = this.redirectUri;
|
||||
|
||||
// Embed userId into the OAuth state for fallback extraction
|
||||
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
||||
|
||||
@ -289,8 +289,14 @@ class GitHubIntegrationService {
|
||||
}
|
||||
|
||||
// Get repository information from GitHub
|
||||
async fetchRepositoryMetadata(owner, repo) {
|
||||
const octokit = await this.getAuthenticatedOctokit();
|
||||
async fetchRepositoryMetadata(owner, repo, skipAuth = false) {
|
||||
// If skipAuth is true, try with unauthenticated octokit first to check visibility
|
||||
let octokit;
|
||||
if (skipAuth) {
|
||||
octokit = this.octokit; // Use unauthenticated instance
|
||||
} else {
|
||||
octokit = await this.getAuthenticatedOctokit();
|
||||
}
|
||||
|
||||
const safe = async (fn, fallback) => {
|
||||
try {
|
||||
@ -301,10 +307,36 @@ class GitHubIntegrationService {
|
||||
}
|
||||
};
|
||||
|
||||
const repoData = await safe(
|
||||
async () => (await octokit.repos.get({ owner, repo })).data,
|
||||
let repoData;
|
||||
try {
|
||||
const response = await octokit.repos.get({ owner, repo });
|
||||
if (skipAuth) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new Error('Authentication required to access repository');
|
||||
} else if (response.status === 404) {
|
||||
throw new Error('Repository not found');
|
||||
}
|
||||
}
|
||||
repoData = response.data;
|
||||
} catch (error) {
|
||||
console.log(`🔍 [GitHub] Error in fetchRepositoryMetadata:`, error.message, error.status);
|
||||
if (skipAuth) {
|
||||
// For GitHub, any error when skipAuth=true likely means private repo
|
||||
if (error.status === 401 || error.status === 403 || error.status === 404) {
|
||||
throw new Error('Authentication required to access repository');
|
||||
}
|
||||
// For other errors, also assume private repo
|
||||
throw new Error('Authentication required to access repository');
|
||||
}
|
||||
// For other errors, use safe fallback
|
||||
repoData = await safe(
|
||||
async () => {
|
||||
const response = await octokit.repos.get({ owner, repo });
|
||||
return response.data;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
const languages = await safe(
|
||||
async () => (await octokit.repos.listLanguages({ owner, repo })).data,
|
||||
|
||||
@ -19,14 +19,10 @@ class GitHubOAuthService {
|
||||
throw new Error('GitHub OAuth not configured');
|
||||
}
|
||||
|
||||
// If a userId is provided, append it to the redirect_uri so the callback can link token to that user
|
||||
let redirectUri = this.redirectUri;
|
||||
if (userId) {
|
||||
const hasQuery = redirectUri.includes('?');
|
||||
redirectUri = `${redirectUri}${hasQuery ? '&' : '?'}user_id=${encodeURIComponent(userId)}`;
|
||||
}
|
||||
// Use the base redirect URI without user_id parameter
|
||||
const redirectUri = this.redirectUri;
|
||||
|
||||
// Also embed userId into the OAuth state for fallback extraction in callback
|
||||
// Embed userId into the OAuth state for fallback extraction in callback
|
||||
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
|
||||
@ -19,12 +19,8 @@ class GitLabOAuthService {
|
||||
throw new Error('GitLab OAuth not configured');
|
||||
}
|
||||
|
||||
// If a userId is provided, append it to the redirect_uri
|
||||
let redirectUri = this.redirectUri;
|
||||
if (userId) {
|
||||
const hasQuery = redirectUri.includes('?');
|
||||
redirectUri = `${redirectUri}${hasQuery ? '&' : '?'}user_id=${encodeURIComponent(userId)}`;
|
||||
}
|
||||
// Use the base redirect URI without user_id parameter
|
||||
const redirectUri = this.redirectUri;
|
||||
|
||||
// Embed userId into the OAuth state for fallback extraction
|
||||
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
||||
@ -33,7 +29,7 @@ class GitLabOAuthService {
|
||||
client_id: this.clientId,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
scope: 'read_api api read_user read_repository',
|
||||
scope: 'read_api read_user read_repository',
|
||||
state: stateWithUser
|
||||
});
|
||||
|
||||
|
||||
@ -117,7 +117,34 @@ class BitbucketAdapter extends VcsProviderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchRepositoryMetadata(owner, repo) {
|
||||
async fetchRepositoryMetadata(owner, repo, skipAuth = false) {
|
||||
// If skipAuth is true, try without authentication first to check visibility
|
||||
if (skipAuth) {
|
||||
try {
|
||||
const resp = await fetch(`https://api.bitbucket.org/2.0/repositories/${owner}/${repo}`);
|
||||
if (resp.ok) {
|
||||
const d = await resp.json();
|
||||
// Check if the response contains an error message
|
||||
if (d.error || (d.message && d.message.includes('Not Found'))) {
|
||||
throw new Error('Repository not found');
|
||||
}
|
||||
return { full_name: d.full_name, visibility: d.is_private ? 'private' : 'public', default_branch: d.mainbranch?.name || 'main', updated_at: d.updated_on };
|
||||
} else if (resp.status === 404) {
|
||||
// For Bitbucket, 404 can mean either "not found" or "private repo"
|
||||
// We'll assume it's private and require authentication
|
||||
throw new Error('Authentication required to access repository');
|
||||
} else if (resp.status === 401 || resp.status === 403) {
|
||||
throw new Error('Authentication required to access repository');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.message.includes('Authentication required') || error.message.includes('Repository not found')) {
|
||||
throw error;
|
||||
}
|
||||
// If other error, fall through to authenticated attempt
|
||||
}
|
||||
}
|
||||
|
||||
// Try with authentication (original behavior)
|
||||
const token = await this.oauth.getToken();
|
||||
if (token?.access_token) {
|
||||
try {
|
||||
@ -129,6 +156,8 @@ class BitbucketAdapter extends VcsProviderInterface {
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return { full_name: `${owner}/${repo}`, visibility: 'public', default_branch: 'main', updated_at: new Date().toISOString() };
|
||||
}
|
||||
|
||||
|
||||
@ -175,9 +175,44 @@ class GiteaAdapter extends VcsProviderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchRepositoryMetadata(owner, repo) {
|
||||
const token = await this.oauth.getToken();
|
||||
async fetchRepositoryMetadata(owner, repo, skipAuth = false) {
|
||||
const base = (process.env.GITEA_BASE_URL || 'https://gitea.com').replace(/\/$/, '');
|
||||
|
||||
// If skipAuth is true, try without authentication first to check visibility
|
||||
if (skipAuth) {
|
||||
try {
|
||||
const response = await axios.get(`${base}/api/v1/repos/${owner}/${repo}`, {
|
||||
httpsAgent: new https.Agent({
|
||||
keepAlive: true,
|
||||
timeout: 15000,
|
||||
family: 4 // Force IPv4 to avoid IPv6 connectivity issues
|
||||
}),
|
||||
timeout: 15000
|
||||
});
|
||||
if (response.status === 200) {
|
||||
const d = response.data;
|
||||
// Check if the response contains an error message
|
||||
if (d.message && d.message.includes('Not Found')) {
|
||||
throw new Error('Repository not found');
|
||||
}
|
||||
return { full_name: d.full_name || `${owner}/${repo}`, visibility: d.private ? 'private' : 'public', default_branch: d.default_branch || 'main', updated_at: d.updated_at };
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response && (error.response.status === 401 || error.response.status === 404)) {
|
||||
if (error.response.status === 404) {
|
||||
// For Gitea, 404 can mean either "not found" or "private repo"
|
||||
// We'll assume it's private and require authentication
|
||||
throw new Error('Authentication required to access repository');
|
||||
} else {
|
||||
throw new Error('Authentication required to access repository');
|
||||
}
|
||||
}
|
||||
// If other error, fall through to authenticated attempt
|
||||
}
|
||||
}
|
||||
|
||||
// Try with authentication (original behavior)
|
||||
const token = await this.oauth.getToken();
|
||||
if (token?.access_token) {
|
||||
try {
|
||||
const response = await axios.get(`${base}/api/v1/repos/${owner}/${repo}`, {
|
||||
@ -197,6 +232,8 @@ class GiteaAdapter extends VcsProviderInterface {
|
||||
console.log(`❌ [GITEA] Failed to fetch repository metadata: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return { full_name: `${owner}/${repo}`, visibility: 'public', default_branch: 'main', updated_at: new Date().toISOString() };
|
||||
}
|
||||
|
||||
|
||||
@ -98,9 +98,45 @@ class GitlabAdapter extends VcsProviderInterface {
|
||||
};
|
||||
}
|
||||
|
||||
async fetchRepositoryMetadata(owner, repo) {
|
||||
const token = await this.oauth.getToken();
|
||||
async fetchRepositoryMetadata(owner, repo, skipAuth = false) {
|
||||
const base = (process.env.GITLAB_BASE_URL || 'https://gitlab.com').replace(/\/$/, '');
|
||||
|
||||
// If skipAuth is true, try without authentication first to check visibility
|
||||
if (skipAuth) {
|
||||
try {
|
||||
const url = `${base}/api/v4/projects/${encodeURIComponent(`${owner}/${repo}`)}`;
|
||||
console.log(`🔍 [GitLab Adapter] Fetching metadata without auth: ${url}`);
|
||||
const resp = await fetch(url);
|
||||
console.log(`🔍 [GitLab Adapter] Response status: ${resp.status}`);
|
||||
|
||||
if (resp.ok) {
|
||||
const d = await resp.json();
|
||||
console.log(`🔍 [GitLab Adapter] Response data:`, JSON.stringify(d, null, 2));
|
||||
// Check if the response contains an error message
|
||||
if (d.message && (d.message.includes('404') || d.message.includes('Not Found'))) {
|
||||
throw new Error('Repository not found');
|
||||
}
|
||||
return { full_name: d.path_with_namespace, visibility: d.visibility === 'public' ? 'public' : 'private', default_branch: d.default_branch || 'main', updated_at: d.last_activity_at };
|
||||
} else if (resp.status === 404) {
|
||||
// For GitLab, 404 can mean either "not found" or "private repo"
|
||||
// We'll assume it's private and require authentication
|
||||
console.log(`🔍 [GitLab Adapter] 404 response - assuming private repo`);
|
||||
throw new Error('Authentication required to access repository');
|
||||
} else if (resp.status === 401 || resp.status === 403) {
|
||||
console.log(`🔍 [GitLab Adapter] ${resp.status} response - authentication required`);
|
||||
throw new Error('Authentication required to access repository');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`🔍 [GitLab Adapter] Error in skipAuth:`, error.message);
|
||||
if (error.message.includes('Authentication required') || error.message.includes('Repository not found')) {
|
||||
throw error;
|
||||
}
|
||||
// If other error, fall through to authenticated attempt
|
||||
}
|
||||
}
|
||||
|
||||
// Try with authentication (original behavior)
|
||||
const token = await this.oauth.getToken();
|
||||
if (token?.access_token) {
|
||||
try {
|
||||
const resp = await fetch(`${base}/api/v4/projects/${encodeURIComponent(`${owner}/${repo}`)}`, { headers: { Authorization: `Bearer ${token.access_token}` } });
|
||||
@ -110,6 +146,8 @@ class GitlabAdapter extends VcsProviderInterface {
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return { full_name: `${owner}/${repo}`, visibility: 'public', default_branch: 'main', updated_at: new Date().toISOString() };
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user