|
| 1 | +import { defineEventHandler, createError, getQuery, sendRedirect } from "h3" |
| 2 | +import { verifyIdentityState } from "../../../../lib/identity-oauth-state" |
| 3 | +import { upsertGithubIdentity } from "../../../../lib/github-identities" |
| 4 | +import { getGithubAppCredentials } from "../../../../lib/github-app-credentials" |
| 5 | +import { |
| 6 | + exchangeGithubCodeDefault, |
| 7 | + fetchGithubUserDefault, |
| 8 | + __getOauthOverride, |
| 9 | +} from "../../../../lib/github-oauth-link" |
| 10 | +import { requireSession } from "../../../../lib/permissions" |
| 11 | + |
| 12 | +export default defineEventHandler(async (event) => { |
| 13 | + const session = await requireSession(event) |
| 14 | + const query = getQuery(event) |
| 15 | + const code = typeof query.code === "string" ? query.code : null |
| 16 | + const state = typeof query.state === "string" ? query.state : null |
| 17 | + if (!code || !state) { |
| 18 | + throw createError({ statusCode: 400, statusMessage: "Missing code/state" }) |
| 19 | + } |
| 20 | + |
| 21 | + const authSecret = process.env.BETTER_AUTH_SECRET |
| 22 | + if (!authSecret) throw createError({ statusCode: 500, statusMessage: "Missing auth secret" }) |
| 23 | + |
| 24 | + let stateClaim: { userId: string } |
| 25 | + try { |
| 26 | + stateClaim = verifyIdentityState({ state, secret: authSecret }) |
| 27 | + } catch { |
| 28 | + throw createError({ statusCode: 400, statusMessage: "Invalid or expired state" }) |
| 29 | + } |
| 30 | + if (stateClaim.userId !== session.userId) { |
| 31 | + throw createError({ statusCode: 403, statusMessage: "State does not match session" }) |
| 32 | + } |
| 33 | + |
| 34 | + const creds = await getGithubAppCredentials() |
| 35 | + if (!creds?.clientId || !creds.clientSecret) { |
| 36 | + throw createError({ statusCode: 400, statusMessage: "GitHub App is not configured" }) |
| 37 | + } |
| 38 | + |
| 39 | + const override = __getOauthOverride() |
| 40 | + const deps = override ?? { clientId: creds.clientId, clientSecret: creds.clientSecret } |
| 41 | + |
| 42 | + const token = await exchangeGithubCodeDefault(deps, code) |
| 43 | + const ghUser = await fetchGithubUserDefault(deps, token) |
| 44 | + |
| 45 | + try { |
| 46 | + await upsertGithubIdentity(session.userId, { |
| 47 | + externalId: String(ghUser.id), |
| 48 | + externalHandle: ghUser.login, |
| 49 | + externalAvatarUrl: ghUser.avatar_url, |
| 50 | + externalName: ghUser.name, |
| 51 | + externalEmail: ghUser.email, |
| 52 | + }) |
| 53 | + } catch (e) { |
| 54 | + const message = e instanceof Error ? e.message : "Link failed" |
| 55 | + return sendRedirect(event, `/settings/identities?error=${encodeURIComponent(message)}`) |
| 56 | + } |
| 57 | + |
| 58 | + return sendRedirect(event, "/settings/identities?linked=github") |
| 59 | +}) |
0 commit comments