Skip to content

sandhi18/github-integration

Repository files navigation

GitHub Integration Service

A production-ready Go backend REST API that connects GitHub accounts, surfaces user profiles and repositories, manages repository subscriptions, and delivers pull-request notifications through a RabbitMQ-backed async pipeline.


Table of Contents

  1. Prerequisites
  2. GitHub OAuth App Setup
  3. Configuration
  4. Running with Docker (recommended)
  5. Running Locally (without Docker)
  6. Using the Frontend
  7. API Reference
  8. Testing the OAuth Flow (API)
  9. Webhook Setup
  10. Makefile Reference
  11. Project Structure

Prerequisites

Requirement Version Notes
Docker Desktop Latest Required — all infrastructure (Postgres, Redis, RabbitMQ) runs in containers
Go 1.26.3+ Only needed if running outside Docker
GitHub account Needed to create an OAuth App and test the auth flow

Note: If you only plan to run the service through Docker Compose, you do not need Go installed locally.


GitHub OAuth App Setup

  1. Open https://github.com/settings/applications/new in your browser.

  2. Fill in the form:

    Field Value
    Application name github-integration-local (or anything you like)
    Homepage URL http://localhost:8080
    Authorization callback URL http://localhost:8080/auth/github/callback/index.html

    The callback URL points to index.html because the frontend handles the OAuth code exchange on page load. If you are using the API flow without the frontend, set it to http://localhost:8080/api/auth/github/callback instead.

  3. Click Register application.

  4. On the next page:

    • Copy the Client ID — you will need it for github.client_id in config.yaml.
    • Click Generate a new client secret, then copy the generated value — you will need it for github.client_secret.

⚠️ The client secret is shown once. Save it immediately.


Configuration

All configuration lives in config.yaml at the project root. The file is mounted read-only into the Docker container; environment variables set in docker-compose.yml override infra connection values (host, port, credentials) automatically via Viper.

You must set the following fields before the service will start correctly:

Key How to generate / where to find
github.client_id From your GitHub OAuth App page
github.client_secret From your GitHub OAuth App page
github.oauth_redirect_url http://localhost:8080/auth/github/callback (matches OAuth App)
jwt.private_key openssl genrsa -out private.pem 2048
jwt.public_key penssl rsa -in private.pem -pubout -out public.pem
encryption.token_key openssl rand -hex 32 (must be exactly 32 bytes / 64 hex chars)
webhook.secret Set after creating the webhook in GitHub (see Webhook Setup)
internal.service_key Any random string — used to authenticate POST /internal/notifications/pr

Full config.yaml with placeholders:

server:
  port: 8080
  env: development

database:
  host: localhost          # overridden to "postgres" in Docker
  port: 5432
  name: github_int
  user: 
  password: 
  sslmode: disable
  max_open_conns: 25
  max_idle_conns: 5

redis:
  addr: localhost:6379     # overridden to "redis:6379" in Docker
  password: ""
  db: 0

rabbitmq:
  url: amqp://guest:guest@localhost:5672/   # overridden in Docker
  exchange: github.events
  routing_key: pull_request.created
  queue: email.pr.notifications

github:
  client_id: ""            # REQUIRED — from GitHub OAuth App
  client_secret: ""        # REQUIRED — from GitHub OAuth App
  oauth_redirect_url: "http://localhost:8080/auth/github/callback"

jwt:
  private_key: |
    -----BEGIN RSA PRIVATE KEY-----
    <paste private key content here>
    -----END RSA PRIVATE KEY-----
  public_key: |
    -----BEGIN PUBLIC KEY-----
    <paste public key content here>
    -----END PUBLIC KEY-----
  expiry_minutes: 15

encryption:
  token_key: ""            # REQUIRED — openssl rand -hex 32 (64 hex chars = 32 bytes)

webhook:
  secret: ""               # Set after creating the webhook in GitHub
  app_url: ""              # Publicly reachable URL, e.g. https://your-app.ngrok.io

internal:
  service_key: ""          # Any random string for POST /internal/notifications/pr

