mirror your GitHub repos to tangled.org automatically
1

Configure Feed

Select the types of activity you want to include in your feed.

1import { and, eq, ne } from 'drizzle-orm' 2import { userIdentity } from '~~/server/db/schema' 3import { enqueue } from '~~/server/utils/queue' 4import { writeSession } from '~~/server/utils/server-session' 5import { generateAndPublishKey, revokeKeyForInstallationDid } 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 // One installation maps to exactly one DID. If another DID is currently 41 // bound to this installation, this connect displaces it: revoke that DID's 42 // now-dead SSH key (PDS record + local row) and null its installationId so 43 // the worker stops syncing for it. The displaced user_identity row is 44 // preserved — the user keeps their identity and can re-bind elsewhere. 45 const displaced = await db.select({ did: userIdentity.did }) 46 .from(userIdentity) 47 .where(and( 48 eq(userIdentity.installationId, installationId), 49 ne(userIdentity.did, session.did), 50 )) 51 52 for (const row of displaced) { 53 // eslint-disable-next-line no-await-in-loop -- sequential PDS revocations 54 await revokeKeyForInstallationDid(installationId, row.did) 55 } 56 57 if (displaced.length > 0) { 58 await db.update(userIdentity) 59 .set({ installationId: null, updatedAt: new Date() }) 60 .where(and( 61 eq(userIdentity.installationId, installationId), 62 ne(userIdentity.did, session.did), 63 )) 64 } 65 66 await db.insert(userIdentity).values({ 67 did: session.did, 68 handle: null, 69 installationId, 70 updatedAt: new Date(), 71 }).onConflictDoUpdate({ 72 target: userIdentity.did, 73 set: { installationId, updatedAt: new Date() }, 74 }) 75 76 // Generate and publish the SSH key inline: it's one ed25519 keygen + one 77 // PDS write, well under the function timeout, and lets us land users on 78 // the dashboard already enrolled. Rotation is a separate dashboard 79 // action that goes via the queue. 80 await generateAndPublishKey({ oauthSession: session, installationId }) 81 82 // Backfill: enqueue a single job that walks the installation's repo list 83 // and fans out per-repo enrolment. Doing this in the worker (rather than 84 // inline here) keeps the OAuth callback fast regardless of repo count. 85 await enqueue('tangled.backfill-installation', { installationId, page: 1 }) 86 } 87 else { 88 // Returning sign-in. Look up the installation we previously bound. 89 const rows = await db.select({ installationId: userIdentity.installationId }) 90 .from(userIdentity) 91 .where(eq(userIdentity.did, session.did)) 92 .limit(1) 93 94 if (rows.length === 0 || rows[0]!.installationId === null) { 95 // Authenticated tangled identity, but no GitHub install bound. Send 96 // them to install the App. We deliberately do NOT write a session 97 // yet — the dashboard needs an installation to be useful. 98 const appInstallUrl = process.env.NUXT_GITHUB_APP_INSTALL_URL 99 ?? 'https://github.com/apps/synchub-to/installations/new' 100 await sendRedirect(event, appInstallUrl, 302) 101 return 102 } 103 installationId = rows[0]!.installationId 104 } 105 106 // Sealed cookie session for the dashboard. Handle resolution is deferred. 107 await writeSession(event, { did: session.did, installationId }) 108 109 await sendRedirect(event, '/dashboard', 302) 110})