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_ACCESS_SECRET=access-secret-key-2024-tech4biz-secure_pipeline_2024
|
||||||
# - JWT_REFRESH_SECRET=refresh-secret-key-2024-tech4biz-secure_pipeline_2024
|
# - JWT_REFRESH_SECRET=refresh-secret-key-2024-tech4biz-secure_pipeline_2024
|
||||||
# Service URLs
|
# Service URLs
|
||||||
- USER_AUTH_URL=http://user-auth:8011
|
- USER_AUTH_URL=http://pipeline_user_auth:8011
|
||||||
- TEMPLATE_MANAGER_URL=http://template-manager:8009
|
- TEMPLATE_MANAGER_URL=http://template-manager:8009
|
||||||
- GIT_INTEGRATION_URL=http://git-integration:8012
|
- GIT_INTEGRATION_URL=http://git-integration:8012
|
||||||
- REQUIREMENT_PROCESSOR_URL=http://requirement-processor:8001
|
- REQUIREMENT_PROCESSOR_URL=http://requirement-processor:8001
|
||||||
|
|||||||
@ -1814,7 +1814,10 @@ app.use('/api/vcs',
|
|||||||
},
|
},
|
||||||
timeout: 45000,
|
timeout: 45000,
|
||||||
validateStatus: () => true,
|
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
|
// 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.session && req.session.user && (req.session.user.id || req.session.user.userId)) ||
|
||||||
(req.user && (req.user.id || req.user.userId));
|
(req.user && (req.user.id || req.user.userId));
|
||||||
if (!user_id && typeof state === 'string' && state.includes('|uid=')) {
|
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) {
|
if (!user_id) {
|
||||||
@ -113,11 +119,20 @@ router.get('/auth/github/callback', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Redirect back to frontend IMMEDIATELY (before heavy cloning operation)
|
// 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 {
|
try {
|
||||||
const redirectUrl = `${frontendUrl}?github_connected=1&user=${encodeURIComponent(githubUser.login)}&processing=1`;
|
// Check if this is a private repository OAuth flow
|
||||||
console.log('[GitHub OAuth] Redirecting to:', redirectUrl);
|
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
|
// Send redirect response immediately
|
||||||
res.redirect(302, redirectUrl);
|
res.redirect(302, redirectUrl);
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
// Attach repository (provider-agnostic)
|
||||||
router.post('/:provider/attach-repository', async (req, res) => {
|
router.post('/:provider/attach-repository', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -53,33 +156,86 @@ router.post('/:provider/attach-repository', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { owner, repo, branch } = provider.parseRepoUrl(repository_url);
|
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);
|
const accessCheck = await provider.checkRepositoryAccess(owner, repo, userId);
|
||||||
|
|
||||||
if (!accessCheck.hasAccess) {
|
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' });
|
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) {
|
if (!tokenRecord) {
|
||||||
return res.status(401).json({
|
// Return empty list instead of 401 error for better UX
|
||||||
success: false,
|
return res.status(200).json({
|
||||||
message: `${providerKey.charAt(0).toUpperCase() + providerKey.slice(1)} authentication required`,
|
success: true,
|
||||||
|
data: [],
|
||||||
|
message: `${providerKey.charAt(0).toUpperCase() + providerKey.slice(1)} authentication required to view repositories`,
|
||||||
requires_auth: true,
|
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 accessToken = await oauth.exchangeCodeForToken(code);
|
||||||
const user = await oauth.getUserInfo(accessToken);
|
const user = await oauth.getUserInfo(accessToken);
|
||||||
const userId =
|
|
||||||
req.query.user_id ||
|
// Extract userId from state parameter (embedded in OAuth state)
|
||||||
(req.body && req.body.user_id) ||
|
let userId = null;
|
||||||
req.headers['x-user-id'] ||
|
const state = req.query.state;
|
||||||
(req.cookies && (req.cookies.user_id || req.cookies.uid)) ||
|
if (state && state.includes('|uid=')) {
|
||||||
(req.session && req.session.user && (req.session.user.id || req.session.user.userId)) ||
|
const stateParts = state.split('|');
|
||||||
(req.user && (req.user.id || req.user.userId));
|
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) {
|
if (providerKey === 'github' && !userId) {
|
||||||
return res.status(400).json({ success: false, message: 'user_id is required to complete GitHub authentication' });
|
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);
|
console.log('[VCS OAuth] callback provider=%s resolved user_id = %s', providerKey, userId || null);
|
||||||
const tokenRecord = await oauth.storeToken(accessToken, user, 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) {
|
} catch (e) {
|
||||||
|
|
||||||
console.error(`❌ [VCS OAUTH] Callback error for ${req.params.provider}:`, 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
|
// Provide more specific error messages
|
||||||
let errorMessage = e.message || 'OAuth callback failed';
|
let errorMessage = e.message || 'OAuth callback failed';
|
||||||
let statusCode = 500;
|
|
||||||
|
|
||||||
if (e.message.includes('not configured')) {
|
if (e.message.includes('not configured')) {
|
||||||
statusCode = 500;
|
|
||||||
errorMessage = `OAuth configuration error: ${e.message}`;
|
errorMessage = `OAuth configuration error: ${e.message}`;
|
||||||
} else if (e.message.includes('timeout')) {
|
} else if (e.message.includes('timeout')) {
|
||||||
statusCode = 504;
|
|
||||||
errorMessage = `OAuth timeout: ${e.message}`;
|
errorMessage = `OAuth timeout: ${e.message}`;
|
||||||
} else if (e.message.includes('network error') || e.message.includes('Cannot connect')) {
|
} else if (e.message.includes('network error') || e.message.includes('Cannot connect')) {
|
||||||
statusCode = 502;
|
|
||||||
errorMessage = `Network error: ${e.message}`;
|
errorMessage = `Network error: ${e.message}`;
|
||||||
} else if (e.message.includes('HTTP error')) {
|
} else if (e.message.includes('HTTP error')) {
|
||||||
statusCode = 502;
|
|
||||||
errorMessage = `OAuth provider error: ${e.message}`;
|
errorMessage = `OAuth provider error: ${e.message}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(statusCode).json({
|
// Redirect to frontend with error parameters
|
||||||
success: false,
|
const redirectUrl = `${frontendUrl}/project-builder?oauth_error=true&provider=${providerKey}&error_message=${encodeURIComponent(errorMessage)}&user_id=${encodeURIComponent(userId || '')}`;
|
||||||
message: errorMessage,
|
console.log(`❌ [VCS OAUTH] Redirecting to frontend with error: ${redirectUrl}`);
|
||||||
provider: req.params.provider,
|
res.redirect(redirectUrl);
|
||||||
error: e.message,
|
|
||||||
details: process.env.NODE_ENV === 'development' ? e.stack : undefined
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
@ -1393,7 +1614,4 @@ async function getMinimalFileTree(repositoryId, filePath) {
|
|||||||
// Return null tree as fallback
|
// Return null tree as fallback
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@ -18,12 +18,8 @@ class BitbucketOAuthService {
|
|||||||
throw new Error('Bitbucket OAuth not configured');
|
throw new Error('Bitbucket OAuth not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a userId is provided, append it to the redirect_uri
|
// Use the base redirect URI without user_id parameter
|
||||||
let redirectUri = this.redirectUri;
|
const redirectUri = this.redirectUri;
|
||||||
if (userId) {
|
|
||||||
const hasQuery = redirectUri.includes('?');
|
|
||||||
redirectUri = `${redirectUri}${hasQuery ? '&' : '?'}user_id=${encodeURIComponent(userId)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Embed userId into the OAuth state for fallback extraction
|
// Embed userId into the OAuth state for fallback extraction
|
||||||
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
||||||
|
|||||||
@ -21,12 +21,8 @@ class GiteaOAuthService {
|
|||||||
throw new Error('Gitea OAuth not configured');
|
throw new Error('Gitea OAuth not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a userId is provided, append it to the redirect_uri
|
// Use the base redirect URI without user_id parameter
|
||||||
let redirectUri = this.redirectUri;
|
const redirectUri = this.redirectUri;
|
||||||
if (userId) {
|
|
||||||
const hasQuery = redirectUri.includes('?');
|
|
||||||
redirectUri = `${redirectUri}${hasQuery ? '&' : '?'}user_id=${encodeURIComponent(userId)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Embed userId into the OAuth state for fallback extraction
|
// Embed userId into the OAuth state for fallback extraction
|
||||||
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
||||||
|
|||||||
@ -289,22 +289,54 @@ class GitHubIntegrationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get repository information from GitHub
|
// Get repository information from GitHub
|
||||||
async fetchRepositoryMetadata(owner, repo) {
|
async fetchRepositoryMetadata(owner, repo, skipAuth = false) {
|
||||||
const octokit = await this.getAuthenticatedOctokit();
|
// 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) => {
|
const safe = async (fn, fallback) => {
|
||||||
try {
|
try {
|
||||||
return await fn();
|
return await fn();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`API call failed: ${error.message}`);
|
console.warn(`API call failed: ${error.message}`);
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const repoData = await safe(
|
let repoData;
|
||||||
async () => (await octokit.repos.get({ owner, repo })).data,
|
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(
|
const languages = await safe(
|
||||||
async () => (await octokit.repos.listLanguages({ owner, repo })).data,
|
async () => (await octokit.repos.listLanguages({ owner, repo })).data,
|
||||||
|
|||||||
@ -19,14 +19,10 @@ class GitHubOAuthService {
|
|||||||
throw new Error('GitHub OAuth not configured');
|
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
|
// Use the base redirect URI without user_id parameter
|
||||||
let redirectUri = this.redirectUri;
|
const redirectUri = this.redirectUri;
|
||||||
if (userId) {
|
|
||||||
const hasQuery = redirectUri.includes('?');
|
|
||||||
redirectUri = `${redirectUri}${hasQuery ? '&' : '?'}user_id=${encodeURIComponent(userId)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
|||||||
@ -19,12 +19,8 @@ class GitLabOAuthService {
|
|||||||
throw new Error('GitLab OAuth not configured');
|
throw new Error('GitLab OAuth not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a userId is provided, append it to the redirect_uri
|
// Use the base redirect URI without user_id parameter
|
||||||
let redirectUri = this.redirectUri;
|
const redirectUri = this.redirectUri;
|
||||||
if (userId) {
|
|
||||||
const hasQuery = redirectUri.includes('?');
|
|
||||||
redirectUri = `${redirectUri}${hasQuery ? '&' : '?'}user_id=${encodeURIComponent(userId)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Embed userId into the OAuth state for fallback extraction
|
// Embed userId into the OAuth state for fallback extraction
|
||||||
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
||||||
@ -33,7 +29,7 @@ class GitLabOAuthService {
|
|||||||
client_id: this.clientId,
|
client_id: this.clientId,
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
scope: 'read_api api read_user read_repository',
|
scope: 'read_api read_user read_repository',
|
||||||
state: stateWithUser
|
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();
|
const token = await this.oauth.getToken();
|
||||||
if (token?.access_token) {
|
if (token?.access_token) {
|
||||||
try {
|
try {
|
||||||
@ -129,6 +156,8 @@ class BitbucketAdapter extends VcsProviderInterface {
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
return { full_name: `${owner}/${repo}`, visibility: 'public', default_branch: 'main', updated_at: new Date().toISOString() };
|
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) {
|
async fetchRepositoryMetadata(owner, repo, skipAuth = false) {
|
||||||
const token = await this.oauth.getToken();
|
|
||||||
const base = (process.env.GITEA_BASE_URL || 'https://gitea.com').replace(/\/$/, '');
|
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) {
|
if (token?.access_token) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${base}/api/v1/repos/${owner}/${repo}`, {
|
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}`);
|
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() };
|
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) {
|
async fetchRepositoryMetadata(owner, repo, skipAuth = false) {
|
||||||
const token = await this.oauth.getToken();
|
|
||||||
const base = (process.env.GITLAB_BASE_URL || 'https://gitlab.com').replace(/\/$/, '');
|
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) {
|
if (token?.access_token) {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${base}/api/v4/projects/${encodeURIComponent(`${owner}/${repo}`)}`, { headers: { Authorization: `Bearer ${token.access_token}` } });
|
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 (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
return { full_name: `${owner}/${repo}`, visibility: 'public', default_branch: 'main', updated_at: new Date().toISOString() };
|
return { full_name: `${owner}/${repo}`, visibility: 'public', default_branch: 'main', updated_at: new Date().toISOString() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user