frontend changes

This commit is contained in:
Chandini 2025-09-26 17:05:02 +05:30
parent 797f2e8657
commit 69def8560b
4 changed files with 505 additions and 3 deletions

View File

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

View File

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

View File

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