Skip to content
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

feat: connect github app to our web app #6

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/(dashboard)/select-repo/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use server"

import { selectRepository } from "@/github/services/repository"

export const selectRepoActions = async (id: string) => {
return await selectRepository(id)
}
13 changes: 13 additions & 0 deletions app/(dashboard)/select-repo/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DashboardHeader } from "@/components/header"
import { DashboardShell } from "@/components/shell"

export default function SelectRepoLoading() {
return (
<DashboardShell>
<DashboardHeader
heading="Connect a Repository"
text="Select the repository you want to integrate oss.gg with."
/>
</DashboardShell>
)
}
43 changes: 43 additions & 0 deletions app/(dashboard)/select-repo/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { redirect } from "next/navigation"
import { getRepositoriesForUser } from "@/github/services/repository"

import { authOptions } from "@/lib/auth"
import { db } from "@/lib/db"
import { getCurrentUser } from "@/lib/session"
import { DashboardHeader } from "@/components/header"
import { RepoSelector } from "@/components/repo-selecor"
import { DashboardShell } from "@/components/shell"

import { selectRepoActions } from "./actions"

export const metadata = {
title: "Connect a Repository",
description: "Select the repository you want to integrate oss.gg with.",
}

export default async function SelectRepoPage() {
const user = await getCurrentUser()
if (!user) {
redirect(authOptions?.pages?.signIn || "/login")
}

const repos = await getRepositoriesForUser(user.id)

return (
<DashboardShell>
<DashboardHeader
heading="Connect a Repository"
text="Select the repository you want to integrate oss.gg with."
/>
<div className="space-y-2">
{repos.map((repo) => (
<RepoSelector
key={repo.id}
repo={repo}
selectRepoAction={selectRepoActions}
/>
))}
</div>
</DashboardShell>
)
}
2 changes: 1 addition & 1 deletion app/[profile]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const metadata = {
title: "Profile Page",
}

