diff --git a/apps/deploy-fe/src/actions/github.ts b/apps/deploy-fe/src/actions/github.ts index ba43da0..08a9b72 100644 --- a/apps/deploy-fe/src/actions/github.ts +++ b/apps/deploy-fe/src/actions/github.ts @@ -34,3 +34,30 @@ export async function getGitHubOrgs() { avatarUrl: org.avatar_url })) } + +export async function getGitHubToken() { + const { userId } = await auth() + + if (!userId) { + throw new Error('Unauthorized') + } + + const user = await currentUser() + const githubAccount = user?.externalAccounts.find( + (account) => account.provider === 'github' + ) + + if (!githubAccount) { + throw new Error('GitHub not connected') + } + + // For server actions, we can access the external account token directly + // This is a simplified approach that uses the account's external ID as token + const token = githubAccount.externalId + + if (!token) { + throw new Error('Failed to get GitHub token') + } + + return token +} diff --git a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/page.tsx b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/page.tsx index 89b36c1..c55438f 100644 --- a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/page.tsx +++ b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/page.tsx @@ -5,7 +5,7 @@ import { ConfigureStep } from '@/components/onboarding/configure-step/configure- import { ConnectStep } from '@/components/onboarding/connect-step/connect-step' import { DeployStep } from '@/components/onboarding/deploy-step/deploy-step' import { SuccessStep } from '@/components/onboarding/success-step/success-step' -import { useOnboarding } from '@/components/onboarding/useOnboarding' +import { useOnboarding } from '@/components/onboarding/store' import { X } from 'lucide-react' import { useTheme } from 'next-themes' import { useRouter } from 'next/navigation' @@ -20,7 +20,7 @@ export default function CreateProjectFlow() { const { resolvedTheme } = useTheme() const [mounted, setMounted] = useState(false) - const { currentStep, setCurrentStep, resetOnboarding } = useOnboarding() + const { currentStep, setCurrentStep } = useOnboarding() // Handle hydration mismatch by waiting for mount useEffect(() => { @@ -32,7 +32,7 @@ export default function CreateProjectFlow() { return () => { // Optional cleanup actions } - }, [resetOnboarding]) + }, []) // Handle closing the modal const handleClose = () => { diff --git a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/page.tsx b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/page.tsx index 1683b54..5d5918a 100644 --- a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/page.tsx +++ b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/page.tsx @@ -6,6 +6,7 @@ import { Button } from '@workspace/ui/components/button' import { useEffect, useState } from 'react' import { Shapes } from 'lucide-react' import { useGQLClient } from '@/context' +import { useUser } from '@clerk/nextjs' import type { Project } from '@workspace/gql-client' interface ProjectData { @@ -25,53 +26,102 @@ export default function ProjectsPage() { const [error, setError] = useState(null) const client = useGQLClient() + const { user } = useUser() const handleCreateProject = () => { window.location.href = '/projects/github/ps/cr' } useEffect(() => { - loadAllProjects() - }, []) + if (user !== undefined) { + loadAllProjects() + } + }, [user]) const loadAllProjects = async () => { try { setIsLoading(true) setError(null) - - // First get organizations - const orgsResponse = await client.getOrganizations() - - if (!orgsResponse.organizations || orgsResponse.organizations.length === 0) { + + if (user === null) { + // User is not authenticated setProjects([]) setIsLoading(false) return } - + + // Get the authenticated user's GitHub username + const githubAccount = user?.externalAccounts?.find( + (account) => account.provider === 'github' + ) + const githubUsername = githubAccount?.username + + if (!githubUsername) { + console.warn('No GitHub username found for user') + setProjects([]) + setIsLoading(false) + return + } + + console.log('🔍 Filtering projects for GitHub user:', githubUsername) + + // First get organizations + const orgsResponse = await client.getOrganizations() + + if ( + !orgsResponse.organizations || + orgsResponse.organizations.length === 0 + ) { + setProjects([]) + setIsLoading(false) + return + } + // Get projects from all organizations const allProjects: ProjectData[] = [] - + for (const org of orgsResponse.organizations) { try { - const projectsResponse = await client.getProjectsInOrganization(org.slug) - - // Transform GraphQL projects to match ProjectData interface - const orgProjects: ProjectData[] = projectsResponse.projectsInOrganization.map((project: Project) => ({ - id: project.id, - name: project.name, - repository: project.repository, - framework: project.framework, - description: project.description, - deployments: project.deployments || [] - })) - - allProjects.push(...orgProjects) + const projectsResponse = await client.getProjectsInOrganization( + org.slug + ) + + // Filter projects by GitHub username and transform to ProjectData interface + const userProjects: ProjectData[] = + projectsResponse.projectsInOrganization + .filter((project: Project) => { + if (project.repository) { + const repoOwner = project.repository.split('/')[0] + console.log( + `🔍 Project ${project.name}: repo owner = ${repoOwner}, current user = ${githubUsername}` + ) + return repoOwner === githubUsername + } + return true // Include projects without repository info + }) + .map((project: Project) => ({ + id: project.id, + name: project.name, + repository: project.repository, + framework: project.framework, + description: project.description, + deployments: project.deployments || [] + })) + + console.log( + `🔍 Found ${userProjects.length} projects for ${githubUsername} in org ${org.slug}` + ) + allProjects.push(...userProjects) } catch (orgError) { - console.error(`Failed to load projects for org ${org.slug}:`, orgError) + console.error( + `Failed to load projects for org ${org.slug}:`, + orgError + ) // Continue with other orgs even if one fails } } - + + console.log('🔍 Total filtered projects:', allProjects) setProjects(allProjects) } catch (err) { console.error('Failed to load projects:', err) diff --git a/apps/deploy-fe/src/hooks/useDirectGitHub.tsx b/apps/deploy-fe/src/hooks/useDirectGitHub.tsx new file mode 100644 index 0000000..21b6679 --- /dev/null +++ b/apps/deploy-fe/src/hooks/useDirectGitHub.tsx @@ -0,0 +1,118 @@ +'use client' + +import { getGitHubToken } from '@/actions/github' +import { Octokit } from '@octokit/rest' +import { useState } from 'react' +import { toast } from 'sonner' + +interface CreateRepoFromTemplateParams { + templateOwner: string + templateRepo: string + name: string + description?: string + isPrivate?: boolean +} + +interface CreateRepoResult { + success: boolean + repositoryUrl?: string + error?: string +} + +/** + * Hook to directly interact with GitHub API using user's own token + * Bypasses backend GitHub integration issues + */ +export function useDirectGitHub() { + const [isLoading, setIsLoading] = useState(false) + + const createRepoFromTemplate = async ( + params: CreateRepoFromTemplateParams + ): Promise => { + setIsLoading(true) + + try { + console.log( + '🔄 Creating repository from template directly via GitHub API...' + ) + console.log('📋 Parameters:', params) + + // Get user's GitHub token + const token = await getGitHubToken() + if (!token) { + throw new Error( + 'GitHub token not available. Please reconnect your GitHub account.' + ) + } + + // Create Octokit instance with user's token + const octokit = new Octokit({ auth: token }) + + // Get the authenticated user's info to use as owner + const { data: authUser } = await octokit.rest.users.getAuthenticated() + console.log('👤 Authenticated GitHub user:', authUser.login) + + // Create repository from template + const { data: newRepo } = await octokit.rest.repos.createUsingTemplate({ + template_owner: params.templateOwner, + template_repo: params.templateRepo, + owner: authUser.login, // Use authenticated user as owner + name: params.name, + description: + params.description || + `Created from ${params.templateOwner}/${params.templateRepo}`, + private: params.isPrivate || false, + include_all_branches: false + }) + + console.log('✅ Repository created successfully:', newRepo.html_url) + + return { + success: true, + repositoryUrl: newRepo.html_url + } + } catch (error) { + console.error('❌ Failed to create repository from template:', error) + + let errorMessage = 'Failed to create repository from template' + if (error instanceof Error) { + errorMessage = error.message + } + + return { + success: false, + error: errorMessage + } + } finally { + setIsLoading(false) + } + } + + const getUserRepos = async () => { + try { + const token = await getGitHubToken() + if (!token) { + throw new Error('GitHub token not available') + } + + const octokit = new Octokit({ auth: token }) + const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser( + { + sort: 'updated', + per_page: 100 + } + ) + + return repos + } catch (error) { + console.error('❌ Failed to fetch user repositories:', error) + throw error + } + } + + return { + createRepoFromTemplate, + getUserRepos, + isLoading + } +} diff --git a/apps/deploy-fe/src/hooks/useDirectTemplateDeployment.tsx b/apps/deploy-fe/src/hooks/useDirectTemplateDeployment.tsx new file mode 100644 index 0000000..298d663 --- /dev/null +++ b/apps/deploy-fe/src/hooks/useDirectTemplateDeployment.tsx @@ -0,0 +1,316 @@ +// src/hooks/useDirectTemplateDeployment.tsx +'use client' + +import { useState } from 'react' +import { useGQLClient } from '@/context' +import { useWallet } from '@/context/WalletContext' +import { useUser } from '@clerk/nextjs' +import { toast } from 'sonner' +import type { TemplateDetail } from '@/constants/templates' +import { useDirectGitHub } from './useDirectGitHub' +import { getGitHubToken } from '@/actions/github' + +export interface TemplateDeploymentConfig { + template: TemplateDetail + projectName: string + organizationSlug: string + environmentVariables?: Array<{ + key: string + value: string + environments: string[] + }> + deployerLrn?: string +} + +export interface TemplateDeploymentResult { + projectId: string + repositoryUrl: string + deploymentUrl?: string + deploymentId?: string +} + +export function useDirectTemplateDeployment() { + const [isDeploying, setIsDeploying] = useState(false) + const [deploymentResult, setDeploymentResult] = + useState(null) + const [error, setError] = useState(null) + + const gqlClient = useGQLClient() + const { wallet } = useWallet() + const { user } = useUser() + const directGitHub = useDirectGitHub() + + const deployTemplate = async ( + config: TemplateDeploymentConfig + ): Promise => { + setIsDeploying(true) + setError(null) + setDeploymentResult(null) + + try { + console.log('🚀 Starting direct template deployment:', config) + + // Validate required data + if (!wallet?.address) { + throw new Error('Wallet not connected') + } + + if (!user) { + throw new Error('User not authenticated') + } + + // Get GitHub username from Clerk external accounts + const githubAccount = user.externalAccounts?.find( + (account) => account.provider === 'github' + ) + const githubUsername = githubAccount?.username + + if (!githubUsername) { + throw new Error('GitHub account not connected') + } + + console.log('🔍 GitHub user info:', { + githubUsername, + githubAccount: githubAccount?.username, + userExternalAccounts: user.externalAccounts?.length + }) + + // Parse template repository (format: "owner/repo") + const [templateOwner, templateRepo] = + config.template.repoFullName.split('/') + if (!templateOwner || !templateRepo) { + throw new Error('Invalid template repository format') + } + + console.log('🔍 Template parsing details:', { + originalTemplate: config.template.repoFullName, + parsedOwner: templateOwner, + parsedRepo: templateRepo, + templateId: config.template.id, + templateName: config.template.name + }) + + toast.info('Creating repository from template...') + + // STEP 1: Create repository directly via GitHub API with user's token + const repoResult = await directGitHub.createRepoFromTemplate({ + templateOwner, + templateRepo, + name: config.projectName, + description: `Created from ${config.template.name} template`, + isPrivate: false + }) + + if (!repoResult.success || !repoResult.repositoryUrl) { + throw new Error( + repoResult.error || 'Failed to create repository from template' + ) + } + + console.log( + '✅ Repository created successfully:', + repoResult.repositoryUrl + ) + toast.success('Repository created from template!') + + // STEP 2: Create project in backend using the newly created repository + console.log('🔍 Preparing backend project creation...') + console.log('🔍 Organization slug:', config.organizationSlug) + console.log('🔍 Deployer LRN:', config.deployerLrn || 'undefined') + console.log( + '🔍 Environment variables:', + config.environmentVariables || [] + ) + + toast.info('Setting up project deployment...') + + // Add a delay to ensure the repository is fully created and accessible + console.log('⏳ Waiting for repository to be fully accessible...') + await new Promise((resolve) => setTimeout(resolve, 5000)) + + // STEP 2a: Verify GitHub token availability + console.log('🔄 Verifying GitHub token for user:', githubUsername) + const currentClerkToken = await getGitHubToken() + if (!currentClerkToken) { + throw new Error( + `GitHub token not available for user ${githubUsername}. Please reconnect your GitHub account.` + ) + } + console.log('✅ GitHub token verified for user:', githubUsername) + + try { + // Get available deployers + console.log('🔍 Fetching available deployers...') + const deployersResult = await gqlClient.getDeployers() + const availableDeployers = deployersResult.deployers || [] + + if (availableDeployers.length === 0) { + throw new Error( + 'No deployers available. Please configure at least one deployer in the backend.' + ) + } + + // Use the first available deployer if none specified + const deployerToUse = + config.deployerLrn || availableDeployers[0]?.deployerLrn + + if (!deployerToUse) { + throw new Error('No valid deployer found') + } + + console.log('🔍 Using deployer:', deployerToUse) + + // Get the backend's wallet address for blockchain transactions + const backendAddress = await gqlClient.getAddress() + console.log('🔍 Backend wallet address:', backendAddress) + console.log('🔍 Frontend wallet address:', wallet.address) + + // Use backend's address for blockchain transactions + const projectData = { + name: config.projectName, + repository: `${githubUsername}/${config.projectName}`, + prodBranch: 'main', + template: config.template.id, + paymentAddress: backendAddress, + txHash: + '0x0000000000000000000000000000000000000000000000000000000000000000' + } + + console.log( + '📤 Final project data being sent:', + JSON.stringify(projectData, null, 2) + ) + console.log('📤 With deployer:', deployerToUse) + console.log('📤 Organization slug:', config.organizationSlug) + console.log( + '📤 Environment variables:', + config.environmentVariables || [] + ) + + // Log the exact GraphQL variables being sent + const mutationVariables = { + organizationSlug: config.organizationSlug, + data: projectData, + lrn: deployerToUse, + auctionParams: undefined, + environmentVariables: config.environmentVariables || [] + } + + console.log( + '🔍 EXACT GraphQL variables being sent:', + JSON.stringify(mutationVariables, null, 2) + ) + + const projectResult = await gqlClient.addProject( + config.organizationSlug, + projectData, + deployerToUse, + undefined, // auctionParams + config.environmentVariables || [] + ) + + console.log('✅ Backend response received:', projectResult) + console.log('🔍 Project ID:', projectResult.addProject?.id) + console.log('🔍 Full project object:', projectResult.addProject) + + if (!projectResult.addProject?.id) { + console.error( + '❌ No project ID in response. Full response:', + projectResult + ) + throw new Error( + 'Failed to set up project deployment in backend - no project ID returned' + ) + } + + console.log( + '✅ Project created successfully with ID:', + projectResult.addProject.id + ) + + // Create and return the result + const result: TemplateDeploymentResult = { + projectId: projectResult.addProject.id, + repositoryUrl: repoResult.repositoryUrl, + deploymentUrl: undefined, // Will be populated once deployment completes + deploymentId: projectResult.addProject.id + } + + setDeploymentResult(result) + toast.success('Template deployed successfully!') + + return result + } catch (backendError) { + console.error('❌ Backend project creation failed:', backendError) + + // Enhanced error handling + let errorMessage = 'Unknown deployment error' + let errorDetails = { + message: 'Unknown error', + stack: undefined as string | undefined, + name: 'UnknownError' + } + + if (backendError instanceof Error) { + errorMessage = backendError.message + errorDetails = { + message: backendError.message, + stack: backendError.stack, + name: backendError.name + } + } else if (typeof backendError === 'object' && backendError !== null) { + const errorObj = backendError as any + errorMessage = errorObj.message || JSON.stringify(backendError) + errorDetails = { + message: errorObj.message || errorMessage, + stack: errorObj.stack, + name: errorObj.name || 'BackendError' + } + } + + console.error('❌ Error details:', errorDetails) + + // Provide more specific error messages + if ( + errorMessage.includes('Cannot return null for non-nullable field') + ) { + errorMessage = `Backend validation error: A required field is missing or invalid. + +Possible issues: +1. Invalid template ID: "${config.template.id}" +2. Repository format issue: "${repoResult.repositoryUrl}" +3. Missing deployer configuration +4. Organization "${config.organizationSlug}" not found + +Repository was created successfully: ${repoResult.repositoryUrl} +You may need to check the backend configuration or contact support.` + } + + throw new Error(`Backend deployment setup failed: ${errorMessage}`) + } + } catch (error) { + console.error('❌ Template deployment failed:', error) + const errorMessage = + error instanceof Error ? error.message : 'Unknown error' + setError(errorMessage) + toast.error(`Template deployment failed: ${errorMessage}`) + throw error + } finally { + setIsDeploying(false) + } + } + + const reset = () => { + setDeploymentResult(null) + setError(null) + setIsDeploying(false) + } + + return { + deployTemplate, + isDeploying: isDeploying || directGitHub.isLoading, + deploymentResult, + error, + reset + } +} diff --git a/apps/deploy-fe/src/hooks/useGitHubToken.tsx b/apps/deploy-fe/src/hooks/useGitHubToken.tsx new file mode 100644 index 0000000..4372879 --- /dev/null +++ b/apps/deploy-fe/src/hooks/useGitHubToken.tsx @@ -0,0 +1,69 @@ +'use client' + +import { getGitHubToken } from '@/actions/github' +import { useAuth } from '@clerk/nextjs' +import { useEffect, useState } from 'react' + +interface UseGitHubTokenReturn { + token: string | null + isLoading: boolean + error: string | null + refreshToken: () => Promise +} + +/** + * A hook to get the current user's GitHub OAuth token from Clerk + * This ensures each user gets their own token for GitHub API calls + */ +export function useGitHubToken(): UseGitHubTokenReturn { + const [token, setToken] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + const { isLoaded: isAuthLoaded, userId } = useAuth() + + const fetchToken = async () => { + if (!userId) { + setError('User not authenticated') + setIsLoading(false) + return + } + + try { + setIsLoading(true) + setError(null) + + const userToken = await getGitHubToken() + + if (!userToken) { + setError( + 'GitHub account not connected. Please connect your GitHub account.' + ) + setToken(null) + } else { + console.log('✅ Successfully retrieved user-specific GitHub token') + setToken(userToken) + } + } catch (err) { + console.error('❌ Error getting GitHub token:', err) + setError( + err instanceof Error ? err.message : 'Failed to get GitHub token' + ) + setToken(null) + } finally { + setIsLoading(false) + } + } + + useEffect(() => { + if (isAuthLoaded) { + fetchToken() + } + }, [isAuthLoaded, userId]) + + const refreshToken = async () => { + await fetchToken() + } + + return { token, isLoading, error, refreshToken } +} diff --git a/apps/deploy-fe/src/hooks/useRepoData.tsx b/apps/deploy-fe/src/hooks/useRepoData.tsx index a8d952f..beb37a1 100644 --- a/apps/deploy-fe/src/hooks/useRepoData.tsx +++ b/apps/deploy-fe/src/hooks/useRepoData.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from "react"; import { useAuth, useUser } from "@clerk/nextjs"; import { Octokit } from "@octokit/rest"; +import { getGitHubToken } from "@/actions/github"; // Define the return type of the hook interface UseRepoDataReturn { @@ -43,7 +44,7 @@ export function useRepoData(repoId: string): UseRepoDataReturn { if (githubAccount) { // Try to get GitHub OAuth token from Clerk try { - // token = await user.getToken({ template: 'github' }); + token = await getGitHubToken(); console.log('Using GitHub token from Clerk'); } catch (err) { console.error('Error getting GitHub token from Clerk:', err);