Running with Docker (recommended)

Make sure Docker Desktop is running, then:

# 1. Build the app Docker image
make build

# 2. Start PostgreSQL, Redis, RabbitMQ, and the app
make up

# 3. Apply the database migration (run once after first 'make up')
make migrate

# 4. Tail app logs
make logs

The app container waits for all three infrastructure services to pass their health checks before it starts. No manual ordering is required.

To stop everything:

make down

Data volumes (pgdata, redisdata, rmqdata) are preserved across make down / make up cycles. Use docker compose down -v to wipe them.

Infrastructure management UIs (available while containers are running):

Service URL Credentials
RabbitMQ Management http://localhost:15672 guest / guest

Running Locally (without Docker)

Requires Go 1.26.3+ installed locally.

Step 1 — Start infrastructure

Either start the full stack with Docker Compose and stop only the app container, or start infra services individually:

docker compose up -d postgres redis rabbitmq

Step 2 — Apply the database migration

psql -h localhost -U postgres -d github_int -f migrations/001_initial_schema.sql

Step 3 — Run the service

go run ./cmd/main.go

The server starts on http://localhost:8080.


Using the Frontend

The service ships a minimal GitHub-themed single-page frontend served from the /frontend directory at http://localhost:8080.

Pages

URL Description
/index.html Login page — initiates GitHub OAuth and handles the callback
/landing.html Dashboard — profile summary + navigation cards
/repos.html Browse your GitHub repositories; subscribe with one click
/subscriptions.html View and manage active repo subscriptions
/profile.html Full GitHub profile details (bio, stats, followers)

Frontend Login Flow

Browser                          App (Go)                    GitHub
  │                                 │                            │
  │  GET /index.html                │                            │
  │────────────────────────────────>│                            │
  │<────────────────────────────────│                            │
  │                                 │                            │
  │  GET /api/auth/github/login     │                            │
  │────────────────────────────────>│                            │
  │  { authorizeUrl }               │                            │
  │<────────────────────────────────│                            │
  │                                 │                            │
  │  window.location = authorizeUrl │                            │
  │────────────────────────────────────────────────────────────>│
  │                                 │                            │
  │  Redirect to /index.html        │                            │
  │  ?code=...&state=...            │                            │
  │<────────────────────────────────────────────────────────────│
  │                                 │                            │
  │  GET /api/auth/github/callback  │                            │
  │  ?code=...&state=...            │                            │
  │────────────────────────────────>│                            │
  │                                 │  Exchange code for token   │
  │                                 │───────────────────────────>│
  │                                 │  { access_token }          │
  │                                 │<───────────────────────────│
  │  { accessToken, refreshToken,   │                            │
  │    expiresAt, user }            │                            │
  │<────────────────────────────────│                            │
  │                                 │                            │
  │  Store tokens in localStorage   │                            │
  │  Redirect to /landing.html      │                            │

Token Lifecycle (Frontend)

  • Tokens are stored in localStorage (accessToken, refreshToken, expiresAt).
  • A scheduleRefresh() timer fires automatically 60 seconds before expiresAt, calling POST /api/auth/refresh and rotating both tokens.
  • On 401 or 410 responses from any API call, the frontend clears localStorage and redirects to /index.html.
  • The back-button bfcache is handled via a pageshow listener — navigating back to a protected page after logout redirects to login.

Navigating the App

  1. Open http://localhost:8080 (or http://localhost:8080/index.html).
  2. Click Sign in with GitHub — you will be redirected to GitHub for authorization.
  3. After authorizing, you land on the Dashboard (/landing.html).
  4. Use the navigation cards to browse Repositories or manage Subscriptions.
  5. On the Repositories page, click Subscribe on any repo to receive PR notifications.
  6. Click Sign out (top-right) to log out of the current session, or use the API to log out all sessions.

API Reference

All authenticated endpoints require an Authorization: Bearer <accessToken> header.

