Skip to content
Open
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
76 changes: 76 additions & 0 deletions app/api/github/webhook/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { headers } from 'next/headers'
import { db } from '@/db'
import { installations, accounts } from '@/db/schema'
import { eq, and } from 'drizzle-orm'
import { verifyWebhookSignature } from '@/lib/github'
import crypto from 'crypto'

export async function POST(request: Request) {
try {
const headersList = await headers()
const signature = headersList.get('x-hub-signature-256')
const githubEvent = headersList.get('x-github-event')
const payload = await request.json()

console.log('payload',payload)

// Early return if not an installation event
if (githubEvent !== 'installation') {
return new Response('OK')
}

// Verify webhook signature
if (!signature || !verifyWebhookSignature(payload, signature)) {
console.error('Invalid webhook signature')
return new Response('Invalid signature', { status: 401 })
}

// Handle installation events
if (payload.action === 'created') {
const githubAccount = await db
.select()
.from(accounts)
.where(
and(
eq(accounts.providerAccountId, payload.sender.id.toString()),
eq(accounts.provider, 'github')
)
)
.limit(1)

if (!githubAccount[0]) {
console.error('No GitHub account found for user:', payload.sender.id)
return new Response('User not found', { status: 404 })
}

const repositoryIds = payload.repositories?.map((repo: any) => repo.id.toString()) || []

await db.insert(installations).values({
userId: githubAccount[0].userId,
installationId: payload.installation.id.toString(),
accountName: payload.installation.account.login,
repositoryIds,
})

console.log(`New installation created for ${payload.installation.account.login}`)
}

// Handle repository updates
if (['added', 'removed'].includes(payload.action)) {
await db
.update(installations)
.set({
repositoryIds: payload.repositories.map((repo: any) => repo.id.toString()),
updatedAt: new Date(),
})
.where(eq(installations.installationId, payload.installation.id.toString()))

console.log(`Repositories updated for installation ${payload.installation.id}`)
}

return new Response('OK')
} catch (error) {
console.error('Error processing webhook:', error)
return new Response('Internal Server Error', { status: 500 })
}
}
11 changes: 9 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { auth } from '@/auth'
import SignIn from '@/components/auth/SignIn'
import SignOut from '@/components/auth/SignOut'
import ConnectRepository from '@/components/repository/ConnectRepository'

