From 69def8560b2e5fee0e844bb4b10cbc7242446470 Mon Sep 17 00:00:00 2001 From: Chandini Date: Fri, 26 Sep 2025 17:05:02 +0530 Subject: [PATCH] frontend changes --- src/components/apis/authApiClients.tsx | 20 ++ src/components/main-dashboard.tsx | 437 ++++++++++++++++++++++++- src/config/backend.ts | 2 +- src/lib/api/github.ts | 49 +++ 4 files changed, 505 insertions(+), 3 deletions(-) create mode 100644 src/lib/api/github.ts diff --git a/src/components/apis/authApiClients.tsx b/src/components/apis/authApiClients.tsx index 71c8394..15eba34 100644 --- a/src/components/apis/authApiClients.tsx +++ b/src/components/apis/authApiClients.tsx @@ -95,6 +95,19 @@ const addAuthTokenInterceptor = (client: typeof authApiClient) => { (config) => { // Always get fresh token from localStorage instead of using module variable const freshToken = getAccessToken(); + // Attach user_id for backend routing that requires it + try { + const rawUser = safeLocalStorage.getItem('codenuk_user'); + if (rawUser) { + const parsed = JSON.parse(rawUser); + const userId = parsed?.id; + if (userId) { + config.headers = config.headers || {}; + // Header preferred by backend + (config.headers as any)['x-user-id'] = userId; + } + } + } catch (_) {} if (freshToken) { config.headers = config.headers || {}; config.headers.Authorization = `Bearer ${freshToken}`; @@ -110,6 +123,13 @@ const addTokenRefreshInterceptor = (client: typeof authApiClient) => { client.interceptors.response.use( (response) => response, async (error) => { + // Surface detailed server error info in the console for debugging + try { + const status = error?.response?.status + const data = error?.response?.data + console.error('🛑 API error:', { url: error?.config?.url, method: error?.config?.method, status, data }) + } catch (_) {} + const originalRequest = error.config; const isRefreshEndpoint = originalRequest?.url?.includes('/api/auth/refresh'); if (error.response?.status === 401 && !originalRequest._retry && !isRefreshEndpoint) { diff --git a/src/components/main-dashboard.tsx b/src/components/main-dashboard.tsx index 96f1a1a..f6d06ca 100644 --- a/src/components/main-dashboard.tsx +++ b/src/components/main-dashboard.tsx @@ -22,6 +22,7 @@ import PromptSidePanel from "@/components/prompt-side-panel" import { DualCanvasEditor } from "@/components/dual-canvas-editor" import { getAccessToken } from "@/components/apis/authApiClients" import TechStackSummary from "@/components/tech-stack-summary" +import { attachRepository, getGitHubAuthStatus } from "@/lib/api/github" interface Template { id: string @@ -51,7 +52,188 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi const [selectedCategory, setSelectedCategory] = useState("all") const [searchQuery, setSearchQuery] = useState("") const [showCustomForm, setShowCustomForm] = useState(false) + const [showCreateOptionDialog, setShowCreateOptionDialog] = useState(false) + const [showGitForm, setShowGitForm] = useState(false) + const [gitProvider, setGitProvider] = useState('') + const [gitUrl, setGitUrl] = useState('') + const [gitBranch, setGitBranch] = useState('main') + const [gitAuthMethod, setGitAuthMethod] = useState('') + const [gitCredentials, setGitCredentials] = useState({ + username: '', + password: '', + token: '', + sshKey: '' + }) + const [authLoading, setAuthLoading] = useState(false) + const [gitStep, setGitStep] = useState<'provider' | 'url'>('provider') + const [authUrl, setAuthUrl] = useState('') + const [isGeneratingAuth, setIsGeneratingAuth] = useState(false) + const [isGithubConnected, setIsGithubConnected] = useState(null) + useEffect(() => { + (async () => { + try { + const status = await getGitHubAuthStatus() + setIsGithubConnected(!!status?.data?.connected) + } catch { + setIsGithubConnected(false) + } + })() + }, []) const [editingTemplate, setEditingTemplate] = useState(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 {} + + await attachRepository({ + repository_url: gitUrl.trim(), + branch_name: (gitBranch?.trim() || undefined), + }) + // If we reach here without 401, repo is public and attached. Nothing to auth. + } catch (err: any) { + const status = err?.response?.status + const data = err?.response?.data + if (status === 401 && (data?.requires_auth || data?.auth_url)) { + const url: string = data?.auth_url + if (url) { + window.location.replace(url) + return + } + } + console.error('Error generating auth URL via attach:', err) + alert(data?.message || 'Failed to generate authentication URL. Please try again.') + } finally { + setIsGeneratingAuth(false) + } + } + + // Authentication handlers for different providers + const handleOAuthAuth = async (provider: string) => { + setAuthLoading(true) + try { + const providerConfig = gitProviders[provider as keyof typeof gitProviders] + if (!providerConfig.oauthEndpoint) { + throw new Error('OAuth not supported for this provider') + } + + // Redirect to OAuth endpoint + window.open(providerConfig.oauthEndpoint, '_blank', 'width=600,height=700') + + // In a real implementation, you'd handle the OAuth callback + // and store the access token + alert(`Redirecting to ${providerConfig.name} OAuth...`) + } catch (error) { + console.error('OAuth error:', error) + alert('OAuth authentication failed') + } finally { + setAuthLoading(false) + } + } + + const handleTokenAuth = async (provider: string, token: string) => { + setAuthLoading(true) + try { + const providerConfig = gitProviders[provider as keyof typeof gitProviders] + + // Validate token with provider API + const response = await fetch(`${providerConfig.apiEndpoint}/user`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }) + + if (!response.ok) { + throw new Error('Invalid token') + } + + const userData = await response.json() + alert(`Authenticated as ${userData.login || userData.username || userData.name}`) + return true + } catch (error) { + console.error('Token auth error:', error) + alert('Token authentication failed') + return false + } finally { + setAuthLoading(false) + } + } + + const handleUsernamePasswordAuth = async (provider: string, username: string, password: string) => { + setAuthLoading(true) + try { + // For username/password auth, you'd typically use Basic Auth + // or convert to token-based auth + const credentials = btoa(`${username}:${password}`) + + const response = await fetch(`${gitProviders[provider as keyof typeof gitProviders].apiEndpoint}/user`, { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Accept': 'application/json' + } + }) + + if (!response.ok) { + throw new Error('Invalid credentials') + } + + const userData = await response.json() + alert(`Authenticated as ${userData.login || userData.username || userData.name}`) + return true + } catch (error) { + console.error('Username/password auth error:', error) + alert('Authentication failed') + return false + } finally { + setAuthLoading(false) + } + } + + // Provider-specific configuration + const gitProviders = { + github: { + name: 'GitHub', + icon: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z', + placeholder: 'https://github.com/org/repo.git', + authMethods: ['token', 'ssh', 'oauth'], + oauthEndpoint: '/api/auth/github', + apiEndpoint: 'https://api.github.com' + }, + bitbucket: { + name: 'Bitbucket', + icon: 'M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891L.778 1.213zM14.518 18.197H9.482l-1.4-8.893h7.436l-1.4 8.893z', + placeholder: 'https://bitbucket.org/org/repo.git', + authMethods: ['username_password', 'app_password', 'oauth'], + oauthEndpoint: '/api/auth/bitbucket', + apiEndpoint: 'https://api.bitbucket.org/2.0' + }, + gitlab: { + name: 'GitLab', + icon: 'M23.6004 9.5927l-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.874.874 0 0 0-.9997.0539.874.874 0 0 0-.29.4399l-2.5465 7.7838H7.2162l-2.5465-7.7838a.857.857 0 0 0-.29-.4412.874.874 0 0 0-.9997-.0537.858.858 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.065 6.065 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.008 1.008 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7466.0125-.01a6.068 6.068 0 0 0 2.0094-7.003z', + placeholder: 'https://gitlab.com/org/repo.git', + authMethods: ['token', 'oauth', 'ssh'], + oauthEndpoint: '/api/auth/gitlab', + apiEndpoint: 'https://gitlab.com/api/v4' + }, + other: { + name: 'Other Git', + 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', + authMethods: ['username_password', 'token', 'ssh'], + oauthEndpoint: null, + apiEndpoint: null + } + } const [deletingTemplate, setDeletingTemplate] = useState(null) const [deleteLoading, setDeleteLoading] = useState(false) // Keep a stable list of all categories seen so the filter chips don't disappear @@ -274,6 +456,74 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi } }; + const handleCreateFromGit = async () => { + try { + if (!gitUrl.trim()) { + alert('Please enter a Git repository URL'); + return; + } + // Attach the repository via backend (skip template creation) + try { + await attachRepository({ + repository_url: gitUrl.trim(), + branch_name: (gitBranch?.trim() || undefined), + }) + + // Show success message and reset form + alert('Repository attached successfully! You can now proceed with your project.'); + setShowCreateOptionDialog(false) + setShowGitForm(false) + setGitProvider('') + setGitUrl('') + setGitBranch('main') + + // Return a mock template object to proceed to next step + return { + id: 'git-imported', + title: `Imported from ${gitProvider === 'other' ? 'Git' : gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1)}`, + description: `Template imported from ${gitProvider === 'other' ? 'Git' : gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1)}: ${gitUrl}`, + type: 'custom', + category: 'imported', + is_custom: true, + source: 'git', + git_url: gitUrl.trim(), + git_branch: gitBranch?.trim() || 'main', + git_provider: gitProvider + } + } catch (attachErr) { + console.error('[TemplateSelectionStep] attachRepository failed:', attachErr) + // If backend signals GitHub auth required, open the OAuth URL for this user + try { + const err: any = attachErr + const status = err?.response?.status + const data = err?.response?.data + if (status === 401 && (data?.requires_auth || data?.message?.includes('authentication'))) { + // Use the exact URL provided by backend (already includes redirect=1 and user_id when needed) + const url: string = data?.auth_url + if (!url) { alert('Authentication URL is missing.'); return } + // Persist pending repo so we resume after OAuth callback + try { + sessionStorage.setItem('pending_git_attach', JSON.stringify({ + repository_url: gitUrl.trim(), + branch_name: (gitBranch?.trim() || undefined) + })) + } catch {} + // Force same-tab redirect directly to GitHub consent screen + window.location.replace(url) + return + } + } catch {} + alert('Failed to attach repository. Please verify the URL/branch and your auth.'); + return; + } + } catch (error) { + console.error('[TemplateSelectionStep] Error importing from Git:', error) + const errorMessage = error instanceof Error ? error.message : 'Failed to import from Git' + alert(`Error: ${errorMessage}`) + throw error as Error + } + } + const handleUpdateTemplate = async (id: string, templateData: Partial) => { try { // Find the template to determine if it's custom @@ -819,7 +1069,10 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi - user?.id ? setShowCustomForm(true) : window.location.href = '/signin'}> + { + if (!user?.id) { window.location.href = '/signin'; return } + setShowCreateOptionDialog(true) + }}>
@@ -833,7 +1086,11 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi : "Sign in to create custom project templates with your specific requirements and tech stack." }

- + +
+ ) : gitStep === 'provider' ? ( +
+
+ +
+ {Object.entries(gitProviders).map(([key, provider]) => ( + + ))} +
+
+
+ +
+
+ ) : ( +
+
+ + + Import from {gitProviders[gitProvider as keyof typeof gitProviders]?.name} + +
+ +
+ + setGitUrl(e.target.value)} + placeholder={gitProviders[gitProvider as keyof typeof gitProviders]?.placeholder} + className="bg-white/10 border-white/20 text-white" + /> +

