GitHub Integration
OAuth authentication and repository access
📋 Overview
GitHub Integration provides OAuth-based authentication using Better Auth, enabling users to log in with their GitHub account and access their repositories for analysis and test generation.
🎯 Current Status
Status: ✅ Implemented
GitHub OAuth is fully functional for authentication. The GitHub App side (installation-scoped webhooks, Check Runs, sticky PR comments, and the PR-event delta orchestrator) is also live; see apps/api/src/routes/githubApp.ts and packages/trpc/src/services/prDeltaOrchestrator.ts.
🏗️ Authentication Architecture
Better Auth Configuration
Location: packages/auth/src/index.ts
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg'
}),
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
redirectURI: process.env.GITHUB_REDIRECT_URI ||
`${process.env.BETTER_AUTH_URL}/auth/callback/github`
}
},
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24 // 1 day
}
})
🔐 OAuth Flow
1. User Clicks "Sign in with GitHub"
// Frontend button
<Button asChild>
<a href={`${API_URL}/api/auth/github`}>
<GithubIcon /> Sign in with GitHub
</a>
</Button>
2. Redirect to GitHub
GET https://github.com/login/oauth/authorize
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://api.yourdomain.com/auth/callback/github
&scope=read:user,user:email,repo
3. GitHub Callback
// Better Auth handles this automatically
// Exchanges code for access token
// Creates/updates user in database
// Creates session
// Redirects to frontend
4. Frontend Receives Session
// Frontend callback handler
export async function GitHubCallback() {
const searchParams = useSearchParams()
const session = searchParams.get('session')
if (session) {
// Store session
localStorage.setItem('session', session)
// Redirect to dashboard
navigate('/dashboard')
}
}
🔌 API Integration
Database Schema
accounts table (Better Auth):
{
id: string
userId: string
providerId: 'github'
accountId: string // GitHub user ID
accessToken: string // OAuth access token
refreshToken: string?
accessTokenExpiresAt: Date?
scope: string
createdAt: Date
updatedAt: Date
}
Accessing GitHub API
import { Octokit } from '@octokit/rest'
async function getUserRepositories(userId: string) {
// Get GitHub access token from accounts table
const account = await db.query.accounts.findFirst({
where: and(
eq(accounts.userId, userId),
eq(accounts.providerId, 'github')
)
})
if (!account?.accessToken) {
throw new Error('GitHub not connected')
}
// Use Octokit with user's token
const octokit = new Octokit({
auth: account.accessToken
})
const { data } = await octokit.repos.listForAuthenticatedUser({
sort: 'updated',
per_page: 100
})
return data
}
🎨 Frontend Implementation
GitHub Sign In Button
import { GithubIcon } from 'lucide-react'
import { Button } from '~/components/ui/button'
export function GitHubSignIn() {
const apiUrl = import.meta.env.VITE_API_URL
return (
<Button asChild variant="outline" className="w-full">
<a href={`${apiUrl}/api/auth/github`}>
<GithubIcon className="mr-2 h-4 w-4" />
Sign in with GitHub
</a>
</Button>
)
}
Repository Selector
export function RepositorySelector({ onSelect }: Props) {
const { data, isLoading } = trpc.github.listRepositories.useQuery()
return (
<Command>
<CommandInput placeholder="Search repositories..." />
<CommandList>
{isLoading && <CommandLoading />}
<CommandGroup heading="Your Repositories">
{data?.repositories.map((repo) => (
<CommandItem
key={repo.id}
onSelect={() => onSelect(repo)}
>
<GithubIcon className="mr-2" />
<div>
<div>{repo.name}</div>
<div className="text-sm text-muted-foreground">
{repo.fullName}
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
)
}
🪝 Webhooks (Planned)
GitHub Webhook Setup
Planned for Phase 4:
// Register webhook with GitHub
POST https://api.github.com/repos/owner/repo/hooks
{
"config": {
"url": "https://api.yourdomain.com/webhooks/github",
"content_type": "json",
"secret": "your_webhook_secret"
},
"events": ["push", "pull_request"],
"active": true
}
Webhook Handler (Planned)
app.post('/webhooks/github', async (c) => {
const signature = c.req.header('x-hub-signature-256')
const payload = await c.req.json()
// Verify signature
if (!verifySignature(payload, signature)) {
return c.json({ error: 'Invalid signature' }, 401)
}
// Handle event
if (payload.action === 'push') {
// Trigger analysis for new commit
await analyzeRepository({
repoUrl: payload.repository.clone_url,
commitSha: payload.after
})
}
return c.json({ received: true })
})
🔧 Configuration
Environment Variables
# GitHub OAuth App
GITHUB_CLIENT_ID=Iv1.1234567890abcdef
GITHUB_CLIENT_SECRET=1234567890abcdef1234567890abcdef12345678
GITHUB_REDIRECT_URI=https://api.yourdomain.com/auth/callback/github
# Better Auth
BETTER_AUTH_SECRET=your_32_char_secret_here
BETTER_AUTH_URL=https://api.yourdomain.com
GitHub OAuth App Setup
- Go to GitHub Settings → Developer Settings → OAuth Apps
- Click "New OAuth App"
- Fill in details:
- Application name:
Izri - Homepage URL:
https://yourdomain.com - Authorization callback URL:
https://api.yourdomain.com/auth/callback/github
- Application name:
- Copy Client ID and Client Secret
- Add to environment variables
Required Scopes
read:user # Read user profile
user:email # Read user email
repo # Access repositories (for private repos)
🔐 Security
Token Storage
- Access tokens stored encrypted in database
- Never exposed to frontend
- Used only on backend for API calls
Token Refresh
Better Auth handles token refresh automatically when:
- Access token expires
- Refresh token is available
Webhook Security
import crypto from 'node:crypto'
function verifySignature(payload: any, signature: string): boolean {
const secret = process.env.GITHUB_WEBHOOK_SECRET
const hmac = crypto.createHmac('sha256', secret)
const digest = 'sha256=' + hmac.update(JSON.stringify(payload)).digest('hex')
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest))
}
✅ Testing
Mock GitHub OAuth
describe('GitHub OAuth', () => {
it('creates user after GitHub callback', async () => {
const mockGitHubUser = {
id: 123456,
login: 'testuser',
email: 'test@example.com'
}
// Simulate OAuth callback
const user = await handleGitHubCallback(mockGitHubUser)
expect(user.email).toBe('test@example.com')
expect(user.accounts[0].providerId).toBe('github')
})
})
🔗 Related Documentation
- Authentication: Better Auth setup
- Auth Package: @izri/auth
- Projects: Repository selection for projects
Next: API Keys →