export default async function Home() {
const session = await auth()
return (
<div className="flex h-screen items-center justify-center bg-black text-white">
<div className="flex flex-col gap-4 justify-center items-center">
<h1 className="text-4xl font-bold">Welcome to Perlify</h1>
<span>{ JSON.stringify(session)}</span>
{session?.user ? <SignOut /> : <SignIn />}
{session?.user ? (
<>
<ConnectRepository />
<SignOut />
</>
) : (
<SignIn />
)}
</div>
</div>
)
Expand Down
1 change: 1 addition & 0 deletions auth.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ import type { NextAuthConfig } from 'next-auth'
export default {
providers: [GitHub],
} satisfies NextAuthConfig

2 changes: 1 addition & 1 deletion auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import NextAuth from 'next-auth'
import { DrizzleAdapter } from '@auth/drizzle-adapter'
import { db } from '@/db/schema'
import { db } from '@/db'
import authConfig from '@/auth.config'


Expand Down
19 changes: 19 additions & 0 deletions components/repository/ConnectRepository.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client'


export default function ConnectRepository() {
const handleConnect = () => {
// GitHub App installation URL
const githubAppUrl = `https://github.com/apps/${process.env.NEXT_PUBLIC_GITHUB_APP_NAME}/installations/new`
window.location.href = githubAppUrl
}

return (
<button
onClick={handleConnect}
className="bg-violet-600 hover:bg-violet-700 px-3 py-1 rounded-md"
>
Connect Repositories
</button>
)
}
3 changes: 2 additions & 1 deletion db/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'
import * as schema from './schema'

import { config } from 'dotenv'

config({ path: '.env.local' })

const sql = neon(process.env.DATABASE_URL!)
export const db = drizzle({ client: sql })
export const db = drizzle(sql, { schema })
34 changes: 19 additions & 15 deletions db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,17 @@ import {
boolean,
timestamp,
pgTable,
text,
text,
primaryKey,
integer,
index,
} from 'drizzle-orm/pg-core'
import postgres from 'postgres'
import { drizzle } from 'drizzle-orm/postgres-js'
import type { AdapterAccountType } from 'next-auth/adapters'

const pool = postgres(process.env.DATABASE_URL!, { max: 1 })

export const db = drizzle(pool)


export const users = pgTable('user', {
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text('name'),
email: text('email').unique(),
emailVerified: timestamp('emailVerified', { mode: 'date' }),
Expand All @@ -27,9 +22,8 @@ export const users = pgTable('user', {
export const accounts = pgTable(
'account',
{
userId: text('userId')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
id: text('id').$defaultFn(() => crypto.randomUUID()).unique(),
userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
type: text('type').$type<AdapterAccountType>().notNull(),
provider: text('provider').notNull(),
providerAccountId: text('providerAccountId').notNull(),
Expand All @@ -42,9 +36,9 @@ export const accounts = pgTable(
session_state: text('session_state'),
},
(account) => ({
compoundKey: primaryKey({
columns: [account.provider, account.providerAccountId],
}),
providerAccountIdx: primaryKey({ columns: [account.provider, account.providerAccountId] }),
// Add an index for userId lookups
userIdx: index('user_id_idx').on(account.userId),
})
)

Expand Down Expand Up @@ -90,3 +84,13 @@ export const authenticators = pgTable(
}),
})
)

export const installations = pgTable('installation', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
installationId: text('installationId').notNull().unique(),
accountName: text('accountName').notNull(),
repositoryIds: text('repositoryIds').array(),
createdAt: timestamp('createdAt').defaultNow(),
updatedAt: timestamp('updatedAt').defaultNow(),
})
18 changes: 18 additions & 0 deletions lib/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import crypto from 'crypto'

export function verifyWebhookSignature(payload: any, signature: string) {
const sigHashAlg = 'sha256'
const sigHash = crypto
.createHmac(sigHashAlg, process.env.GITHUB_WEBHOOK_SECRET!)
.update(JSON.stringify(payload))
.digest('hex')

const expectedSignature = `sha256=${sigHash}`
console.log('Webhook signature verification:', {
received: signature,
expected: expectedSignature,
matches: expectedSignature === signature
})

return expectedSignature === signature
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
CREATE TABLE IF NOT EXISTS "account" (
"id" text,
"userId" text NOT NULL,
"type" text NOT NULL,
"provider" text NOT NULL,
Expand All @@ -10,7 +11,8 @@ CREATE TABLE IF NOT EXISTS "account" (
"scope" text,
"id_token" text,
"session_state" text,
CONSTRAINT "account_provider_providerAccountId_pk" PRIMARY KEY("provider","providerAccountId")
CONSTRAINT "account_provider_providerAccountId_pk" PRIMARY KEY("provider","providerAccountId"),
CONSTRAINT "account_id_unique" UNIQUE("id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "authenticator" (
Expand All @@ -26,6 +28,17 @@ CREATE TABLE IF NOT EXISTS "authenticator" (
CONSTRAINT "authenticator_credentialID_unique" UNIQUE("credentialID")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "installation" (
"id" text PRIMARY KEY NOT NULL,
"userId" text NOT NULL,
"installationId" text NOT NULL,
"accountName" text NOT NULL,
"repositoryIds" text[],
"createdAt" timestamp DEFAULT now(),
"updatedAt" timestamp DEFAULT now(),
CONSTRAINT "installation_installationId_unique" UNIQUE("installationId")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "session" (
"sessionToken" text PRIMARY KEY NOT NULL,
"userId" text NOT NULL,
Expand All @@ -48,7 +61,6 @@ CREATE TABLE IF NOT EXISTS "verificationToken" (
CONSTRAINT "verificationToken_identifier_token_pk" PRIMARY KEY("identifier","token")
);
--> statement-breakpoint
DROP TABLE "users";--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
Expand All @@ -61,8 +73,16 @@ EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "installation" ADD CONSTRAINT "installation_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "user_id_idx" ON "account" USING btree ("userId");
8 changes: 0 additions & 8 deletions migrations/0000_many_longshot.sql

This file was deleted.

Loading