-
Notifications
You must be signed in to change notification settings - Fork 963
Add github integration #842
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
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
9b728e1
Add github integration
Kitenite d777426
Update package.json and routes
Kitenite 27e1574
Use API instead
Kitenite 9b49b68
Add sync
Kitenite 16d580b
fix(trpc): scope PR queries by repositoryId to prevent cross-org data…
Kitenite f70be4f
fix: add defensive error handling for GitHub integration
Kitenite dbed1f6
fix: address additional PR review feedback
Kitenite 5dcf826
fix: address agent review feedback
Kitenite 9b0e40e
Update security concerns
Kitenite 27bffd0
use GH_ instead
Kitenite 5e75fa2
Update workflow
Kitenite 1100942
fix: quote GH_APP_PRIVATE_KEY to handle PEM newlines
Kitenite 9ea0e2d
fix: skip QStash in development (can't reach localhost)
Kitenite 4708231
fix: skip QStash signature verification in development
Kitenite 9dfa41f
Merge remote-tracking branch 'origin/main' into github-app-in-web
Kitenite File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| import { db } from "@superset/db/client"; | ||
| import { githubInstallations, members } from "@superset/db/schema"; | ||
| import { Client } from "@upstash/qstash"; | ||
| import { and, eq } from "drizzle-orm"; | ||
|
|
||
| import { env } from "@/env"; | ||
| import { verifySignedState } from "@/lib/oauth-state"; | ||
| import { githubApp } from "../octokit"; | ||
|
|
||
| const qstash = new Client({ token: env.QSTASH_TOKEN }); | ||
|
|
||
| /** | ||
| * Callback handler for GitHub App installation. | ||
| * GitHub redirects here after the user installs/configures the app. | ||
| */ | ||
| export async function GET(request: Request) { | ||
| const url = new URL(request.url); | ||
| const installationId = url.searchParams.get("installation_id"); | ||
| const setupAction = url.searchParams.get("setup_action"); | ||
| const state = url.searchParams.get("state"); | ||
|
|
||
| if (setupAction === "cancel") { | ||
| return Response.redirect( | ||
| `${env.NEXT_PUBLIC_WEB_URL}/integrations/github?error=installation_cancelled`, | ||
| ); | ||
| } | ||
|
|
||
| if (!installationId || !state) { | ||
| return Response.redirect( | ||
| `${env.NEXT_PUBLIC_WEB_URL}/integrations/github?error=missing_params`, | ||
| ); | ||
| } | ||
|
|
||
| // Verify signed state (prevents forgery) | ||
| const stateData = verifySignedState(state); | ||
| if (!stateData) { | ||
| return Response.redirect( | ||
| `${env.NEXT_PUBLIC_WEB_URL}/integrations/github?error=invalid_state`, | ||
| ); | ||
| } | ||
|
|
||
| const { organizationId, userId } = stateData; | ||
|
|
||
| // Re-verify membership at callback time (defense-in-depth) | ||
| const membership = await db.query.members.findFirst({ | ||
| where: and( | ||
| eq(members.organizationId, organizationId), | ||
| eq(members.userId, userId), | ||
| ), | ||
| }); | ||
|
|
||
| if (!membership) { | ||
| console.error("[github/callback] Membership verification failed:", { | ||
| organizationId, | ||
| userId, | ||
| }); | ||
| return Response.redirect( | ||
| `${env.NEXT_PUBLIC_WEB_URL}/integrations/github?error=unauthorized`, | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| const octokit = await githubApp.getInstallationOctokit( | ||
| Number(installationId), | ||
| ); | ||
|
|
||
| const installationResult = await octokit | ||
| .request("GET /app/installations/{installation_id}", { | ||
| installation_id: Number(installationId), | ||
| }) | ||
| .catch((error: Error) => { | ||
| console.error("[github/callback] Failed to fetch installation:", error); | ||
| return null; | ||
| }); | ||
|
|
||
| if (!installationResult) { | ||
| return Response.redirect( | ||
| `${env.NEXT_PUBLIC_WEB_URL}/integrations/github?error=installation_fetch_failed`, | ||
| ); | ||
| } | ||
|
|
||
| const installation = installationResult.data; | ||
|
|
||
| // Extract account info - account can be User or Enterprise | ||
| const account = installation.account; | ||
| const accountLogin = | ||
| account && "login" in account ? account.login : (account?.name ?? ""); | ||
| const accountType = | ||
| account && "type" in account ? account.type : "Organization"; | ||
|
|
||
| // Save the installation to our database | ||
| const [savedInstallation] = await db | ||
| .insert(githubInstallations) | ||
| .values({ | ||
| organizationId, | ||
| connectedByUserId: userId, | ||
| installationId: String(installation.id), | ||
| accountLogin, | ||
| accountType, | ||
| permissions: installation.permissions as Record<string, string>, | ||
| }) | ||
| .onConflictDoUpdate({ | ||
| target: [githubInstallations.organizationId], | ||
| set: { | ||
| connectedByUserId: userId, | ||
| installationId: String(installation.id), | ||
| accountLogin, | ||
| accountType, | ||
| permissions: installation.permissions as Record<string, string>, | ||
| suspended: false, | ||
| suspendedAt: null, // Clear suspension if reinstalling | ||
| updatedAt: new Date(), | ||
| }, | ||
| }) | ||
| .returning(); | ||
|
|
||
| if (!savedInstallation) { | ||
| return Response.redirect( | ||
| `${env.NEXT_PUBLIC_WEB_URL}/integrations/github?error=save_failed`, | ||
| ); | ||
| } | ||
|
|
||
| // Queue initial sync job | ||
| try { | ||
| await qstash.publishJSON({ | ||
| url: `${env.NEXT_PUBLIC_API_URL}/api/github/jobs/initial-sync`, | ||
| body: { | ||
| installationDbId: savedInstallation.id, | ||
| organizationId, | ||
| }, | ||
| retries: 3, | ||
| }); | ||
| } catch (error) { | ||
| console.error( | ||
| "[github/callback] Failed to queue initial sync job:", | ||
| error, | ||
| ); | ||
| return Response.redirect( | ||
| `${env.NEXT_PUBLIC_WEB_URL}/integrations/github?warning=sync_queue_failed`, | ||
| ); | ||
| } | ||
|
|
||
| return Response.redirect( | ||
| `${env.NEXT_PUBLIC_WEB_URL}/integrations/github?success=github_installed`, | ||
| ); | ||
| } catch (error) { | ||
| console.error("[github/callback] Unexpected error:", error); | ||
| return Response.redirect( | ||
| `${env.NEXT_PUBLIC_WEB_URL}/integrations/github?error=unexpected`, | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import { auth } from "@superset/auth/server"; | ||
| import { db } from "@superset/db/client"; | ||
| import { members } from "@superset/db/schema"; | ||
| import { and, eq } from "drizzle-orm"; | ||
|
|
||
| import { env } from "@/env"; | ||
| import { createSignedState } from "@/lib/oauth-state"; | ||
|
|
||
| export async function GET(request: Request) { | ||
| const session = await auth.api.getSession({ headers: request.headers }); | ||
|
|
||
| if (!session?.user) { | ||
| return Response.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const url = new URL(request.url); | ||
| const organizationId = url.searchParams.get("organizationId"); | ||
|
|
||
| if (!organizationId) { | ||
| return Response.json( | ||
| { error: "Missing organizationId parameter" }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| const membership = await db.query.members.findFirst({ | ||
| where: and( | ||
| eq(members.organizationId, organizationId), | ||
| eq(members.userId, session.user.id), | ||
| ), | ||
| }); | ||
|
|
||
| if (!membership) { | ||
| return Response.json( | ||
| { error: "User is not a member of this organization" }, | ||
| { status: 403 }, | ||
| ); | ||
| } | ||
|
|
||
| if (!env.GH_APP_ID) { | ||
| return Response.json( | ||
| { error: "GitHub App not configured" }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
|
|
||
| const state = createSignedState({ | ||
| organizationId, | ||
| userId: session.user.id, | ||
| }); | ||
|
|
||
| const installUrl = new URL( | ||
| "https://github.com/apps/superset-app/installations/new", | ||
| ); | ||
| installUrl.searchParams.set("state", state); | ||
|
|
||
| return Response.redirect(installUrl.toString()); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Add inline documentation for GitHub App credentials.
The new GitHub App credentials section lacks documentation to guide developers. Include comments explaining:
📝 Suggested documentation improvements
📝 Committable suggestion
🤖 Prompt for AI Agents