57 lines
1.8 KiB
TypeScript
57 lines
1.8 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|