diff --git a/docker-compose.yml b/docker-compose.yml index 709946e..9c05177 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/services/api-gateway/src/server.js b/services/api-gateway/src/server.js index 860672e..4c64e39 100644 --- a/services/api-gateway/src/server.js +++ b/services/api-gateway/src/server.js @@ -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 diff --git a/services/git-integration/src/routes/github-oauth.js b/services/git-integration/src/routes/github-oauth.js index 6fc28e9..3f390de 100644 --- a/services/git-integration/src/routes/github-oauth.js +++ b/services/git-integration/src/routes/github-oauth.js @@ -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,11 +119,20 @@ 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`; - console.log('[GitHub OAuth] Redirecting to:', redirectUrl); + // 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 res.redirect(302, redirectUrl); diff --git a/services/git-integration/src/routes/vcs.routes.js b/services/git-integration/src/routes/vcs.routes.js index ed5f178..cbe0225 100644 --- a/services/git-integration/src/routes/vcs.routes.js +++ b/services/git-integration/src/routes/vcs.routes.js @@ -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,33 +156,86 @@ router.post('/:provider/attach-repository', async (req, res) => { } const { owner, repo, branch } = provider.parseRepoUrl(repository_url); + + // 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; + if (userId) { + tokenRecord = await oauthService.getTokenForUser(userId); + } + if (!tokenRecord) { + tokenRecord = await oauthService.getToken(); + } + + if (!tokenRecord) { + // 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 private repository`, + requires_auth: true, + 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) { - if (accessCheck.requiresAuth) { - // Check if we have OAuth token for this provider - const providerKey = (req.params.provider || '').toLowerCase(); - const oauthService = getOAuthService(providerKey); - if (oauthService) { - let tokenRecord = null; - if (userId) { - tokenRecord = await oauthService.getTokenForUser(userId); - } - if (!tokenRecord) { - tokenRecord = await oauthService.getToken(); - } - - if (!tokenRecord) { - return res.status(401).json({ - success: false, - message: `${providerKey.charAt(0).toUpperCase() + providerKey.slice(1)} authentication required for this repository`, - requires_auth: true, - auth_url: `/api/vcs/${providerKey}/auth/start?user_id=${encodeURIComponent(userId)}` - }); - } - } - } - 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 || - (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)); + + // 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); } })(); }); @@ -1393,7 +1614,4 @@ async function getMinimalFileTree(repositoryId, filePath) { // Return null tree as fallback return null; } -} - - -module.exports = router; \ No newline at end of file +} \ No newline at end of file diff --git a/services/git-integration/src/services/bitbucket-oauth.js b/services/git-integration/src/services/bitbucket-oauth.js index 114f98c..25d8de7 100644 --- a/services/git-integration/src/services/bitbucket-oauth.js +++ b/services/git-integration/src/services/bitbucket-oauth.js @@ -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; diff --git a/services/git-integration/src/services/gitea-oauth.js b/services/git-integration/src/services/gitea-oauth.js index 940de1f..9c6293f 100644 --- a/services/git-integration/src/services/gitea-oauth.js +++ b/services/git-integration/src/services/gitea-oauth.js @@ -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; diff --git a/services/git-integration/src/services/github-integration.service.js b/services/git-integration/src/services/github-integration.service.js index 82e70e4..c602078 100644 --- a/services/git-integration/src/services/github-integration.service.js +++ b/services/git-integration/src/services/github-integration.service.js @@ -289,22 +289,54 @@ 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 { - return await fn(); - } catch (error) { + try { + return await fn(); + } catch (error) { console.warn(`API call failed: ${error.message}`); - return fallback; + return fallback; } }; - 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, diff --git a/services/git-integration/src/services/github-oauth.js b/services/git-integration/src/services/github-oauth.js index fa5af97..bf251ea 100644 --- a/services/git-integration/src/services/github-oauth.js +++ b/services/git-integration/src/services/github-oauth.js @@ -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({ diff --git a/services/git-integration/src/services/gitlab-oauth.js b/services/git-integration/src/services/gitlab-oauth.js index a3dad21..642e5d9 100644 --- a/services/git-integration/src/services/gitlab-oauth.js +++ b/services/git-integration/src/services/gitlab-oauth.js @@ -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 }); diff --git a/services/git-integration/src/services/providers/bitbucket.adapter.js b/services/git-integration/src/services/providers/bitbucket.adapter.js index 3fecaef..b8ed9c7 100644 --- a/services/git-integration/src/services/providers/bitbucket.adapter.js +++ b/services/git-integration/src/services/providers/bitbucket.adapter.js @@ -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() }; } diff --git a/services/git-integration/src/services/providers/gitea.adapter.js b/services/git-integration/src/services/providers/gitea.adapter.js index d01b4b1..3fa4112 100644 --- a/services/git-integration/src/services/providers/gitea.adapter.js +++ b/services/git-integration/src/services/providers/gitea.adapter.js @@ -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() }; } diff --git a/services/git-integration/src/services/providers/gitlab.adapter.js b/services/git-integration/src/services/providers/gitlab.adapter.js index 72ef030..cd457e7 100644 --- a/services/git-integration/src/services/providers/gitlab.adapter.js +++ b/services/git-integration/src/services/providers/gitlab.adapter.js @@ -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() }; }