GitHub Integration
GitHub integration allows users to link their GitHub accounts for quest verification and repository access.
OAuth Flow
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Frontend │────▶│ GitHub │────▶│ Frontend │────▶│ /api/ │
│ (Profile) │ │ OAuth │ │ (callback) │ │github/token │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐
│ │ GitHub │
│ │ User API │
│ └─────────────┘
│ │
│◀───────────────────────────────────────────────────────────┘
│ username + token
▼
┌─────────────┐
│ AppContext │
│ (linkGithub)│
└─────────────┘
│
▼
┌─────────────┐
│ /api/profile│
│ (PUT) │
└─────────────┘Step-by-Step Flow
1. User Clicks "Link GitHub"
typescript
// Profile.tsx
const handleLinkGithub = async () => {
// Generate CSRF state token
const state = Math.random().toString(36).slice(2) + Date.now().toString(36);
sessionStorage.setItem('github_oauth_state', state);
// Redirect to GitHub OAuth
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
scope: 'read:user,repo,read:org',
state,
redirect_uri: window.location.origin,
});
window.location.href = `https://github.com/login/oauth/authorize?${params}`;
};2. GitHub Redirects Back
GitHub redirects to {redirect_uri}?code=xxx&state=xxx
3. Frontend Exchanges Code
typescript
// App.tsx
useEffect(() => {
const url = new URL(window.location.href);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
// Verify CSRF state
if (state !== sessionStorage.getItem('github_oauth_state')) return;
// Exchange code via backend proxy
fetch('/api/github/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
})
.then(r => r.json())
.then(data => {
if (data.username) {
linkGithub(data.username); // Persists to profile
sessionStorage.setItem('github_token', data.token);
}
});
}, []);4. Backend Token Exchange
javascript
// api/github/token.js
export default async function handler(req, res) {
const { code } = req.body;
// Exchange code for access token
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
client_id: process.env.GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code,
}),
});
const { access_token } = await tokenRes.json();
// Fetch GitHub username
const userRes = await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${access_token}`,
'User-Agent': 'FetchQuests-OAuth/1.0',
},
});
const user = await userRes.json();
res.json({ username: user.login, token: access_token });
}5. Persist to Profile
typescript
// AppContext.tsx
const linkGithub = useCallback(async (username: string) => {
_setGithubUsername(username);
writeStorage(key, username);
// Ensure authenticated
let token = authToken;
if (!token) token = await signIn();
// Persist to database
if (token) {
await updateProfile(account, { githubUsername: username }, token);
}
}, [account, authToken, signIn]);Environment Variables
| Variable | Description | Required |
|---|---|---|
VITE_GITHUB_CLIENT_ID | GitHub OAuth App Client ID | Yes (frontend) |
GITHUB_CLIENT_ID | Same ID for backend | Yes (backend) |
GITHUB_CLIENT_SECRET | OAuth App Secret | Yes (backend only) |
OAuth Scopes
| Scope | Purpose |
|---|---|
read:user | Get GitHub username |
repo | Access private repos for quest verification |
read:org | Check organization membership |
Quest Verification
When a quest links to a GitHub repo/issue:
- Quest Creation: Giver specifies
githubRepoandgithubIssue - CI Workflow: Repo has
.github/workflows/fetch-quest.yml - PR Completion: Farmer submits PR that closes the issue
- Verification: CI calls webhook to verify completion
- Payout: 48-hour review window, then auto-release funds
Example CI Workflow
yaml
# .github/workflows/fetch-quest.yml
name: Fetch Quest Verification
on:
pull_request:
types: [closed]
jobs:
verify:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Notify Fetch Quests
run: |
curl -X POST https://fetchquests.io/api/verify \
-H "Content-Type: application/json" \
-d '{"repo": "${{ github.repository }}", "pr": ${{ github.event.number }}}'Dev Mode
When VITE_GITHUB_CLIENT_ID is not set, a dev fallback allows manual username entry:
typescript
if (!GITHUB_CLIENT_ID) {
const user = prompt('Dev mode: enter your GitHub username to link:');
if (user?.trim()) {
await linkGithub(user.trim());
}
}Unlinking GitHub
typescript
const unlinkGithub = useCallback(async () => {
_setGithubUsername(null);
removeStorage(key);
let token = authToken;
if (!token) token = await signIn();
if (token) {
await updateProfile(account, { githubUsername: null }, token);
}
}, [account, authToken, signIn]);Security Considerations
- Client Secret — Never exposed to frontend; only used in backend
- CSRF Protection — State token verified on callback
- Token Storage — Access token in sessionStorage (cleared on tab close)
- Minimal Scopes — Only request necessary permissions