Method Path Auth Description
GET /api/auth/github/login None Returns { authorizeUrl } — the GitHub OAuth authorization URL. Does not redirect.
GET /api/auth/github/callback None Exchanges the OAuth code for a GitHub token, upserts the user, and returns { accessToken, refreshToken, expiresAt, user }.
POST /api/auth/refresh Bearer JWT Validates the current JWT and refresh token, then issues a new access token and rotates the refresh token. Returns 401 for expired JWT, 410 for a blocked session.
POST /api/auth/logout Bearer JWT Blocks the current session, deletes the stored GitHub token, and invalidates the user's Redis cache.
POST /api/auth/logout-all Bearer JWT Blocks all active sessions for the user, deletes all stored GitHub tokens, and invalidates the Redis cache.
GET /api/profile Bearer JWT Returns the authenticated user's GitHub profile. Cache-first (profile:{user_id}, 15 min TTL); falls back to GET /user on the GitHub API.
GET /api/repos Bearer JWT Returns the user's GitHub repositories. Supports ?page=, ?per_page=, and ?visibility= query params. Cache-first (90 min TTL); falls back to GET /user/repos.
POST /api/subscriptions Bearer JWT Subscribe to a repository. Validates repo ownership via cache or GitHub API. Returns 409 on duplicate.
GET /api/subscriptions Bearer JWT Returns a paginated list of the authenticated user's subscriptions. Supports ?page= and ?per_page=.
GET /api/subscriptions/{subId} Bearer JWT Returns a single subscription. Returns 403 if the subscription belongs to a different user.
DELETE /api/subscriptions/{subId} Bearer JWT Soft-deletes a subscription (sets is_active = false). Returns 403 on ownership mismatch.
POST /api/webhooks/github HMAC signature Receives GitHub webhook events. Validates X-Hub-Signature-256, publishes pull_request events to RabbitMQ, and returns 202 Accepted.
POST /api/internal/notifications/pr Service key Triggers fan-out email notification delivery for a PR event (max 10 concurrent goroutines). Logs email payload; does not send real email.

Error response shape

All errors return JSON:

{ "error": "human-readable message" }
Status Meaning
400 Validation failed or missing required field
401 Missing, invalid, or expired JWT
403 Authenticated user does not own the requested resource
404 Resource not found
409 Conflict (e.g. duplicate subscription)
410 Session has been blocked or explicitly logged out
422 Repository inaccessible or not in the user's repo list
429 GitHub API rate limit exceeded
500 Internal server error (details logged server-side only)

Testing the OAuth Flow (API)

Follow this happy-path sequence with any HTTP client (curl, Postman, Insomnia, etc.) to test the API directly without the frontend:

1. Start the login flow

curl http://localhost:8080/auth/github/login

Response:

{
  "authorizeUrl": "https://github.com/login/oauth/authorize?client_id=...&state=...&scope=..."
}

2. Authorize in the browser

Open the authorizeUrl value in a browser. Sign in to GitHub if prompted, then click Authorize.

GitHub redirects to http://localhost:8080/auth/github/callback?code=...&state=....

3. Capture the access token

The callback response body contains:

{
  "accessToken": "eyJ...",
  "refreshToken": "...",
  "expiresAt": "2026-06-11T21:03:08Z",
  "user": {
    "id": "...",
    "username": "...",
    "email": "...",
    "avatarUrl": "..."
  }
}

Copy the accessToken value.

4. Call authenticated endpoints

Pass the token as a Bearer header on all subsequent requests:

curl -H "Authorization: Bearer <accessToken>" http://localhost:8080/profile
curl -H "Authorization: Bearer <accessToken>" \
     "http://localhost:8080/repos?visibility=public&page=1&per_page=10"

5. Subscribe to a repository

curl -X POST http://localhost:8080/subscriptions \
  -H "Authorization: Bearer <accessToken>" \
  -H "Content-Type: application/json" \
  -d '{"repoFullName": "owner/repo-name"}'

Webhook Setup

Full step-by-step instructions — including ngrok configuration, GitHub webhook registration, and local testing — are in docs/webhook-setup.md.

