-
Notifications
You must be signed in to change notification settings - Fork 39
feat(web): add GitHub OAuth to repo star video tool #644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import type { NextRequest } from "next/server"; | ||
| import { NextResponse } from "next/server"; | ||
| import { | ||
| GITHUB_STATE_COOKIE, | ||
| GITHUB_STATE_MAX_AGE_SECONDS, | ||
| } from "@/lib/star-video/github-cookies"; | ||
| import { | ||
| buildGithubAuthorizeUrl, | ||
| createOAuthState, | ||
| getGithubOAuthConfig, | ||
| } from "@/lib/star-video/github-oauth"; | ||
| import { githubReturnRepoSchema } from "@/schemas/star-video"; | ||
|
|
||
| export const runtime = "nodejs"; | ||
|
|
||
| export function GET(request: NextRequest) { | ||
| const parsedRepo = githubReturnRepoSchema.safeParse( | ||
| request.nextUrl.searchParams.get("repo") ?? "" | ||
| ); | ||
| const repo = parsedRepo.success ? parsedRepo.data : null; | ||
|
|
||
| const returnUrl = new URL("/repo-star-video", request.nextUrl.origin); | ||
| if (repo) { | ||
| returnUrl.searchParams.set("repo", repo); | ||
| } | ||
|
|
||
| const config = getGithubOAuthConfig(); | ||
| if (!config) { | ||
| return NextResponse.redirect(returnUrl); | ||
| } | ||
|
|
||
| const state = createOAuthState(); | ||
| const redirectUri = new URL( | ||
| "/api/star-video/github/callback", | ||
| request.nextUrl.origin | ||
| ).toString(); | ||
|
|
||
| const response = NextResponse.redirect( | ||
| buildGithubAuthorizeUrl(config.clientId, redirectUri, state) | ||
| ); | ||
| response.cookies.set( | ||
| GITHUB_STATE_COOKIE, | ||
| JSON.stringify({ state, repo: repo ?? undefined }), | ||
| { | ||
| httpOnly: true, | ||
| secure: process.env.NODE_ENV === "production", | ||
| sameSite: "lax", | ||
| path: "/", | ||
| maxAge: GITHUB_STATE_MAX_AGE_SECONDS, | ||
| } | ||
| ); | ||
| return response; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import type { NextRequest } from "next/server"; | ||
| import { NextResponse } from "next/server"; | ||
| import { | ||
| GITHUB_CONNECTED_COOKIE, | ||
| GITHUB_COOKIE_MAX_AGE_SECONDS, | ||
| GITHUB_STATE_COOKIE, | ||
| GITHUB_TOKEN_COOKIE, | ||
| } from "@/lib/star-video/github-cookies"; | ||
| import { | ||
| encryptGithubToken, | ||
| exchangeGithubCode, | ||
| getGithubOAuthConfig, | ||
| } from "@/lib/star-video/github-oauth"; | ||
| import { | ||
| githubCallbackQuerySchema, | ||
| githubOAuthStateSchema, | ||
| } from "@/schemas/star-video"; | ||
|
|
||
| export const runtime = "nodejs"; | ||
|
|
||
| function parseStateCookie(value: string | undefined) { | ||
| if (!value) { | ||
| return null; | ||
| } | ||
| try { | ||
| const parsed = githubOAuthStateSchema.safeParse(JSON.parse(value)); | ||
| return parsed.success ? parsed.data : null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| const stateData = parseStateCookie( | ||
| request.cookies.get(GITHUB_STATE_COOKIE)?.value | ||
| ); | ||
|
|
||
| const returnUrl = new URL("/repo-star-video", request.nextUrl.origin); | ||
| if (stateData?.repo) { | ||
| returnUrl.searchParams.set("repo", stateData.repo); | ||
| } | ||
|
|
||
| const response = NextResponse.redirect(returnUrl); | ||
| response.cookies.delete(GITHUB_STATE_COOKIE); | ||
|
|
||
| const config = getGithubOAuthConfig(); | ||
| if (!(config && stateData)) { | ||
| return response; | ||
| } | ||
|
|
||
| const query = githubCallbackQuerySchema.safeParse({ | ||
| code: request.nextUrl.searchParams.get("code") ?? "", | ||
| state: request.nextUrl.searchParams.get("state") ?? "", | ||
| }); | ||
| if (!query.success || query.data.state !== stateData.state) { | ||
| return response; | ||
| } | ||
|
|
||
| const redirectUri = new URL( | ||
| "/api/star-video/github/callback", | ||
| request.nextUrl.origin | ||
| ).toString(); | ||
| const token = await exchangeGithubCode(query.data.code, redirectUri, config); | ||
| if (!token) { | ||
| return response; | ||
| } | ||
|
|
||
| const cookieOptions = { | ||
| secure: process.env.NODE_ENV === "production", | ||
| sameSite: "lax", | ||
| path: "/", | ||
| maxAge: GITHUB_COOKIE_MAX_AGE_SECONDS, | ||
| } as const; | ||
|
|
||
| response.cookies.set( | ||
| GITHUB_TOKEN_COOKIE, | ||
| encryptGithubToken(token, config.clientSecret), | ||
| { ...cookieOptions, httpOnly: true } | ||
| ); | ||
| response.cookies.set(GITHUB_CONNECTED_COOKIE, "1", cookieOptions); | ||
| return response; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { GITHUB_CONNECTED_COOKIE } from "./github-cookies"; | ||
|
|
||
| export function subscribeToGithubConnection(): () => void { | ||
| return () => undefined; | ||
| } | ||
|
|
||
| export function isGithubConnected(): boolean { | ||
| return document.cookie.split("; ").includes(`${GITHUB_CONNECTED_COOKIE}=1`); | ||
| } | ||
|
|
||
| export function getServerGithubConnected(): boolean { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MEDIUM: getServerGithubConnected always returns false causing hydration mismatch The Suggestion: Either pass the initial connection state from a server component (reading Prompt for AI agentsCommit |
||
| return false; | ||
| } | ||
|
|
||
| export function buildGithubConnectHref(repoParam: string | null): string { | ||
| if (!repoParam) { | ||
| return "/api/star-video/github/authorize"; | ||
| } | ||
| return `/api/star-video/github/authorize?repo=${encodeURIComponent(repoParam)}`; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export const GITHUB_CONNECTED_COOKIE = "sv_github_connected"; | ||
| export const GITHUB_TOKEN_COOKIE = "sv_github_token"; | ||
| export const GITHUB_STATE_COOKIE = "sv_github_state"; | ||
| export const GITHUB_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 7; | ||
| export const GITHUB_STATE_MAX_AGE_SECONDS = 60 * 10; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a visitor's OAuth token is revoked, expired, or rate-limited, its still-valid cookie makes this branch skip Redis and query GitHub without the shared credential; the failure is returned as a 503 with no fallback, leaving public-repository lookups broken until the cookie expires or is manually removed.