/** * GET /api/integrations/github/connect * * Kicks off GitHub OAuth. Generates an unguessable `state`, stores it * in a short-lived (10 min) httpOnly cookie, and 302s to GitHub's * authorize endpoint. * * The callback route validates the cookie matches the `state` GitHub * echoes back, defending against login-CSRF. */ import { NextResponse } from "next/server"; import { randomBytes } from "crypto"; import { authSession } from "@/lib/auth/session-server"; import { buildAuthorizeUrl, isGithubOauthConfigured, } from "@/lib/integrations/github"; const STATE_COOKIE = "gh_oauth_state"; const STATE_TTL_S = 10 * 60; export async function GET(req: Request) { if (!isGithubOauthConfigured()) { return NextResponse.json( { error: "GitHub OAuth is not configured on this server." }, { status: 503 }, ); } const session = await authSession(); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const url = new URL(req.url); // Use NEXTAUTH_URL when available — behind a proxy req.url.origin // resolves to the internal bind address (0.0.0.0) rather than the // public hostname, which GitHub then rejects as an unregistered URI. const appOrigin = (process.env.NEXTAUTH_URL ?? url.origin).replace(/\/$/, ""); const callbackUrl = `${appOrigin}/api/integrations/github/callback`; const returnTo = url.searchParams.get("returnTo") ?? "/"; const state = randomBytes(16).toString("hex"); const statePayload = `${state}:${returnTo}`; const authorize = buildAuthorizeUrl(state, callbackUrl); const res = NextResponse.redirect(authorize); res.cookies.set(STATE_COOKIE, statePayload, { httpOnly: true, sameSite: "lax", path: "/", secure: url.protocol === "https:", maxAge: STATE_TTL_S, }); return res; }