mirror your GitHub repos to tangled.org automatically
1import { eq } from 'drizzle-orm'
2import { userIdentity } from '~~/server/db/schema'
3import { enqueue } from '~~/server/utils/queue'
4import { writeSession } from '~~/server/utils/server-session'
5import { generateAndPublishKey } from '~~/server/utils/tangled-pubkey'
6
7export default defineEventHandler(async event => {
8 const url = getRequestURL(event)
9 const params = url.searchParams
10
11 const client = await useOAuthClient()
12 const { session, state } = await client.callback(params)
13
14 const db = useDb()
15
16 // Two flows land here:
17 //
18 // - **First-time connect** (`state` set to the GitHub installation id).
19 // Bind the resulting `user_identity` row, publish an SSH key, kick off
20 // repo backfill. This is the "install GitHub App → connect tangled"
21 // ordering.
22 // - **Returning sign-in** (`state` absent). The user already has a
23 // `user_identity` row; we look it up by DID and write a fresh session
24 // so they can use the dashboard on a new device / browser.
25 //
26 // If `state` is present but invalid, or `state` is absent but no
27 // `user_identity` exists for the DID, we send the user to install the
28 // GitHub App. Trying to land them on `/dashboard` with no installation
29 // would just show a useless page.
30
31 let installationId: number | undefined
32
33 if (state) {
34 const parsed = Number(state)
35 if (!Number.isFinite(parsed)) {
36 throw createError({ statusCode: 400, statusMessage: 'invalid state (non-numeric installation id)' })
37 }
38 installationId = parsed
39
40 await db.insert(userIdentity).values({
41 did: session.did,
42 handle: null,
43 installationId,
44 updatedAt: new Date(),
45 }).onConflictDoUpdate({
46 target: userIdentity.did,
47 set: { installationId, updatedAt: new Date() },
48 })
49
50 // Generate and publish the SSH key inline: it's one ed25519 keygen + one
51 // PDS write, well under the function timeout, and lets us land users on
52 // the dashboard already enrolled. Rotation is a separate dashboard
53 // action that goes via the queue.
54 await generateAndPublishKey({ oauthSession: session, installationId })
55
56 // Backfill: enqueue a single job that walks the installation's repo list
57 // and fans out per-repo enrolment. Doing this in the worker (rather than
58 // inline here) keeps the OAuth callback fast regardless of repo count.
59 await enqueue('tangled.backfill-installation', { installationId, page: 1 })
60 }
61 else {
62 // Returning sign-in. Look up the installation we previously bound.
63 const rows = await db.select({ installationId: userIdentity.installationId })
64 .from(userIdentity)
65 .where(eq(userIdentity.did, session.did))
66 .limit(1)
67
68 if (rows.length === 0 || rows[0]!.installationId === null) {
69 // Authenticated tangled identity, but no GitHub install bound. Send
70 // them to install the App. We deliberately do NOT write a session
71 // yet — the dashboard needs an installation to be useful.
72 const appInstallUrl = process.env.NUXT_GITHUB_APP_INSTALL_URL
73 ?? 'https://github.com/apps/synchub-to/installations/new'
74 await sendRedirect(event, appInstallUrl, 302)
75 return
76 }
77 installationId = rows[0]!.installationId
78 }
79
80 // Sealed cookie session for the dashboard. Handle resolution is deferred.
81 await writeSession(event, { did: session.did, installationId })
82
83 await sendRedirect(event, '/dashboard', 302)
84})