Skip to content

Commit be3fef1

Browse files
Merge pull request Gerome-Elassaad#16 from Gerome-Elassaad/10-integrate-github-repos
integrated github issue Gerome-Elassaad#10 fixed
2 parents 24defd9 + 9c7d54e commit be3fef1

File tree

32 files changed

+1582
-204
lines changed

32 files changed

+1582
-204
lines changed

.env.template

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ KV_REST_API_TOKEN=
3131
SUPABASE_URL=
3232
SUPABASE_ANON_KEY=
3333

34+
# GitHub OAuth
35+
GITHUB_CLIENT_ID=
36+
GITHUB_CLIENT_SECRET=
37+
GITHUB_WEBHOOK_SECRET=
38+
3439
# PostHog (analytics)
3540
NEXT_PUBLIC_POSTHOG_KEY=
3641
NEXT_PUBLIC_POSTHOG_HOST=
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { createServerClient } from '@/lib/supabase-server'
3+
4+
export async function POST(request: NextRequest) {
5+
try {
6+
const { access_token } = await request.json()
7+
const supabase = createServerClient()
8+
9+
const { data: { session } } = await supabase.auth.getSession()
10+
11+
if (!session?.user?.id) {
12+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
13+
}
14+
15+
if (!access_token) {
16+
return NextResponse.json({ error: 'Access token required' }, { status: 400 })
17+
}
18+
19+
const response = await fetch(`https://api.github.com/applications/${process.env.GITHUB_CLIENT_ID}/token`, {
20+
method: 'DELETE',
21+
headers: {
22+
'Authorization': `Basic ${Buffer.from(`${process.env.GITHUB_CLIENT_ID}:${process.env.GITHUB_CLIENT_SECRET}`).toString('base64')}`,
23+
'Accept': 'application/vnd.github.v3+json',
24+
'Content-Type': 'application/json',
25+
},
26+
body: JSON.stringify({
27+
access_token,
28+
}),
29+
})
30+
31+
if (response.ok || response.status === 404) {
32+
const { error: dbError } = await supabase
33+
.from('user_integrations')
34+
.update({
35+
is_connected: false,
36+
connection_data: {},
37+
updated_at: new Date().toISOString(),
38+
})
39+
.eq('user_id', session.user.id)
40+
.eq('service_name', 'github')
41+
42+
if (dbError) {
43+
console.error('Database error during revocation:', dbError)
44+
return NextResponse.json({ error: 'Failed to update database' }, { status: 500 })
45+
}
46+
47+
return NextResponse.json({ success: true })
48+
} else {
49+
const errorText = await response.text()
50+
console.error('GitHub token revocation failed:', response.status, errorText)
51+
return NextResponse.json({ error: 'Failed to revoke token' }, { status: 500 })
52+
}
53+
} catch (error) {
54+
console.error('Error revoking GitHub token:', error)
55+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
56+
}
57+
}

