Automating access to a GitHub Repo using Stripe Webhooks and Astro Endpoints
Tags: webdev, javascript, tutorial, astro
Published: 2024-01-19 06:40:28

In this tutorial, I'll walk you through the steps of leveraging Astro Endpoints to verify Stripe Webhooks, and seamlessly grant access to a GitHub repository, all condensed into concise and developer-friendly code. I'll dissect the process into digestible steps, making it effortless for you to integrate these powerful features into your web projects.

## Prerequisites

Before we dive into the process, make sure you have the following prerequisites in place:

- An Astro project: [Create your Astro project](https://docs.astro.build/en/getting-started/) if you haven't already. You can set up a new project by `npm create astro@latest`

- Stripe Account: You will need a [Stripe account](https://dashboard.stripe.com/) to handle payment transactions. Sign up at stripe.com, and obtain your [Stripe Secret Key](https://stripe.com/docs/keys) and [Webhook Signing Secret](https://stripe.com/docs/webhooks#roll-endpoint-secrets)

- Stripe Checkout Fields: When creating a checkout url, add a [custom field](https://stripe.com/docs/payments/checkout/custom-fields) named `GitHub Username`

- GitHub Repository: Create a [GitHub](https://github.com) repository where you want to grant access to users upon successful payment

- GitHub Personal Access Token: Obtain a [GitHub Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) that allows admin access to the repo

## Installation

Once setup with prerequisites, install `stripe` npm package via:

```bash
npm install stripe
```

## Add Stripe environment variables

Add Stripe Secret Key as `STRIPE_SECRET_KEY` & Webhook Signing Secret as `STRIPE_WEBHOOK_SIG` as the environment variables in your environment (usually it is an .env file).

## Create Webhook Handler Endpoint in your Astro project

```typescript
// File: src/pages/api/stripe/webhook.ts

// Stripe API Reference
// https://stripe.com/docs/webhooks#webhook-endpoint-def
export async function POST({ request }: APIContext) {
  try {
    const STRIPE_SECRET_KEY = import.meta.env.STRIPE_SECRET_KEY
    const STRIPE_WEBHOOK_SIG = import.meta.env.STRIPE_WEBHOOK_SIG
    const GITHUB_PAT_TOKEN = import.meta.env.GITHUB_PAT_TOKEN
    if (!STRIPE_SECRET_KEY || !STRIPE_WEBHOOK_SIG || !GITHUB_PAT_TOKEN) {
      return new Response(null, {
        status: 500,
      })
    }
    const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2023-08-16' })
    // Verify Stripe Signature to protect the endpoint
    // Send GitHub Access to the repo once verified
  } catch (e) {
    return new Response(e.message || e.toString(), { status: 500 })
  }
}
```

## Verify Stripe Signature in your Astro Endpoint

To verify stripe signatures, you need to get access to the raw body. Use the `rawBody` function to get that done for you 👇🏻

```typescript
// File: src/pages/api/stripe/webhook.ts

// Process rawBody from the request Object
async function getRawBody(request: Request) {
  let chunks = []
  let done = false
  const reader = request.body.getReader()
  while (!done) {
    const { value, done: isDone } = await reader.read()
    if (value) {
      chunks.push(value)
    }
    done = isDone
  }
  const bodyData = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0))
  let offset = 0
  for (const chunk of chunks) {
    bodyData.set(chunk, offset)
    offset += chunk.length
  }
  return Buffer.from(bodyData)
}
```

With that done, now verify the request using the `rawBody` function as below using stripe's `constructEvent` function 👇🏻

```typescript
// File: src/pages/api/stripe/webhook.ts

export async function POST({ request }: APIContext) {
    // Verify Stripe Signature to protect the endpoint
    const rawBody = await getRawBody(request)
    let event = JSON.parse(rawBody.toString())
    const sig = request.headers.get('stripe-signature')
    try {
      event = stripe.webhooks.constructEvent(rawBody, sig, STRIPE_WEBHOOK_SIG)
    } catch (err) {
      console.log(err.message)
      return new Response(`Webhook Error: ${err.message}`, {
        status: 400,
      })
    }
    // Send GitHub Access to the repo once verified
}
```

## Add GitHub environment variable

Add GitHub Personal Access Token as `GITHUB_PAT_TOKEN` as an environment variable in your environment (usually it is an .env file).

## Send GitHub Access to the repo once verified

To send access to a GitHub repo, you need to make a PUT request using the GitHub API. Use the `sendAccess` function to get that done for you 👇🏻

```typescript
// File: src/pages/api/stripe/webhook.ts

// Send access to a github repo using custom field in Stripe checkout
async function sendAccess(paymentIntent) {
  const username = paymentIntent['custom_fields'].find((i) => i.key === 'githubusername').text.value
  await fetch(`https://api.github.com/repos/repo-owner/repo-name/collaborators/${username}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `token ${import.meta.env.GITHUB_PAT_TOKEN}`,
    },
    body: JSON.stringify({ permission: 'read' }),
  })
}
```

With that done, now select the Stripe Events for which you need to automate the access. Right now, I'm demonstrating sending access when a Checkout Session is completed or a Payment is made successfully 👇🏻

```typescript
// File: src/pages/api/stripe/webhook.ts

export async function POST({ request }: APIContext) {
    // Send GitHub Access to the repo once verified
    if (event.type === 'checkout.session.completed') {
        return await sendAccess(event.data.object)
    }
    if (event.type === 'payment_intent.succeeded') {
        return await sendAccess(event.data.object)
    }
}
```

## You're Done!

You've successfully set up verification of Stripe Webhooks and automated granting access to a GitHub repository using Astro Endpoints. This powerful combination of technologies allows you to create seamless payment and access management for your web projects.

Happy coding!