async function fetchGithubUserData(userName) {
async function fetchGithubUserData(userName: string) {
const res = await fetch(`https://api.github.com/users/${userName}`)
const data = await res.json()
return data
Expand Down
21 changes: 21 additions & 0 deletions components/repo-selecor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client"

import { useToast } from "@/components/ui/use-toast"

export const RepoSelector = ({ repo, selectRepoAction }) => {
const { toast } = useToast()
return (
<div
className="rounded-md p-3 flex space-x-3 items-center hover:bg-slate-10 hover:scale-102 border border-transparent hover:border-slate-200 transition-all hover:cursor-pointer ease-in-out duration-150"
onClick={() => {
selectRepoAction(repo.id)
toast({
title: `${repo.name} selected`,
description: "Next steps to be built",
})
}}
>
{repo.name}
</div>
)
}
40 changes: 40 additions & 0 deletions github/services/repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { db } from "@/lib/db"

export const selectRepository = async (id: string) => {
try {
const selectedRepository = await db.repository.update({
where: {
id,
},
data: {
configured: true,
},
})
return selectedRepository
} catch (error) {
throw new Error(`Failed to select repository: ${error}`)
}
}

export const getRepositoriesForUser = async (userId: string) => {
try {
const installationIds = await db.membership.findMany({
where: {
userId,
},
})

const repos = await db.repository.findMany({
where: {
installationId: {
in: installationIds.map((id) => id.installationId),
},
configured: false,
},
})
repos.sort((a, b) => a.name.localeCompare(b.name))
return repos
} catch (error) {
throw new Error(`Failed to get repositories for user: ${error}`)
}
}
175 changes: 87 additions & 88 deletions github/services/user.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { readFileSync } from "fs"
import path from "path"
import { createAppAuth } from "@octokit/auth-app"
import { Octokit } from "@octokit/rest"
import { App } from "octokit"

import { env } from "@/env.mjs"
import { db } from "@/lib/db"

const privateKeyPath = "../../../../key.pem"
Expand All @@ -12,119 +12,118 @@ const privateKey = readFileSync(resolvedPath, "utf8")
export const sendInstallationDetails = async (
installationId: number,
appId: number,
repos: any,
repos:
| {
id: number
node_id: string
name: string
full_name: string
private: boolean
}[]
| undefined,
installation: any
): Promise<unknown> => {
): Promise<void> => {
try {
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId,
installationId,
privateKey,
const app = new App({
appId,
privateKey,
webhooks: {
secret: env.GITHUB_WEBHOOK_SECRET!,
},
})
const octokit = await app.getInstallationOctokit(installationId)

const installationPrisma = await db.installation.create({
data: {
githubId: installationId,
type: installation?.account?.type.toLowerCase(),
},
})
await db.$transaction(async (tx) => {
const installationPrisma = await tx.installation.upsert({
where: { githubId: installationId },
update: { type: installation?.account?.type.toLowerCase() },
create: {
githubId: installationId,
type: installation?.account?.type.toLowerCase(),
},
})

if (installationPrisma.type === "organization") {
try {
const userType = installation?.account?.type.toLowerCase()
if (userType === "organization") {
const membersOfOrg = await octokit.rest.orgs.listMembers({
org: installation?.account?.login,
role: "all",
})

membersOfOrg.data.map(async (member) => {
// check if this member exists in the database
// if not, create a new member

const memberInDatabase = await db.user.findUnique({
where: {
githubId: member.id,
},
})

if (!memberInDatabase) {
const newUser = await db.user.upsert({
where: {
githubId: member.id,
},
await Promise.all(
membersOfOrg.data.map(async (member) => {
const newUser = await tx.user.upsert({
where: { githubId: member.id },
update: {},
create: {
githubId: member.id,
login: member.login,
name: member.name,
email: member.email,
},
update: {
githubId: member.id,
login: member.login,
name: member.name,
email: member.email,
},
})

// create a new membership
const newMembership = await db.membership.create({
data: {
user: {
connect: {
id: newUser.id,
},
await tx.membership.upsert({
where: {
userId_installationId: {
userId: newUser.id,
installationId: installationPrisma.id,
},
installation: { connect: { id: installationPrisma.id } },
},
update: {},
create: {
userId: newUser.id,
installationId: installationPrisma.id,
role: "member",
},
})
}

// check if this member has a membership
// if not, create a new membership
})
)
} else {
const user = installation.account
const newUser = await tx.user.upsert({
where: { githubId: user.id },
update: {},
create: {
githubId: user.id,
login: user.login,
name: user.name || "",
email: user.email || "",
},
})
} catch (error) {
console.error({ error })
}
} else {
const user = installation.account

const newUser = await db.user.upsert({
where: {
githubId: user.id,
},
create: {
githubId: user.id,
login: user.login,
name: user.name,
email: user.email,
},
update: {
githubId: user.id,
login: user.login,
name: user.name,
email: user.email,
},
})

// create a new membership
const newMembership = await db.membership.create({
data: {
user: {
connect: {
id: newUser.id,
await tx.membership.upsert({
where: {
userId_installationId: {
userId: newUser.id,
installationId: installationPrisma.id,
},
},
installation: { connect: { id: installationPrisma.id } },
role: "owner",
},
})
}
update: {},
create: {
userId: newUser.id,
installationId: installationPrisma.id,
role: "owner",
},
})
}

return { data: "data" }
if (repos) {
await Promise.all(
repos.map(async (repo) => {
await tx.repository.upsert({
where: { githubId: repo.id },
update: {},
create: {
githubId: repo.id,
name: repo.name,
installationId: installationPrisma.id,
},
})
})
)
}
})
} catch (error) {
console.error(`Failed to post installation details: ${error}`)
throw new Error(`Failed to post installation details: ${error}`)
}
}
3 changes: 2 additions & 1 deletion github/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { Octokit } from "@octokit/rest"

import { GITHUB_APP_APP_ID } from "./constants"

const resolvedPath = path.resolve(__dirname, "../../../../key.pem")
const privateKeyPath = "../../../../key.pem"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ShubhamPalriwala How can we solve the key.pem requirement in the cloud?

const resolvedPath = path.resolve(__dirname, privateKeyPath)
const privateKey = readFileSync(resolvedPath, "utf8")

export const getOctokitInstance = (installationId: number) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "repositories" ADD COLUMN "configured" BOOLEAN NOT NULL DEFAULT false,
ALTER COLUMN "default_branch" DROP NOT NULL;
3 changes: 2 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ model Repository {
description String?
homepage String?
topics String[]
default_branch String
default_branch String?
installationId String
configured Boolean @default(false)

levels Json @default("[]")
pointTransactions PointTransaction[]
Expand Down
Loading