+ Enter the full URL to your {gitProviders[gitProvider as keyof typeof gitProviders]?.name} repository +

+
+ +
+ + setGitBranch(e.target.value)} + placeholder="main" + className="bg-white/10 border-white/20 text-white" + /> +

+ Leave empty to use the default branch +

+
+ + {/* Authentication URL Generation: show only if not already connected */} + {gitUrl.trim() && isGithubConnected === false && ( +
+
+
+ + + + Ready to authenticate with {gitProviders[gitProvider as keyof typeof gitProviders]?.name} +
+

+ 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. +

+ +
+ + {authUrl && ( +
+
+ + + + Authentication URL Generated +
+

+ If the authentication window didn't open, you can click the link below: +

+ + {authUrl} + +
+ )} +
+ )} + +
+ + +
+
+ )} + + ) } diff --git a/src/config/backend.ts b/src/config/backend.ts index 62e8f87..aea0e9a 100644 --- a/src/config/backend.ts +++ b/src/config/backend.ts @@ -2,7 +2,7 @@ // // export const BACKEND_URL = 'https://backend.codenuk.com'; -export const BACKEND_URL = 'http://192.168.1.13:8000'; +export const BACKEND_URL = 'http://localhost:8000'; export const SOCKET_URL = BACKEND_URL; diff --git a/src/lib/api/github.ts b/src/lib/api/github.ts new file mode 100644 index 0000000..9eced34 --- /dev/null +++ b/src/lib/api/github.ts @@ -0,0 +1,49 @@ +import { authApiClient } from '@/components/apis/authApiClients' + +export interface AttachRepositoryPayload { + repository_url: string + branch_name?: string + user_id?: string +} + +export interface AttachRepositoryResponse { + success: boolean + message?: string + data?: T + requires_auth?: boolean + auth_url?: string + auth_error?: boolean +} + +export async function attachRepository(payload: AttachRepositoryPayload): Promise { + // Add user_id as query fallback besides header for gateway caching/proxies + const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null + const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null + const url = userId ? `/api/github/attach-repository?user_id=${encodeURIComponent(userId)}` : '/api/github/attach-repository' + const response = await authApiClient.post(url, { ...payload, user_id: userId || payload.user_id }, { + headers: { + 'Content-Type': 'application/json', + }, + }) + return response.data as AttachRepositoryResponse +} + +export interface GitHubAuthStatusData { + connected: boolean + github_username?: string + github_user_id?: string + connected_at?: string + scopes?: string[] + requires_auth?: boolean + auth_url?: string +} + +export async function getGitHubAuthStatus(): Promise> { + const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null + const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null + const url = userId ? `/api/github/auth/github/status?user_id=${encodeURIComponent(userId)}` : '/api/github/auth/github/status' + const response = await authApiClient.get(url) + return response.data as AttachRepositoryResponse +} + +