done the redirection issue

This commit is contained in:
Pradeep 2025-10-15 14:59:57 +05:30
parent 6698d11597
commit 64f2e401af
4 changed files with 552 additions and 254 deletions

View File

@ -19,16 +19,81 @@ function ProjectBuilderContent() {
} }
}, [user, isLoading, router]) }, [user, isLoading, router])
// Handle GitHub OAuth callback parameters // Handle VCS OAuth callback parameters (including private repo sync)
useEffect(() => { useEffect(() => {
if (isLoading || !user) return if (isLoading || !user) return
const oauthSuccess = searchParams.get('oauth_success')
const provider = searchParams.get('provider')
const userId = searchParams.get('user_id')
const syncPrivateRepo = searchParams.get('sync_private_repo')
const repositoryUrl = searchParams.get('repository_url')
const branchName = searchParams.get('branch_name')
const syncStatus = searchParams.get('sync_status')
// Handle OAuth errors
const oauthError = searchParams.get('oauth_error')
const errorMessage = searchParams.get('error_message')
if (oauthError === 'true' && provider && errorMessage) {
console.error(`❌ [Project Builder] OAuth error for ${provider}:`, errorMessage)
// Show error message to user
alert(`OAuth Error for ${provider.toUpperCase()}:\n\n${decodeURIComponent(errorMessage)}\n\nPlease try again or contact support if the issue persists.`)
// Clear URL parameters
const newUrl = new URL(window.location.href)
newUrl.searchParams.delete('oauth_error')
newUrl.searchParams.delete('provider')
newUrl.searchParams.delete('error_message')
newUrl.searchParams.delete('user_id')
window.history.replaceState({}, '', newUrl.toString())
return
}
// Handle new private repo sync flow
if (oauthSuccess === 'true' && provider && syncPrivateRepo === 'true' && repositoryUrl) {
console.log(`🔄 [Project Builder] Private repo sync initiated:`, {
provider,
repositoryUrl,
branchName,
syncStatus
})
// Store sync info in sessionStorage for the main dashboard to pick up
try {
sessionStorage.setItem('private_repo_sync', JSON.stringify({
provider,
repositoryUrl,
branchName,
syncStatus,
timestamp: Date.now()
}))
} catch (e) {
console.warn('Failed to store sync info:', e)
}
// Clear URL parameters
const newUrl = new URL(window.location.href)
newUrl.searchParams.delete('oauth_success')
newUrl.searchParams.delete('provider')
newUrl.searchParams.delete('user_id')
newUrl.searchParams.delete('sync_private_repo')
newUrl.searchParams.delete('repository_url')
newUrl.searchParams.delete('branch_name')
newUrl.searchParams.delete('sync_status')
window.history.replaceState({}, '', newUrl.toString())
return
}
// Handle legacy GitHub OAuth callback parameters
const githubConnected = searchParams.get('github_connected') const githubConnected = searchParams.get('github_connected')
const githubUser = searchParams.get('user') const githubUser = searchParams.get('user')
const processing = searchParams.get('processing') const processing = searchParams.get('processing')
const repoAttached = searchParams.get('repo_attached') const repoAttached = searchParams.get('repo_attached')
const repositoryId = searchParams.get('repository_id') const repositoryId = searchParams.get('repository_id')
const syncStatus = searchParams.get('sync_status')
if (githubConnected === '1') { if (githubConnected === '1') {
console.log('🎉 GitHub OAuth callback successful!', { console.log('🎉 GitHub OAuth callback successful!', {

View File

@ -23,7 +23,9 @@ import {
Server Server
} from 'lucide-react'; } from 'lucide-react';
import { authApiClient } from '@/components/apis/authApiClients'; import { authApiClient } from '@/components/apis/authApiClients';
import { useAuth } from '@/contexts/auth-context';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation';
interface VcsRepoSummary { interface VcsRepoSummary {
id: string; id: string;
@ -44,6 +46,8 @@ interface VcsRepoSummary {
} }
const VcsReposPage: React.FC = () => { const VcsReposPage: React.FC = () => {
const { user, isLoading: authLoading } = useAuth();
const router = useRouter();
const [repositories, setRepositories] = useState<VcsRepoSummary[]>([]); const [repositories, setRepositories] = useState<VcsRepoSummary[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -53,72 +57,111 @@ const VcsReposPage: React.FC = () => {
const [aiAnalysisLoading, setAiAnalysisLoading] = useState<string | null>(null); const [aiAnalysisLoading, setAiAnalysisLoading] = useState<string | null>(null);
const [aiAnalysisError, setAiAnalysisError] = useState<string | null>(null); const [aiAnalysisError, setAiAnalysisError] = useState<string | null>(null);
// Redirect to sign-in if not authenticated
useEffect(() => {
if (!authLoading && !user) {
const returnUrl = encodeURIComponent(window.location.pathname + window.location.search);
router.push(`/signin?returnUrl=${returnUrl}`);
}
}, [user, authLoading, router]);
// Handle OAuth callback for all providers // Handle OAuth callback for all providers
useEffect(() => { useEffect(() => {
const handleVcsCallback = async () => { const handleVcsCallback = async () => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code'); const oauthSuccess = urlParams.get('oauth_success');
const state = urlParams.get('state'); const provider = urlParams.get('provider');
const error = urlParams.get('error'); const attachRepo = urlParams.get('attach_repo');
const repositoryUrl = urlParams.get('repository_url');
const branchName = urlParams.get('branch_name');
if (error) { // Handle OAuth success redirect from backend
console.error('VCS OAuth error:', error); if (oauthSuccess === 'true' && provider) {
alert(`OAuth error: ${error}`); console.log(`🔐 [VCS OAuth] OAuth success for ${provider.toUpperCase()}`);
return;
}
if (code && state) {
console.log('🔐 [VCS OAuth] Callback received, processing...');
try { if (attachRepo === 'true' && repositoryUrl) {
// Check if we have a pending repository attachment console.log(`🔄 [VCS OAuth] Auto-attaching repository after OAuth: ${repositoryUrl}`);
const pendingAttach = sessionStorage.getItem('pending_git_attach');
if (pendingAttach) { try {
const { repository_url, branch_name, provider } = JSON.parse(pendingAttach); // Automatically attach the repository
console.log(`🔐 [VCS OAuth] Resuming repository attachment:`, { repository_url, branch_name, provider }); const response = await authApiClient.post(`/api/vcs/${provider}/attach-repository`, {
repository_url: repositoryUrl,
branch_name: branchName || undefined,
user_id: user.id
}, {
headers: {
'x-user-id': user.id
}
});
// Clear the pending attach if (response.data?.success) {
sessionStorage.removeItem('pending_git_attach'); alert(`${provider.toUpperCase()} account connected and repository attached successfully!`);
// Now attach the repository
try {
const response = await authApiClient.post(`/api/vcs/${provider}/attach-repository`, {
repository_url,
branch_name: branch_name || undefined,
});
alert(`${provider?.toUpperCase()} account connected and repository attached successfully!`);
// Clean up URL parameters // Clean up URL parameters
const newUrl = window.location.pathname; const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl); window.history.replaceState({}, document.title, newUrl);
// Refresh the page to show the attached repository // Refresh repositories to show the new one
window.location.reload(); loadRepositories();
} catch (attachError) { } else {
console.error('Failed to attach repository after OAuth:', attachError); alert(`${provider.toUpperCase()} account connected, but failed to attach repository. Please try again.`);
alert(`${provider?.toUpperCase()} account connected, but failed to attach repository. Please try again.`);
} }
} else { } catch (attachError) {
console.log('🔐 [VCS OAuth] No pending repository attachment, just connecting account'); console.error('Failed to attach repository after OAuth:', attachError);
alert('Account connected successfully!'); alert(`${provider.toUpperCase()} account connected, but failed to attach repository. Please try again.`);
// Clean up URL parameters
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
// Refresh repositories
loadRepositories();
} }
} catch (error) { } else {
console.error('Error handling VCS OAuth callback:', error); console.log(`🔐 [VCS OAuth] ${provider.toUpperCase()} account connected successfully`);
alert('Error processing OAuth callback'); alert(`${provider.toUpperCase()} account connected successfully!`);
// Clean up URL parameters
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
// Refresh repositories
loadRepositories();
} }
} }
}; };
handleVcsCallback(); handleVcsCallback();
}, []); }, [user, loadRepositories]);
// Show loading while checking authentication
if (authLoading) {
return (
<div className="max-w-7xl mx-auto p-6">
<div className="flex items-center justify-center h-64">
<div className="text-center">
<RefreshCw className="h-8 w-8 animate-spin mx-auto mb-4" />
<p className="text-muted-foreground">Loading...</p>
</div>
</div>
</div>
);
}
// Show sign-in prompt if not authenticated
if (!user) {
return (
<div className="max-w-7xl mx-auto p-6">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">Authentication Required</h1>
<p className="text-muted-foreground mb-6">
Please sign in to view your repositories from GitHub, GitLab, Bitbucket, and Gitea.
</p>
<div className="space-y-4">
<Button asChild size="lg">
<Link href="/signin">Sign In to Continue</Link>
</Button>
<div className="text-sm text-muted-foreground">
After signing in, you can connect your VCS accounts and view all your repositories in one place.
</div>
</div>
</div>
</div>
);
}
// Load repositories from all providers // Load repositories from all providers
const loadRepositories = async () => { const loadRepositories = async () => {
@ -133,7 +176,11 @@ const VcsReposPage: React.FC = () => {
for (const provider of providers) { for (const provider of providers) {
try { try {
const response = await authApiClient.get(`/api/vcs/${provider}/repositories`); const response = await authApiClient.get(`/api/vcs/${provider}/repositories`, {
headers: {
'x-user-id': user.id
}
});
const repos = response.data?.data || []; const repos = response.data?.data || [];
// Add provider info to each repo // Add provider info to each repo

View File

@ -27,6 +27,8 @@ import { attachRepository, detectProvider, connectProvider, AttachRepositoryResp
import { getGitHubAuthStatus } from "@/lib/api/github" import { getGitHubAuthStatus } from "@/lib/api/github"
import ViewUserReposButton from "@/components/github/ViewUserReposButton" import ViewUserReposButton from "@/components/github/ViewUserReposButton"
import { ErrorBanner } from "@/components/ui/error-banner" import { ErrorBanner } from "@/components/ui/error-banner"
import { useAuth } from "@/contexts/auth-context"
import { authApiClient } from "@/components/apis/authApiClients"
interface Template { interface Template {
id: string id: string
@ -53,6 +55,7 @@ interface Template {
} }
function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => void }) { function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => void }) {
const { user } = useAuth()
const [selectedCategory, setSelectedCategory] = useState("all") const [selectedCategory, setSelectedCategory] = useState("all")
const [searchQuery, setSearchQuery] = useState("") const [searchQuery, setSearchQuery] = useState("")
const [showCustomForm, setShowCustomForm] = useState(false) const [showCustomForm, setShowCustomForm] = useState(false)
@ -70,10 +73,99 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
}) })
const [authLoading, setAuthLoading] = useState(false) const [authLoading, setAuthLoading] = useState(false)
const [gitStep, setGitStep] = useState<'provider' | 'url'>('provider') const [gitStep, setGitStep] = useState<'provider' | 'url'>('provider')
const [authUrl, setAuthUrl] = useState('')
const [isGeneratingAuth, setIsGeneratingAuth] = useState(false) // Sync progress state for private repositories
const [syncProgress, setSyncProgress] = useState<{
show: boolean;
provider: string;
repositoryUrl: string;
branchName: string;
status: string;
stage: string;
} | null>(null)
// Monitor private repository sync progress
const monitorPrivateRepoSync = async (provider: string, repositoryUrl: string) => {
let pollCount = 0;
const maxPolls = 60; // 2 minutes with 2-second intervals (reduced from 5 minutes)
const pollInterval = setInterval(async () => {
pollCount++;
try {
// Poll for repository sync status
const response = await authApiClient.get(`/api/vcs/${provider}/repositories?t=${Date.now()}`, {
headers: { 'x-user-id': user?.id }
});
if (response.data?.success) {
const repositories = response.data.data || [];
const repo = repositories.find((r: any) => r.repository_url === repositoryUrl);
if (repo) {
const status = repo.sync_status;
let stage = '';
switch (status) {
case 'authenticating':
stage = 'Authenticating with provider...';
break;
case 'syncing':
stage = 'Downloading repository files...';
break;
case 'synced':
stage = 'Repository sync completed!';
clearInterval(pollInterval);
setSyncProgress(null);
alert(`✅ Repository attached successfully!\n\nProvider: ${provider.toUpperCase()}\nRepository: ${repositoryUrl}\n\nYour repository is now available in your repositories list.`);
// Refresh repositories list
window.location.reload();
return;
case 'error':
stage = 'Sync failed. Please try again.';
clearInterval(pollInterval);
setTimeout(() => {
setSyncProgress(null);
}, 3000);
alert(`❌ Repository sync failed!\n\nProvider: ${provider.toUpperCase()}\nRepository: ${repositoryUrl}\n\nPlease try again or contact support.`);
return;
default:
stage = 'Processing...';
}
setSyncProgress(prev => prev ? { ...prev, status, stage } : null);
} else {
// Repository not found in list yet, continue polling
setSyncProgress(prev => prev ? { ...prev, stage: 'Processing...' } : null);
}
}
} catch (error) {
console.error('Error monitoring sync:', error);
// If we get too many errors, stop polling
if (pollCount > 10) {
clearInterval(pollInterval);
setSyncProgress(null);
alert(`⚠️ Unable to monitor repository sync status.\n\nRepository: ${repositoryUrl}\n\nPlease check your repositories list manually.`);
}
}
// Stop polling after max attempts
if (pollCount >= maxPolls) {
clearInterval(pollInterval);
setSyncProgress(null);
alert(`⏰ Repository sync is taking longer than expected.\n\nRepository: ${repositoryUrl}\n\nPlease check your repositories list manually.`);
}
}, 2000); // Poll every 2 seconds
}
const [isGithubConnected, setIsGithubConnected] = useState<boolean | null>(null) const [isGithubConnected, setIsGithubConnected] = useState<boolean | null>(null)
const [connectionError, setConnectionError] = useState<string | null>(null) const [connectionError, setConnectionError] = useState<string | null>(null)
// Cleanup sync progress on component unmount
useEffect(() => {
return () => {
setSyncProgress(null);
};
}, []);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@ -94,119 +186,122 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
} }
})() })()
}, []) }, [])
// Handle OAuth callback for all providers with enhanced private repo flow
useEffect(() => {
const handleVcsCallback = async () => {
// Check for private repo sync info from project-builder redirect
try {
const syncInfo = sessionStorage.getItem('private_repo_sync');
if (syncInfo) {
const { provider, repositoryUrl, branchName, syncStatus } = JSON.parse(syncInfo);
console.log(`🔄 [Main Dashboard] Restoring private repo sync:`, { provider, repositoryUrl, syncStatus });
setSyncProgress({
show: true,
provider,
repositoryUrl,
branchName,
status: syncStatus,
stage: 'Starting sync...'
});
// Start monitoring sync progress
monitorPrivateRepoSync(provider, repositoryUrl);
// Clear the stored info
sessionStorage.removeItem('private_repo_sync');
}
} catch (e) {
console.warn('Failed to restore sync info:', e);
}
const urlParams = new URLSearchParams(window.location.search);
const oauthSuccess = urlParams.get('oauth_success');
const provider = urlParams.get('provider');
const syncPrivateRepo = urlParams.get('sync_private_repo');
const repositoryUrl = urlParams.get('repository_url');
const branchName = urlParams.get('branch_name');
const syncStatus = urlParams.get('sync_status');
// Handle OAuth success redirect from backend
if (oauthSuccess === 'true' && provider) {
console.log(`🔐 [VCS OAuth] OAuth success for ${provider.toUpperCase()}`);
if (syncPrivateRepo === 'true' && repositoryUrl) {
console.log(`🔄 [VCS OAuth] Starting private repository sync monitoring: ${repositoryUrl}`);
// Show progress notification for private repo sync
setSyncProgress({
show: true,
provider: provider,
repositoryUrl: repositoryUrl,
branchName: branchName || 'main',
status: syncStatus || 'authenticating',
stage: 'Starting sync...'
});
// Start monitoring sync progress
monitorPrivateRepoSync(provider, repositoryUrl);
// Clean up URL parameters but keep sync progress visible
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
} else {
const attachRepo = urlParams.get('attach_repo');
if (attachRepo === 'true' && repositoryUrl) {
console.log(`🔄 [VCS OAuth] Auto-attaching repository after OAuth: ${repositoryUrl}`);
try {
// Automatically attach the repository
const response = await authApiClient.post(`/api/vcs/${provider}/attach-repository`, {
repository_url: repositoryUrl,
branch_name: branchName || undefined,
user_id: user?.id
}, {
headers: {
'x-user-id': user?.id
}
});
if (response.data?.success) {
alert(`${provider.toUpperCase()} account connected and repository attached successfully!`);
// Clean up URL parameters
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
// Reset form and close dialogs
setShowGitForm(false);
setShowCreateOptionDialog(false);
setGitUrl('');
setGitBranch('main');
setGitProvider('');
} else {
alert(`${provider.toUpperCase()} account connected, but failed to attach repository. Please try again.`);
}
} catch (attachError) {
console.error('Failed to attach repository after OAuth:', attachError);
alert(`${provider.toUpperCase()} account connected, but failed to attach repository. Please try again.`);
}
} else {
console.log(`🔐 [VCS OAuth] ${provider.toUpperCase()} account connected successfully`);
alert(`${provider.toUpperCase()} account connected successfully!`);
// Clean up URL parameters
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
}
}
}
};
handleVcsCallback();
}, [user])
const [editingTemplate, setEditingTemplate] = useState<DatabaseTemplate | null>(null) const [editingTemplate, setEditingTemplate] = useState<DatabaseTemplate | null>(null)
// Generate authentication by hitting the same attach endpoint and using its auth_url
const generateAuthUrl = async () => {
if (!gitUrl.trim()) return
setIsGeneratingAuth(true)
try {
// Persist pending attach so we can resume after OAuth
try {
sessionStorage.setItem('pending_git_attach', JSON.stringify({
repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined)
}))
} catch {}
const result: AttachRepositoryResponse = await attachRepository({
repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined),
})
// Debug logging
console.log('📦 Full result object:', result)
console.log('📦 result.success value:', result?.success)
console.log('📦 result.success type:', typeof result?.success)
console.log('📦 Strict equality check:', result?.success === true)
// Check if response is successful
if (result?.success !== true) {
console.error('❌ Response indicates failure:', result)
throw new Error('Repository attachment failed')
}
const isPrivate = result?.data?.is_public === false
console.log('✅ Repository attached successfully:', result)
const repoType = isPrivate ? 'private' : 'public'
alert(`Repository attached successfully! (${repoType}) You can now proceed with your project.`)
setShowCreateOptionDialog(false)
setShowGitForm(false)
setGitProvider('')
setGitUrl('')
setGitBranch('main')
} catch (err: any) {
const status = err?.response?.status
let data = err?.response?.data
// Some proxies or middlewares may stringify JSON error bodies; handle that here
if (typeof data === 'string') {
try { data = JSON.parse(data) } catch {}
}
console.log('❌ Error attaching repository:', {
status,
data,
message: err?.message,
code: err?.code,
url: gitUrl.trim()
})
if (status === 401 && (data?.requires_auth || data?.auth_url || data?.service_auth_url)) {
console.log('🔐 Private repository detected - initiating OAuth with repository context')
// Reset loading state before redirect
setIsGeneratingAuth(false)
// Detect provider from URL
const detectedProvider = detectProvider(gitUrl.trim());
// Use the universal OAuth helper that will auto-attach the repo after authentication
setTimeout(() => {
connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => {
console.error('OAuth initiation failed:', oauthError)
alert(`Failed to initiate ${detectedProvider.toUpperCase()} authentication. Please try again.`)
})
}, 100)
return
}
if (status === 403) {
// Reset loading state before showing dialog
setIsGeneratingAuth(false)
// Repository not accessible with current account - prompt to re-authenticate
setTimeout(() => {
// Detect provider from URL
const detectedProvider = detectProvider(gitUrl.trim());
const confirmReauth = confirm(`Repository not accessible with your current ${detectedProvider.toUpperCase()} account.\n\nThis could mean:\n- The repository belongs to a different ${detectedProvider.toUpperCase()} account\n- Your token expired or lacks permissions\n\nWould you like to re-authenticate with ${detectedProvider.toUpperCase()}?`)
if (confirmReauth) {
console.log(`🔐 Re-authenticating with ${detectedProvider.toUpperCase()} for private repository access`)
connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => {
console.error('OAuth initiation failed:', oauthError)
alert(`Failed to initiate ${detectedProvider.toUpperCase()} authentication. Please try again.`)
})
}
}, 100)
return
}
if (status === 404) {
alert('Repository not found - please check the URL and try again')
return
}
console.error('❌ Full error details:', err)
const errorMessage = data?.message || err?.message || 'Failed to attach repository. Please check the URL and try again.'
alert(errorMessage)
} finally {
setIsGeneratingAuth(false)
}
}
// Authentication handlers for different providers // Authentication handlers for different providers
const handleOAuthAuth = async (provider: string) => { const handleOAuthAuth = async (provider: string) => {
@ -309,13 +404,13 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
oauthEndpoint: '/api/vcs/gitlab/auth/start', oauthEndpoint: '/api/vcs/gitlab/auth/start',
apiEndpoint: 'https://gitlab.com/api/v4' apiEndpoint: 'https://gitlab.com/api/v4'
}, },
other: { gitea: {
name: 'Other Git', name: 'Gitea',
icon: 'M12 0C5.374 0 0 5.373 0 12s5.374 12 12 12 12-5.373 12-12S18.626 0 12 0zm0 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-6h2v2h-2v-2zm0-8h2v6h-2V8z', icon: 'M12 0C5.374 0 0 5.373 0 12s5.374 12 12 12 12-5.373 12-12S18.626 0 12 0zm0 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-6h2v2h-2v-2zm0-8h2v6h-2V8z',
placeholder: 'https://your-git-server.com/org/repo.git', placeholder: 'https://gitea.com/org/repo.git',
authMethods: ['username_password', 'token', 'ssh'], authMethods: ['token', 'oauth', 'ssh'],
oauthEndpoint: null, oauthEndpoint: '/api/vcs/gitea/auth/start',
apiEndpoint: null apiEndpoint: 'https://gitea.com/api/v1'
} }
} }
const [deletingTemplate, setDeletingTemplate] = useState<DatabaseTemplate | null>(null) const [deletingTemplate, setDeletingTemplate] = useState<DatabaseTemplate | null>(null)
@ -331,7 +426,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
const [descDialogData, setDescDialogData] = useState<{ title: string; description: string }>({ title: '', description: '' }) const [descDialogData, setDescDialogData] = useState<{ title: string; description: string }>({ title: '', description: '' })
const { const {
user, user: templateUser,
combined, combined,
loading, loading,
error, error,
@ -529,7 +624,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
is_custom: true, is_custom: true,
source: 'custom', source: 'custom',
// Attach user id so custom template is associated with creator // Attach user id so custom template is associated with creator
user_id: (user as any)?.id, user_id: (templateUser as any)?.id,
} as any); } as any);
setShowCustomForm(false); setShowCustomForm(false);
return created; return created;
@ -552,13 +647,62 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
console.log('🔍 [handleCreateFromGit] Detected provider:', detectedProvider); console.log('🔍 [handleCreateFromGit] Detected provider:', detectedProvider);
console.log('🔍 [handleCreateFromGit] Git URL:', gitUrl.trim()); console.log('🔍 [handleCreateFromGit] Git URL:', gitUrl.trim());
// Check if repository already exists for this user
try {
const existingReposResponse = await authApiClient.get(`/api/vcs/${detectedProvider}/repositories?t=${Date.now()}`, {
headers: { 'x-user-id': user?.id }
});
if (existingReposResponse.data?.success) {
const repositories = existingReposResponse.data.data || [];
const existingRepo = repositories.find((r: any) => r.repository_url === gitUrl.trim());
if (existingRepo) {
alert(`✅ Repository already exists!\n\nProvider: ${detectedProvider.toUpperCase()}\nRepository: ${gitUrl.trim()}\nStatus: ${existingRepo.sync_status}\n\nThis repository is already available in your repositories list.`);
setShowCreateOptionDialog(false);
setShowGitForm(false);
return;
}
}
} catch (error) {
console.log('🔍 [handleCreateFromGit] Could not check existing repositories, proceeding with attachment:', error);
// Continue with attachment even if we can't check existing repos
}
// Attach the repository via backend (skip template creation) // Attach the repository via backend (skip template creation)
try { try {
await attachRepository({ const attachResult = await attachRepository({
repository_url: gitUrl.trim(), repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined), branch_name: (gitBranch?.trim() || undefined),
}) })
// Check if authentication is required
if (attachResult.requires_auth) {
console.log('🔐 Private repository detected, auto-redirecting to OAuth:', attachResult)
// Store the auth_url for OAuth redirect
try {
sessionStorage.setItem('pending_git_attach', JSON.stringify({
repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined),
provider: detectedProvider,
auth_url: attachResult.auth_url
}))
console.log('💾 Stored pending git attach with auth_url in sessionStorage')
} catch (e) {
console.warn('⚠️ Failed to store pending git attach:', e)
}
// Auto-redirect to OAuth
console.log('🔐 Private repository detected - auto-redirecting to OAuth:', attachResult.auth_url)
if (attachResult.auth_url) {
window.location.replace(attachResult.auth_url)
} else {
alert('Authentication URL not available. Please try again.')
}
return
}
// Show success message and reset form // Show success message and reset form
alert('Repository attached successfully! You can now proceed with your project.'); alert('Repository attached successfully! You can now proceed with your project.');
setShowCreateOptionDialog(false) setShowCreateOptionDialog(false)
@ -588,28 +732,33 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
const data = err?.response?.data const data = err?.response?.data
console.log('🔍 HandleCreateFromGit error response:', { status, data }) console.log('🔍 HandleCreateFromGit error response:', { status, data })
console.log('🔍 Error details:', {
message: err?.message,
requires_auth: data?.requires_auth,
auth_url: data?.auth_url
})
console.log('🔍 Full error object:', err)
console.log('🔍 Error response object:', err?.response)
// If backend signals auth required, redirect to provider OAuth // If backend signals auth required, show authentication button instead of auto-redirect
if ((status === 401 || status === 200) && (data?.requires_auth || data?.message?.includes('authentication'))) { if ((status === 401 || status === 200) && (data?.requires_auth || data?.message?.includes('authentication'))) {
const authUrl: string = data?.auth_url console.log('🔐 Private repository detected, showing authentication button:', { status, requires_auth: data?.requires_auth, message: data?.message })
if (!authUrl) {
alert('Authentication URL is missing.');
return
}
// Persist pending repo so we resume after OAuth callback // Store the auth_url for when user clicks the authenticate button
try { try {
sessionStorage.setItem('pending_git_attach', JSON.stringify({ sessionStorage.setItem('pending_git_attach', JSON.stringify({
repository_url: gitUrl.trim(), repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined), branch_name: (gitBranch?.trim() || undefined),
provider: detectedProvider provider: detectedProvider,
auth_url: data?.auth_url
})) }))
} catch {} console.log('💾 Stored pending git attach with auth_url in sessionStorage')
} catch (e) {
console.warn('⚠️ Failed to store pending git attach:', e)
}
console.log(`🔐 Redirecting to ${detectedProvider.toUpperCase()} OAuth for repository attachment:`, authUrl) // Don't auto-redirect, let the user click the authenticate button
console.log(`🔐 Provider being passed to connectProvider:`, detectedProvider) console.log('🔐 Private repository detected - user must click authenticate button')
// Use connectProvider function to handle OAuth flow properly
await connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim())
return return
} }
@ -618,6 +767,20 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
return return
} }
// Fallback: if we have an auth_url in the error, try to redirect anyway
if (data?.auth_url && !data?.success) {
console.log('🔐 Fallback OAuth redirect - auth_url found in error response:', data.auth_url)
window.location.replace(data.auth_url)
return
}
// Additional fallback: check if the error message contains auth_url
if (err?.message && err.message.includes('authentication') && data?.auth_url) {
console.log('🔐 Additional fallback OAuth redirect - authentication message with auth_url:', data.auth_url)
window.location.replace(data.auth_url)
return
}
if (status === 404) { if (status === 404) {
alert('Repository not found - please check the URL and try again') alert('Repository not found - please check the URL and try again')
return return
@ -814,13 +977,50 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
return ( return (
<div className="max-w-7xl mx-auto space-y-6"> <div className="max-w-7xl mx-auto space-y-6">
{/* Sync Progress Notification */}
{syncProgress?.show && (
<div className="bg-orange-500/10 border border-orange-500/20 rounded-lg p-4 mb-6">
<div className="flex items-center space-x-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-orange-500"></div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-white">
Syncing {syncProgress.provider.toUpperCase()} Repository
</h3>
<p className="text-white/80 text-sm">
{syncProgress.stage}
</p>
<div className="mt-2 text-xs text-white/60">
Repository: {syncProgress.repositoryUrl}
{syncProgress.branchName !== 'main' && ` | Branch: ${syncProgress.branchName}`}
</div>
</div>
<div className="flex items-center space-x-2">
<div className={`px-2 py-1 rounded text-xs font-medium ${
syncProgress.status === 'synced' ? 'bg-green-500/20 text-green-400' :
syncProgress.status === 'error' ? 'bg-red-500/20 text-red-400' :
'bg-orange-500/20 text-orange-400'
}`}>
{syncProgress.status}
</div>
<button
onClick={() => setSyncProgress(null)}
className="text-white/60 hover:text-white/80 transition-colors"
title="Dismiss sync progress"
>
</button>
</div>
</div>
</div>
)}
{/* Header */} {/* Header */}
<div className="text-center space-y-3"> <div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Choose Your Project Template</h1> <h1 className="text-4xl font-bold text-white">Choose Your Project Template</h1>
<p className="text-xl text-white/60 max-w-3xl mx-auto"> <p className="text-xl text-white/60 max-w-3xl mx-auto">
Select from our comprehensive library of professionally designed templates Select from our comprehensive library of professionally designed templates
</p> </p>
{/* Connection Error Banner */} {/* Connection Error Banner */}
{connectionError && ( {connectionError && (
<div className="max-w-2xl mx-auto"> <div className="max-w-2xl mx-auto">
@ -844,7 +1044,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
/> />
</div> </div>
)} )}
{!user?.id && ( {!templateUser?.id && (
<div className="bg-orange-500/10 border border-orange-500/20 rounded-lg p-4 max-w-2xl mx-auto"> <div className="bg-orange-500/10 border border-orange-500/20 rounded-lg p-4 max-w-2xl mx-auto">
<p className="text-orange-300 text-sm"> <p className="text-orange-300 text-sm">
You&apos;re currently viewing public templates. You&apos;re currently viewing public templates.
@ -867,12 +1067,25 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
{/* Right-aligned quick navigation to user repos */} {/* Right-aligned quick navigation to user repos */}
<div className="flex justify-end space-x-2"> <div className="flex justify-end space-x-2">
<ViewUserReposButton className="bg-orange-500 hover:bg-orange-400 text-black" label="My GitHub Repos" /> <ViewUserReposButton className="bg-orange-500 hover:bg-orange-400 text-black" label="My GitHub Repos" />
<Link href="/vcs/repos"> {templateUser?.id ? (
<Button className="bg-blue-500 hover:bg-blue-400 text-white"> <Link href="/vcs/repos">
<Button className="bg-blue-500 hover:bg-blue-400 text-white">
<FolderGit2 className="mr-2 h-5 w-5" />
All My Repos
</Button>
</Link>
) : (
<Button
onClick={() => {
const returnUrl = encodeURIComponent('/vcs/repos');
window.location.href = `/signin?returnUrl=${returnUrl}`;
}}
className="bg-blue-500 hover:bg-blue-400 text-white"
>
<FolderGit2 className="mr-2 h-5 w-5" /> <FolderGit2 className="mr-2 h-5 w-5" />
All My Repos All My Repos
</Button> </Button>
</Link> )}
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
@ -1215,7 +1428,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
</div> </div>
<Card className="group border-dashed border-2 border-white/15 bg-white/5 hover:border-white/25 transition-all cursor-pointer" onClick={() => { <Card className="group border-dashed border-2 border-white/15 bg-white/5 hover:border-white/25 transition-all cursor-pointer" onClick={() => {
if (!user?.id) { window.location.href = '/signin'; return } if (!templateUser?.id) { window.location.href = '/signin'; return }
setShowCreateOptionDialog(true) setShowCreateOptionDialog(true)
}}> }}>
<CardContent className="text-center py-16 px-8 text-white/80"> <CardContent className="text-center py-16 px-8 text-white/80">
@ -1223,20 +1436,20 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
<Plus className="h-10 w-10 text-orange-400" /> <Plus className="h-10 w-10 text-orange-400" />
</div> </div>
<h3 className="text-2xl font-bold text-white mb-3"> <h3 className="text-2xl font-bold text-white mb-3">
{user?.id ? 'Create Custom Template' : 'Sign In to Create Templates'} {templateUser?.id ? 'Create Custom Template' : 'Sign In to Create Templates'}
</h3> </h3>
<p className="mb-8 max-w-md mx-auto text-lg leading-relaxed"> <p className="mb-8 max-w-md mx-auto text-lg leading-relaxed">
{user?.id {templateUser?.id
? "Don't worry, we'll guide you through each step. a custom project type with your specific requirements and tech stack." ? "Don't worry, we'll guide you through each step. a custom project type with your specific requirements and tech stack."
: "Sign in to create custom project templates with your specific requirements and tech stack." : "Sign in to create custom project templates with your specific requirements and tech stack."
} }
</p> </p>
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={(e) => { <Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={(e) => {
e.stopPropagation() e.stopPropagation()
if (!user?.id) { window.location.href = '/signin'; return } if (!templateUser?.id) { window.location.href = '/signin'; return }
setShowCreateOptionDialog(true) setShowCreateOptionDialog(true)
}}> }}>
{user?.id ? ( {templateUser?.id ? (
<> <>
<Plus className="mr-2 h-5 w-5" /> <Plus className="mr-2 h-5 w-5" />
Create Custom Template Create Custom Template
@ -1293,7 +1506,6 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
setGitAuthMethod('') setGitAuthMethod('')
setGitCredentials({ username: '', password: '', token: '', sshKey: '' }) setGitCredentials({ username: '', password: '', token: '', sshKey: '' })
setGitStep('provider') setGitStep('provider')
setAuthUrl('')
} }
}}> }}>
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton> <DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
@ -1363,7 +1575,9 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
<label className="block text-sm text-white/70 mb-1">Repository URL</label> <label className="block text-sm text-white/70 mb-1">Repository URL</label>
<Input <Input
value={gitUrl} value={gitUrl}
onChange={(e) => setGitUrl(e.target.value)} onChange={(e) => {
setGitUrl(e.target.value)
}}
placeholder={gitProviders[gitProvider as keyof typeof gitProviders]?.placeholder} placeholder={gitProviders[gitProvider as keyof typeof gitProviders]?.placeholder}
className="bg-white/10 border-white/20 text-white" className="bg-white/10 border-white/20 text-white"
/> />
@ -1385,62 +1599,6 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
</p> </p>
</div> </div>
{/* Authentication URL Generation: show only if not already connected */}
{gitUrl.trim() && isGithubConnected === false && (
<div className="space-y-3">
<div className="p-4 bg-blue-500/10 border border-blue-500/20 rounded-lg">
<div className="flex items-center gap-2 text-blue-200 text-sm mb-3">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
Ready to authenticate with {gitProviders[gitProvider as keyof typeof gitProviders]?.name}
</div>
<p className="text-xs text-blue-200/80 mb-3">
Click the button below to generate an authentication URL. This will open a new window where you can securely authenticate with your {gitProviders[gitProvider as keyof typeof gitProviders]?.name} account.
</p>
<Button
className="w-full bg-blue-500 hover:bg-blue-400 text-white"
type="button"
onClick={(e) => { e.preventDefault(); generateAuthUrl(); }}
disabled={isGeneratingAuth || !gitUrl.trim()}
>
{isGeneratingAuth ? (
<div className="flex items-center gap-2">
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Generating Auth URL...
</div>
) : (
`Authenticate with ${gitProviders[gitProvider as keyof typeof gitProviders]?.name}`
)}
</Button>
</div>
{authUrl && (
<div className="p-3 bg-green-500/10 border border-green-500/20 rounded-lg">
<div className="flex items-center gap-2 text-green-200 text-sm mb-2">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Authentication URL Generated
</div>
<p className="text-xs text-green-200/80 mb-2">
If the authentication window didn't open, you can click the link below:
</p>
<a
href={authUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-300 hover:text-blue-200 underline break-all"
>
{authUrl}
</a>
</div>
)}
</div>
)}
<div className="flex gap-3 justify-end pt-2"> <div className="flex gap-3 justify-end pt-2">
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={() => setGitStep('provider')}>Back</Button> <Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={() => setGitStep('provider')}>Back</Button>
@ -1448,7 +1606,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
className="bg-orange-500 hover:bg-orange-400 text-black" className="bg-orange-500 hover:bg-orange-400 text-black"
type="button" type="button"
onClick={(e) => { e.preventDefault(); handleCreateFromGit(); }} onClick={(e) => { e.preventDefault(); handleCreateFromGit(); }}
disabled={!gitUrl.trim() || (isGithubConnected === false && !authUrl)} disabled={!gitUrl.trim()}
> >
Import Template Import Template
</Button> </Button>

View File

@ -50,6 +50,22 @@ export async function attachRepository(payload: AttachRepositoryPayload, retries
const provider = detectProvider(payload.repository_url); const provider = detectProvider(payload.repository_url);
console.log('🔍 [attachRepository] Detected provider:', provider); console.log('🔍 [attachRepository] Detected provider:', provider);
// First, try to check if user already has a valid token for this provider
if (userId) {
try {
console.log(`🔍 [attachRepository] Checking for existing ${provider.toUpperCase()} token for user:`, userId);
const tokenCheckResponse = await authApiClient.get(`/api/vcs/${provider}/repositories`, {
headers: { 'x-user-id': userId }
});
if (tokenCheckResponse.data?.success) {
console.log(`✅ [attachRepository] Found existing ${provider.toUpperCase()} token, proceeding with attachment`);
}
} catch (tokenError) {
console.log(`🔍 [attachRepository] No existing ${provider.toUpperCase()} token found, will require OAuth`);
}
}
const url = userId const url = userId
? `/api/vcs/${provider}/attach-repository?user_id=${encodeURIComponent(userId)}` ? `/api/vcs/${provider}/attach-repository?user_id=${encodeURIComponent(userId)}`
: `/api/vcs/${provider}/attach-repository`; : `/api/vcs/${provider}/attach-repository`;
@ -82,6 +98,18 @@ export async function attachRepository(payload: AttachRepositoryPayload, retries
success: (parsed?.success === true || parsed?.success === 'true') success: (parsed?.success === true || parsed?.success === 'true')
}; };
// If authentication is required, return the response instead of throwing error
if (normalized.requires_auth || (normalized.message && normalized.message.includes('authentication'))) {
console.log('🔐 [attachRepository] Authentication required, returning response for UI to show authenticate button:', {
requires_auth: normalized.requires_auth,
message: normalized.message,
auth_url: normalized.auth_url
});
// Return the response so UI can show authenticate button
return normalized;
}
return normalized; return normalized;
} catch (error: any) { } catch (error: any) {
// If it's the last retry or not a connection error, throw immediately // If it's the last retry or not a connection error, throw immediately