Quick summary:

  1. Expose the local service with a public URL (e.g. ngrok http 8080).
  2. Set webhook.app_url and webhook.secret in config.yaml, then restart the service.
  3. Register the webhook in the target GitHub repository under Settings → Webhooks.
  4. Set the Payload URL to {app_url}/webhooks/github, content type application/json, and enable Pull requests events.

The service does not auto-register webhooks. Each repository must be configured manually.


Makefile Reference

Target Command Description
build make build (Re)build the app Docker image via docker compose build
up make up Start all services in detached mode (docker compose up -d)
down make down Stop and remove all containers (named volumes are preserved)
migrate make migrate Apply migrations/001_initial_schema.sql inside the running Postgres container
logs make logs Tail the app container logs (docker compose logs -f app)

Project Structure

github-integration/
├── cmd/
│   └── main.go                    # Server startup: DB/Redis/RMQ init, route registration, worker startup
├── config/
│   └── config.go                  # Config struct + Viper loader
├── config.yaml                    # All infra credentials and app config (edit before running)
├── frontend/
│   ├── index.html                 # Login page — OAuth initiation + callback handling
│   ├── landing.html               # Dashboard — profile summary + navigation cards
│   ├── repos.html                 # Repository browser with subscribe/unsubscribe
│   ├── subscriptions.html         # Active subscriptions list + management
│   └── profile.html               # Full GitHub profile details
├── internal/
│   ├── cache/
│   │   └── cache.go               # Redis helper: get / set / delete with TTL
│   ├── github/
│   │   └── client.go              # GitHub REST API client (profile, repos, token exchange)
│   ├── handler/
│   │   ├── auth.go                # Auth handlers: login, callback, refresh, logout, logout-all
│   │   ├── internal.go            # Internal handler: POST /internal/notifications/pr
│   │   ├── profile.go             # Profile handler: GET /profile
│   │   ├── subscription.go        # Subscription CRUD handlers
│   │   ├── webhook.go             # Webhook handler: POST /webhooks/github
│   │   └── util.go                # Shared handler helpers (JSON write, error response)
│   ├── middleware/
│   │   ├── auth.go                # JWT Bearer auth middleware (validates + injects user/session IDs)
│   │   ├── context.go             # Context key definitions
│   │   ├── service_key.go         # Service-key middleware for internal endpoints
│   │   ├── util.go                # Middleware utilities
│   │   └── webhook.go             # HMAC-SHA256 signature validation middleware
│   ├── models/
│   │   └── models.go              # GORM model structs (User, Session, GithubToken, Subscription, …)
│   ├── notification/
│   │   └── notification.go        # Email template builder + slog logger (no real SMTP)
│   ├── pubsub/
│   │   └── pubsub.go              # RabbitMQ publisher + consumer (topic exchange, durable queue)
│   ├── repository/
│   │   └── repository.go          # All DB queries (no business logic; accepts context.Context)
│   └── service/
│       ├── auth.go                # Auth business logic: OAuth flow, JWT creation, session management
│       ├── errors.go              # Typed service errors mapped to HTTP status codes
│       ├── notification.go        # Notification fan-out: semaphore, retry, delivered_at tracking
│       ├── profile.go             # Profile service: cache-first GitHub API fetch
│       ├── security.go            # AES-GCM token encryption / decryption helpers
│       ├── subscription.go        # Subscription service: repo validation, upsert, soft delete
│       └── webhook.go             # Webhook service: event parsing + RabbitMQ publish
├── migrations/
│   └── 001_initial_schema.sql     # All CREATE TABLE, indexes, and constraints
├── docs/
│   └── webhook-setup.md           # Manual webhook registration guide (ngrok + GitHub UI)
├── Dockerfile                     # Multi-stage Go build → minimal runtime image
├── docker-compose.yml             # Postgres 16, Redis 7, RabbitMQ 3, app (with health checks)
├── Makefile                       # build / up / down / migrate / logs targets
├── go.mod
└── go.sum

About

GitHub Integration Web Application

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages