User can now click "Connect GitHub" inside the Import-existing-code
flow, sign in via GitHub, and pick a repo from a searchable list of
their own + collaborator + org repos. Both public and private repos
work — the encrypted access token on the user's account is auto-
attached when the create endpoint runs the agent-runner mirror.
OAuth flow:
- GET /api/integrations/github/connect — generates state, sets
a 10-min httpOnly cookie, 302s to GitHub authorize.
- GET /api/integrations/github/callback — verifies state,
exchanges code for token, fetches /user, encrypts the
token with secret-box (AES-256-GCM, VIBN_SECRETS_KEY) and
persists it on fs_users.data.integrations.github.
Bounces back to ?gh_connected=login or ?gh_error=msg.
- GET /api/integrations/github/repos — server-side fetches
the connected user's repos (per_page=100, sort=pushed,
affiliation=owner+collaborator+org_member). Returns the
GitHub login + a stripped repo summary; never the token.
- POST /api/integrations/github/disconnect — drops the integration
from fs_users (does NOT revoke on github.com).
Scopes requested: repo, read:user.
Token storage:
- Encrypted at rest with secret-box (lib/auth/secret-box.ts) using
VIBN_SECRETS_KEY. Tokens never leave the server.
- One token per fs_users row, keyed by email.
ImportSetup UI:
- On mount, fires /repos to detect connection state.
- If connected: shows a connected-as-@login chip with disconnect
link, a search-as-you-type repo picker (max 220px scroll, badges
for Private / language), and a "paste a different URL instead"
escape hatch.
- If not connected: shows a Connect GitHub card with a public-URL
fallback inline.
- On return from OAuth (?gh_connected=… or ?gh_error=…), surfaces
a toast and silently refreshes the repo list.
- Selected repo carries default_branch + repo id into the create
payload so we can store them on the project for later UI hints.
/api/projects/create:
- When a githubRepoUrl is mirrored, falls back to the user's
OAuth-linked token if no PAT is explicitly passed. Means the
flow "just works" for private repos once GitHub is connected.
Required env (already set in production):
- GITHUB_CLIENT_ID
- GITHUB_CLIENT_SECRET
Made-with: Cursor
53 lines
1.5 KiB
TypeScript
53 lines
1.5 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);
|
|
const callbackUrl = `${url.origin}/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;
|
|
}
|