Leon4gr45/builder
0
1'use client';2 3import React, { useState, useEffect, useCallback } from 'react';4import { Button } from '@/components/ui/button';5import { Input } from '@/components/ui/input';6import { Label } from '@/components/ui/label';7import { ExternalLink, Eye, EyeOff, Loader2 } from 'lucide-react';8import { ConnectionBadge } from '@/components/settings/connection-badge';9import { toast } from 'sonner';10import { configManager } from '@/lib/config/storage';11import { track } from '@/lib/telemetry';12import { apiFetch } from '@/lib/api/backend-status';13 14interface GitHubAuthPanelProps {15 onAuthChange?: () => void;16}17 18export function GitHubAuthPanel({ onAuthChange }: GitHubAuthPanelProps) {19 const [tokenInput, setTokenInput] = useState('');20 const [showToken, setShowToken] = useState(false);21 const [isConnecting, setIsConnecting] = useState(false);22 const [isLoading, setIsLoading] = useState(false);23 const [isConnected, setIsConnected] = useState(() => !!configManager.getGitHubAuth());24 const [username, setUsername] = useState<string | undefined>(() => configManager.getGitHubAuth()?.username);25 26 const dispatchAuthEvent = useCallback((hasKey: boolean) => {27 onAuthChange?.();28 window.dispatchEvent(new CustomEvent('apiKeyUpdated', {29 detail: { provider: 'github', hasKey }30 }));31 }, [onAuthChange]);32 33 useEffect(() => {34 const handleAuthUpdate = (e: Event) => {35 const detail = (e as CustomEvent).detail;36 if (detail?.provider === 'github') {37 const auth = configManager.getGitHubAuth();38 setIsConnected(detail.hasKey);39 if (auth?.username) setUsername(auth.username);40 }41 };42 window.addEventListener('apiKeyUpdated', handleAuthUpdate);43 return () => window.removeEventListener('apiKeyUpdated', handleAuthUpdate);44 }, []);45 46 const handleConnect = async () => {47 const key = tokenInput.trim();48 if (!key) {49 toast.error('Please enter a Personal Access Token');50 return;51 }52 setIsConnecting(true);53 try {54 const response = await apiFetch('/api/validate-key', {55 method: 'POST',56 headers: { 'Content-Type': 'application/json' },57 body: JSON.stringify({ apiKey: key, provider: 'github' })58 });59 60 if (response.ok) {61 const { valid, username: githubUsername } = await response.json();62 if (valid) {63 configManager.setGitHubAuth({ access_token: key, username: githubUsername });64 setIsConnected(true);65 setUsername(githubUsername);66 setTokenInput('');67 toast.success(`Connected to GitHub as ${githubUsername}`);68 track('connection_added', { provider: 'github' });69 dispatchAuthEvent(true);70 } else {71 toast.error('Invalid token. Check that it has "repo" scope.');72 }73 } else {74 toast.error('Failed to validate token');75 }76 } catch {77 toast.error('Failed to validate token. Please try again.');78 } finally {79 setIsConnecting(false);80 }81 };82 83 const handleDisconnect = () => {84 setIsLoading(true);85 try {86 configManager.clearGitHubAuth();87 setIsConnected(false);88 setUsername(undefined);89 setTokenInput('');90 toast.success('Disconnected from GitHub');91 track('connection_removed', { provider: 'github' });92 dispatchAuthEvent(false);93 } catch {94 toast.error('Failed to disconnect');95 } finally {96 setIsLoading(false);97 }98 };99 100 if (isConnected) {101 return (102 <div className="space-y-3">103 <ConnectionBadge104 method="Personal Access Token"105 extra={username}106 info={107 <>108 Authenticated as {username}.{' '}109 <a110 href="https://github.com/settings/tokens"111 target="_blank"112 rel="noopener noreferrer"113 className="text-primary hover:underline inline-flex items-center gap-0.5"114 >115 Manage Tokens <ExternalLink className="h-2.5 w-2.5" />116 </a>117 </>118 }119 onDisconnect={handleDisconnect}120 disconnecting={isLoading}121 />122 </div>123 );124 }125 126 return (127 <div className="space-y-3">128 <Label>GitHub Authentication</Label>129 <p className="text-xs text-muted-foreground">130 Connect your GitHub account to export projects directly to repositories.131 </p>132 133 <div>134 <Label htmlFor="gh-token" className="text-xs">Personal Access Token</Label>135 <div className="flex gap-2 mt-1.5">136 <div className="relative flex-1">137 <Input138 id="gh-token"139 type={showToken ? 'text' : 'password'}140 value={tokenInput}141 onChange={(e) => setTokenInput(e.target.value)}142 onKeyDown={(e) => { if (e.key === 'Enter' && tokenInput.trim()) handleConnect(); }}143 placeholder="ghp_..."144 className="pr-10"145 disabled={isConnecting}146 />147 <Button148 size="icon"149 variant="ghost"150 className="absolute right-1 top-1 h-7 w-7"151 onClick={() => setShowToken(!showToken)}152 >153 {showToken ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}154 </Button>155 </div>156 <Button157 onClick={handleConnect}158 disabled={isConnecting || !tokenInput.trim()}159 size="sm"160 >161 {isConnecting ? (162 <>163 <Loader2 className="h-3 w-3 animate-spin mr-1" />164 Connecting...165 </>166 ) : (167 'Connect'168 )}169 </Button>170 </div>171 </div>172 173 <div className="p-3 border rounded-md bg-muted/50 text-xs text-muted-foreground space-y-2">174 <p className="font-medium">How to get a Personal Access Token:</p>175 <ol className="list-decimal list-inside space-y-1">176 <li>177 Go to{' '}178 <a179 href="https://github.com/settings/tokens"180 target="_blank"181 rel="noopener noreferrer"182 className="text-primary hover:underline inline-flex items-center gap-0.5"183 >184 GitHub Token Settings <ExternalLink className="h-2.5 w-2.5" />185 </a>186 </li>187 <li>Generate a new token (classic or fine-grained)</li>188 <li>Ensure it has <strong>repo</strong> scope (full control of private repositories)</li>189 <li>Paste the token above and click Connect</li>190 </ol>191 </div>192 </div>193 );194}195 