Add GitHub user filtering and core authentication improvements

This commit is contained in:
NasSharaf 2025-08-28 20:36:31 -04:00
parent 2c7c40697a
commit ce851ef883
7 changed files with 609 additions and 28 deletions

View File

@ -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
}

View File

@ -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 = () => {

View File

@ -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<string | null>(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)

View File

@ -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<CreateRepoResult> => {
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
}
}

View File

@ -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<TemplateDeploymentResult | null>(null)
const [error, setError] = useState<string | null>(null)
const gqlClient = useGQLClient()
const { wallet } = useWallet()
const { user } = useUser()
const directGitHub = useDirectGitHub()
const deployTemplate = async (
config: TemplateDeploymentConfig
): Promise<TemplateDeploymentResult> => {
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
}
}

View File

@ -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<void>
}
/**
* 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<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(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 }
}

View File

@ -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);