frontend changes
This commit is contained in:
parent
797f2e8657
commit
69def8560b
@ -95,6 +95,19 @@ const addAuthTokenInterceptor = (client: typeof authApiClient) => {
|
|||||||
(config) => {
|
(config) => {
|
||||||
// Always get fresh token from localStorage instead of using module variable
|
// Always get fresh token from localStorage instead of using module variable
|
||||||
const freshToken = getAccessToken();
|
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) {
|
if (freshToken) {
|
||||||
config.headers = config.headers || {};
|
config.headers = config.headers || {};
|
||||||
config.headers.Authorization = `Bearer ${freshToken}`;
|
config.headers.Authorization = `Bearer ${freshToken}`;
|
||||||
@ -110,6 +123,13 @@ const addTokenRefreshInterceptor = (client: typeof authApiClient) => {
|
|||||||
client.interceptors.response.use(
|
client.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
async (error) => {
|
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 originalRequest = error.config;
|
||||||
const isRefreshEndpoint = originalRequest?.url?.includes('/api/auth/refresh');
|
const isRefreshEndpoint = originalRequest?.url?.includes('/api/auth/refresh');
|
||||||
if (error.response?.status === 401 && !originalRequest._retry && !isRefreshEndpoint) {
|
if (error.response?.status === 401 && !originalRequest._retry && !isRefreshEndpoint) {
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import PromptSidePanel from "@/components/prompt-side-panel"
|
|||||||
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
|
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
|
||||||
import { getAccessToken } from "@/components/apis/authApiClients"
|
import { getAccessToken } from "@/components/apis/authApiClients"
|
||||||
import TechStackSummary from "@/components/tech-stack-summary"
|
import TechStackSummary from "@/components/tech-stack-summary"
|
||||||
|
import { attachRepository, getGitHubAuthStatus } from "@/lib/api/github"
|
||||||
|
|
||||||
interface Template {
|
interface Template {
|
||||||
id: string
|
id: string
|
||||||
@ -51,7 +52,188 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
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)
|
||||||
|
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<boolean | null>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const status = await getGitHubAuthStatus()
|
||||||
|
setIsGithubConnected(!!status?.data?.connected)
|
||||||
|
} catch {
|
||||||
|
setIsGithubConnected(false)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}, [])
|
||||||
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 {}
|
||||||
|
|
||||||
|
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<DatabaseTemplate | null>(null)
|
const [deletingTemplate, setDeletingTemplate] = useState<DatabaseTemplate | null>(null)
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false)
|
const [deleteLoading, setDeleteLoading] = useState(false)
|
||||||
// Keep a stable list of all categories seen so the filter chips don't disappear
|
// 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<DatabaseTemplate>) => {
|
const handleUpdateTemplate = async (id: string, templateData: Partial<DatabaseTemplate>) => {
|
||||||
try {
|
try {
|
||||||
// Find the template to determine if it's custom
|
// Find the template to determine if it's custom
|
||||||
@ -819,7 +1069,10 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="group border-dashed border-2 border-white/15 bg-white/5 hover:border-white/25 transition-all cursor-pointer" onClick={() => user?.id ? setShowCustomForm(true) : window.location.href = '/signin'}>
|
<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 }
|
||||||
|
setShowCreateOptionDialog(true)
|
||||||
|
}}>
|
||||||
<CardContent className="text-center py-16 px-8 text-white/80">
|
<CardContent className="text-center py-16 px-8 text-white/80">
|
||||||
<div className="w-20 h-20 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
<div className="w-20 h-20 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
<Plus className="h-10 w-10 text-orange-400" />
|
<Plus className="h-10 w-10 text-orange-400" />
|
||||||
@ -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."
|
: "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">
|
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (!user?.id) { window.location.href = '/signin'; return }
|
||||||
|
setShowCreateOptionDialog(true)
|
||||||
|
}}>
|
||||||
{user?.id ? (
|
{user?.id ? (
|
||||||
<>
|
<>
|
||||||
<Plus className="mr-2 h-5 w-5" />
|
<Plus className="mr-2 h-5 w-5" />
|
||||||
@ -879,6 +1136,182 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Create Template Options Modal */}
|
||||||
|
<Dialog open={showCreateOptionDialog} onOpenChange={(open) => {
|
||||||
|
setShowCreateOptionDialog(open)
|
||||||
|
if (!open) {
|
||||||
|
setShowGitForm(false)
|
||||||
|
setGitProvider('')
|
||||||
|
setGitUrl('')
|
||||||
|
setGitBranch('main')
|
||||||
|
setGitAuthMethod('')
|
||||||
|
setGitCredentials({ username: '', password: '', token: '', sshKey: '' })
|
||||||
|
setGitStep('provider')
|
||||||
|
setAuthUrl('')
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">Create Template</DialogTitle>
|
||||||
|
<DialogDescription className="text-white/80">
|
||||||
|
Choose how you want to create a template.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{!showGitForm ? (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<Button className="bg-orange-500 hover:bg-orange-400 text-black" onClick={() => {
|
||||||
|
setShowCreateOptionDialog(false)
|
||||||
|
setShowCustomForm(true)
|
||||||
|
}}>Create Manually</Button>
|
||||||
|
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={() => {
|
||||||
|
setShowGitForm(true)
|
||||||
|
setGitStep('provider')
|
||||||
|
}}>
|
||||||
|
Import from Git
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : gitStep === 'provider' ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-white/70 mb-3">Select Git Provider</label>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{Object.entries(gitProviders).map(([key, provider]) => (
|
||||||
|
<Button
|
||||||
|
key={key}
|
||||||
|
variant="outline"
|
||||||
|
className="border-white/20 text-white hover:bg-white/10 h-12 flex flex-col items-center gap-1"
|
||||||
|
onClick={() => {
|
||||||
|
setGitProvider(key)
|
||||||
|
setGitStep('url')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d={provider.icon}/>
|
||||||
|
</svg>
|
||||||
|
{provider.name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 justify-end pt-2">
|
||||||
|
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={() => { setShowGitForm(false) }}>Back</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-white/70 hover:text-white p-1"
|
||||||
|
onClick={() => setGitStep('provider')}
|
||||||
|
>
|
||||||
|
← Back to Provider
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-white/70">
|
||||||
|
Import from {gitProviders[gitProvider as keyof typeof gitProviders]?.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-white/70 mb-1">Repository URL</label>
|
||||||
|
<Input
|
||||||
|
value={gitUrl}
|
||||||
|
onChange={(e) => setGitUrl(e.target.value)}
|
||||||
|
placeholder={gitProviders[gitProvider as keyof typeof gitProviders]?.placeholder}
|
||||||
|
className="bg-white/10 border-white/20 text-white"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-white/50 mt-1">
|
||||||
|
Enter the full URL to your {gitProviders[gitProvider as keyof typeof gitProviders]?.name} repository
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-white/70 mb-1">Branch (optional)</label>
|
||||||
|
<Input
|
||||||
|
value={gitBranch}
|
||||||
|
onChange={(e) => setGitBranch(e.target.value)}
|
||||||
|
placeholder="main"
|
||||||
|
className="bg-white/10 border-white/20 text-white"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-white/50 mt-1">
|
||||||
|
Leave empty to use the default branch
|
||||||
|
</p>
|
||||||
|
</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">
|
||||||
|
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={() => setGitStep('provider')}>Back</Button>
|
||||||
|
<Button
|
||||||
|
className="bg-orange-500 hover:bg-orange-400 text-black"
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => { e.preventDefault(); handleCreateFromGit(); }}
|
||||||
|
disabled={!gitUrl.trim() || (isGithubConnected === false && !authUrl)}
|
||||||
|
>
|
||||||
|
Import Template
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// export const BACKEND_URL = 'https://backend.codenuk.com';
|
// 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;
|
export const SOCKET_URL = BACKEND_URL;
|
||||||
|
|
||||||
|
|||||||
49
src/lib/api/github.ts
Normal file
49
src/lib/api/github.ts
Normal file
@ -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<T = unknown> {
|
||||||
|
success: boolean
|
||||||
|
message?: string
|
||||||
|
data?: T
|
||||||
|
requires_auth?: boolean
|
||||||
|
auth_url?: string
|
||||||
|
auth_error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function attachRepository(payload: AttachRepositoryPayload): Promise<AttachRepositoryResponse> {
|
||||||
|
// 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<AttachRepositoryResponse<GitHubAuthStatusData>> {
|
||||||
|
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<GitHubAuthStatusData>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user