mirror your GitHub repos to tangled.org automatically
1import { installationAccountLogin, userAdministersInstallation, userOctokitFromCode } from '#server/utils/github-app'
2import { markInstallOwned } from '#server/utils/install-ownership'
3import { sessionConfig } from '#server/utils/server-session'
4
5interface OAuthFlowData {
6 state: string
7 installationId: number
8}
9
10/**
11 * GitHub user-OAuth callback. Verifies the `state` against the sealed flow
12 * cookie, exchanges the code for a user token, and confirms the user
13 * administers the installation. On success, marks the install owned (a sealed
14 * cookie the atproto callback checks before binding) and sends the user into
15 * the tangled connect flow.
16 */
17export default defineEventHandler(async event => {
18 const query = getQuery(event)
19 const code = typeof query.code === 'string' ? query.code : null
20 const state = typeof query.state === 'string' ? query.state : null
21 if (!code || !state) {
22 throw createError({ statusCode: 400, statusMessage: 'missing code or state' })
23 }
24
25 const flow = await useSession<OAuthFlowData>(event, {
26 ...sessionConfig(),
27 name: 'synchub-gh-oauth',
28 maxAge: 10 * 60,
29 })
30 const expectedState = flow.data.state
31 const installationId = flow.data.installationId
32 await flow.clear()
33
34 if (!expectedState || expectedState !== state || typeof installationId !== 'number') {
35 throw createError({ statusCode: 400, statusMessage: 'invalid or expired oauth state' })
36 }
37
38 const userOctokit = await userOctokitFromCode(code)
39 const administers = await userAdministersInstallation(userOctokit, installationId)
40 if (!administers) {
41 throw createError({
42 statusCode: 403,
43 statusMessage: 'your GitHub account does not administer this installation',
44 })
45 }
46
47 await markInstallOwned(event, installationId)
48
49 // Ownership proven. Hand off to the connect page, which now lets the user
50 // pick the tangled handle to bind. Carry the account login for display.
51 const login = await installationAccountLogin(installationId)
52 const params = new URLSearchParams({ installation_id: String(installationId), verified: '1' })
53 if (login) params.set('login', login)
54 await sendRedirect(event, `/connect?${params.toString()}`, 302)
55})