Skip to content

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

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

VariableDescriptionRequired
VITE_GITHUB_CLIENT_IDGitHub OAuth App Client IDYes (frontend)
GITHUB_CLIENT_IDSame ID for backendYes (backend)
GITHUB_CLIENT_SECRETOAuth App SecretYes (backend only)

OAuth Scopes

ScopePurpose
read:userGet GitHub username
repoAccess private repos for quest verification
read:orgCheck organization membership

Quest Verification

When a quest links to a GitHub repo/issue:

  1. Quest Creation: Giver specifies githubRepo and githubIssue
  2. CI Workflow: Repo has .github/workflows/fetch-quest.yml
  3. PR Completion: Farmer submits PR that closes the issue
  4. Verification: CI calls webhook to verify completion
  5. 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

  1. Client Secret — Never exposed to frontend; only used in backend
  2. CSRF Protection — State token verified on callback
  3. Token Storage — Access token in sessionStorage (cleared on tab close)
  4. Minimal Scopes — Only request necessary permissions

Fetch Quests — Decentralized Gig-Work Platform