app/api/auth/github/route.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { createServerClient } from '@/lib/supabase-server'
3+
4+
export async function GET(request: NextRequest) {
5+
const searchParams = request.nextUrl.searchParams
6+
const code = searchParams.get('code')
7+
const state = searchParams.get('state')
8+
const error = searchParams.get('error')
9+
10+
if (error) {
11+
console.error('GitHub OAuth error:', error)
12+
return NextResponse.redirect(
13+
new URL(`/settings/integrations?error=${encodeURIComponent('GitHub authentication failed')}`, request.url)
14+
)
15+
}
16+
17+
if (!code || !state) {
18+
return NextResponse.redirect(
19+
new URL(`/settings/integrations?error=${encodeURIComponent('Missing authorization code')}`, request.url)
20+
)
21+
}
22+
23+
try {
24+
const supabase = createServerClient()
25+
const { data: { user } } = await supabase.auth.getUser()
26+
27+
if (!user) {
28+
return NextResponse.redirect(
29+
new URL(`/settings/integrations?error=${encodeURIComponent('User not authenticated')}`, request.url)
30+
)
31+
}
32+
33+
const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
34+
method: 'POST',
35+
headers: {
36+
'Accept': 'application/json',
37+
'Content-Type': 'application/json',
38+
},
39+
body: JSON.stringify({
40+
client_id: process.env.GITHUB_CLIENT_ID,
41+
client_secret: process.env.GITHUB_CLIENT_SECRET,
42+
code,
43+
state,
44+
}),
45+
})
46+
47+
const tokenData = await tokenResponse.json()
48+
49+
if (tokenData.error) {
50+
console.error('GitHub token exchange error:', tokenData.error)
51+
return NextResponse.redirect(
52+
new URL(`/settings/integrations?error=${encodeURIComponent('Failed to exchange authorization code')}`, request.url)
53+
)
54+
}
55+
56+
const userResponse = await fetch('https://api.github.com/user', {
57+
headers: {
58+
'Authorization': `Bearer ${tokenData.access_token}`,
59+
'Accept': 'application/vnd.github.v3+json',
60+
},
61+
})
62+
63+
const githubUser = await userResponse.json()
64+
65+
if (!userResponse.ok) {
66+
console.error('GitHub user fetch error:', githubUser)
67+
return NextResponse.redirect(
68+
new URL(`/settings/integrations?error=${encodeURIComponent('Failed to fetch user information')}`, request.url)
69+
)
70+
}
71+
72+
const { error: dbError } = await supabase
73+
.from('user_integrations')
74+
.upsert({
75+
user_id: user.id,
76+
service_name: 'github',
77+
is_connected: true,
78+
connection_data: {
79+
access_token: tokenData.access_token,
80+
refresh_token: tokenData.refresh_token,
81+
token_type: tokenData.token_type,
82+
scope: tokenData.scope,
83+
github_user_id: githubUser.id,
84+
username: githubUser.login,
85+
avatar_url: githubUser.avatar_url,
86+
connected_at: new Date().toISOString(),
87+
},
88+
last_sync_at: new Date().toISOString(),
89+
updated_at: new Date().toISOString(),
90+
})
91+
92+
if (dbError) {
93+
console.error('Database error:', dbError)
94+
return NextResponse.redirect(
95+
new URL(`/settings/integrations?error=${encodeURIComponent('Failed to save integration')}`, request.url)
96+
)
97+
}
98+
99+
try {
100+
await setupWebhooks(tokenData.access_token, githubUser.login)
101+
} catch (webhookError) {
102+
console.warn('Webhook setup failed:', webhookError)
103+
}
104+
105+
return NextResponse.redirect(
106+
new URL('/settings/integrations?success=github_connected', request.url)
107+
)
108+
} catch (error) {
109+
console.error('GitHub OAuth error:', error)
110+
return NextResponse.redirect(
111+
new URL(`/settings/integrations?error=${encodeURIComponent('Internal server error')}`, request.url)
112+
)
113+
}
114+
}
115+
116+
async function setupWebhooks(accessToken: string, username: string) {
117+
const webhookUrl = `${process.env.NEXT_PUBLIC_SITE_URL}/api/webhooks/github`
118+
119+
const reposResponse = await fetch(`https://api.github.com/user/repos?type=owner&per_page=10`, {
120+
headers: {
121+
'Authorization': `Bearer ${accessToken}`,
122+
'Accept': 'application/vnd.github.v3+json',
123+
},
124+
})
125+
126+
if (reposResponse.ok) {
127+
const repos = await reposResponse.json()
128+
129+
for (const repo of repos.slice(0, 3)) {
130+
try {
131+
await fetch(`https://api.github.com/repos/${repo.full_name}/hooks`, {
132+
method: 'POST',
133+
headers: {
134+
'Authorization': `Bearer ${accessToken}`,
135+
'Accept': 'application/vnd.github.v3+json',
136+
'Content-Type': 'application/json',
137+
},
138+
body: JSON.stringify({
139+
name: 'web',
140+
active: true,
141+
events: ['push', 'pull_request', 'issues'],
142+
config: {
143+
url: webhookUrl,
144+
content_type: 'json',
145+
insecure_ssl: '0',
146+
secret: process.env.GITHUB_WEBHOOK_SECRET,
147+
},
148+
}),
149+
})
150+
} catch (error) {
151+
console.warn(`Failed to setup webhook for ${repo.full_name}:`, error)
152+
}
153+
}
154+
}
155+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { createServerClient } from '@/lib/supabase-server'
3+
4+
export async function GET(
5+
request: NextRequest,
6+
{ params }: { params: { owner: string; repo: string } }
7+
) {
8+
try {
9+
const supabase = createServerClient()
10+
const { data: { session } } = await supabase.auth.getSession()
11+
12+
if (!session?.user?.id) {
13+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
14+
}
15+
16+
const { data: integration } = await supabase
17+
.from('user_integrations')
18+
.select('connection_data')
19+
.eq('user_id', session.user.id)
20+
.eq('service_name', 'github')
21+
.eq('is_connected', true)
22+
.single()
23+
24+
if (!integration?.connection_data?.access_token) {
25+
return NextResponse.json({ error: 'GitHub not connected' }, { status: 400 })
26+
}
27+
28+
const { owner, repo } = params
29+
const searchParams = request.nextUrl.searchParams
30+
const path = searchParams.get('path') || ''
31+
const ref = searchParams.get('ref') || 'main'
32+
33+
const response = await fetch(
34+
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${ref}`,
35+
{
36+
headers: {
37+
'Authorization': `Bearer ${integration.connection_data.access_token}`,
38+
'Accept': 'application/vnd.github.v3+json',
39+
},
40+
}
41+
)
42+
43+
if (!response.ok) {
44+
const errorData = await response.json()
45+
console.error('GitHub API error:', errorData)
46+
47+
if (response.status === 401) {
48+
await supabase
49+
.from('user_integrations')
50+
.update({
51+
is_connected: false,
52+
connection_data: {},
53+
updated_at: new Date().toISOString(),
54+
})
55+
.eq('user_id', session.user.id)
56+
.eq('service_name', 'github')
57+
58+
return NextResponse.json({ error: 'GitHub token expired' }, { status: 401 })
59+
}
60+
61+
if (response.status === 404) {
62+
return NextResponse.json({ error: 'Repository or path not found' }, { status: 404 })
63+
}
64+
65+
return NextResponse.json({ error: 'Failed to fetch repository contents' }, { status: 500 })
66+
}
67+
68+
const contents = await response.json()
69+
70+
if (Array.isArray(contents)) {
71+
const formattedContents = contents.map((item: any) => ({
72+
name: item.name,
73+
path: item.path,
74+
type: item.type,
75+
size: item.size,
76+
sha: item.sha,
77+
download_url: item.download_url,
78+
html_url: item.html_url,
79+
}))
80+
81+
return NextResponse.json({ contents: formattedContents, type: 'directory' })
82+
} else {
83+
const formattedContent = {
84+
name: contents.name,
85+
path: contents.path,
86+
type: contents.type,
87+
size: contents.size,
88+
sha: contents.sha,
89+
content: contents.content,
90+
encoding: contents.encoding,
91+
download_url: contents.download_url,
92+
html_url: contents.html_url,
93+
}
94+
95+
return NextResponse.json({ content: formattedContent, type: 'file' })
96+
}
97+
} catch (error) {
98+
console.error('Error fetching repository contents:', error)
99+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
100+
}
101+
}

0 commit comments

Comments
 (0)