Files
vibn-frontend/app/api/integrations/github/repos/route.ts
Mark Henderson 90bed6ab31 feat(github): OAuth integration + repo picker for Import flow
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
2026-04-29 16:44:13 -07:00

54 lines
1.6 KiB
TypeScript

/**
* GET /api/integrations/github/repos
*
* Returns the connected user's GitHub repos for the import-flow picker.
* Server-side: never exposes the raw token, only the public repo
* metadata the picker needs (name, full_name, description, language,
* is-private, last-pushed, default branch, html_url).
*
* 200 → { connected: true, login, repos: [...] }
* 200 → { connected: false } ← user hasn't linked GitHub yet
* 401 → unauthorized
*/
import { NextResponse } from "next/server";
import { authSession } from "@/lib/auth/session-server";
import { loadGithubIntegration, listUserRepos } from "@/lib/integrations/github";
export async function GET() {
const session = await authSession();
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const link = await loadGithubIntegration(session.user.email);
if (!link) {
return NextResponse.json({ connected: false });
}
try {
const repos = await listUserRepos(link.token);
return NextResponse.json({
connected: true,
login: link.login,
repos: repos.map(r => ({
id: r.id,
name: r.name,
fullName: r.full_name,
description: r.description,
defaultBranch: r.default_branch,
htmlUrl: r.html_url,
private: r.private,
language: r.language,
pushedAt: r.pushed_at,
fork: r.fork,
})),
});
} catch (err) {
return NextResponse.json(
{ error: err instanceof Error ? err.message : "Failed to list repos" },
{ status: 502 },
);
}
}