178 lines
4.0 KiB
TypeScript
178 lines
4.0 KiB
TypeScript
/**
|
|
* GitHub OAuth integration for VIBN
|
|
* Allows users to connect their GitHub account for repo analysis and tracking
|
|
*/
|
|
|
|
export interface GitHubUser {
|
|
id: number;
|
|
login: string;
|
|
name: string | null;
|
|
email: string | null;
|
|
avatar_url: string;
|
|
}
|
|
|
|
export interface GitHubRepo {
|
|
id: number;
|
|
name: string;
|
|
full_name: string;
|
|
description: string | null;
|
|
html_url: string;
|
|
language: string | null;
|
|
default_branch: string;
|
|
private: boolean;
|
|
topics: string[];
|
|
}
|
|
|
|
export interface GitHubConnection {
|
|
userId: string;
|
|
githubUserId: number;
|
|
githubUsername: string;
|
|
accessToken: string; // Encrypted
|
|
refreshToken?: string; // Encrypted
|
|
tokenExpiresAt?: Date;
|
|
scopes: string[];
|
|
connectedAt: Date;
|
|
lastSyncedAt?: Date;
|
|
}
|
|
|
|
/**
|
|
* Initiates GitHub OAuth flow
|
|
* Redirects user to GitHub authorization page
|
|
*/
|
|
export function initiateGitHubOAuth(redirectUri: string) {
|
|
const clientId = process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID;
|
|
|
|
if (!clientId) {
|
|
throw new Error('GitHub OAuth not configured');
|
|
}
|
|
|
|
// Scopes we need:
|
|
// - repo: Access repositories (read commits, PRs, issues)
|
|
// - read:user: Get user profile
|
|
const scopes = ['repo', 'read:user'];
|
|
|
|
// Generate state for CSRF protection
|
|
const state = generateRandomString(32);
|
|
sessionStorage.setItem('github_oauth_state', state);
|
|
|
|
const params = new URLSearchParams({
|
|
client_id: clientId,
|
|
redirect_uri: redirectUri,
|
|
scope: scopes.join(' '),
|
|
state,
|
|
});
|
|
|
|
window.location.href = `https://github.com/login/oauth/authorize?${params}`;
|
|
}
|
|
|
|
/**
|
|
* Exchanges authorization code for access token
|
|
*/
|
|
export async function exchangeCodeForToken(code: string): Promise<{
|
|
access_token: string;
|
|
token_type: string;
|
|
scope: string;
|
|
}> {
|
|
const response = await fetch('/api/github/oauth/token', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ code }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to exchange code for token');
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Fetches GitHub user profile
|
|
*/
|
|
export async function getGitHubUser(accessToken: string): Promise<GitHubUser> {
|
|
const response = await fetch('https://api.github.com/user', {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
Accept: 'application/vnd.github.v3+json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch GitHub user');
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Fetches user's repositories
|
|
*/
|
|
export async function getGitHubRepos(
|
|
accessToken: string,
|
|
options?: {
|
|
sort?: 'created' | 'updated' | 'pushed' | 'full_name';
|
|
direction?: 'asc' | 'desc';
|
|
per_page?: number;
|
|
}
|
|
): Promise<GitHubRepo[]> {
|
|
const params = new URLSearchParams({
|
|
sort: options?.sort || 'updated',
|
|
direction: options?.direction || 'desc',
|
|
per_page: String(options?.per_page || 100),
|
|
});
|
|
|
|
const response = await fetch(`https://api.github.com/user/repos?${params}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
Accept: 'application/vnd.github.v3+json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch repositories');
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Fetches a specific repository
|
|
*/
|
|
export async function getGitHubRepo(
|
|
accessToken: string,
|
|
owner: string,
|
|
repo: string
|
|
): Promise<GitHubRepo> {
|
|
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
Accept: 'application/vnd.github.v3+json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch repository');
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Utility: Generate random string for state parameter
|
|
*/
|
|
function generateRandomString(length: number): string {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
let result = '';
|
|
const randomValues = new Uint8Array(length);
|
|
crypto.getRandomValues(randomValues);
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
result += chars[randomValues[i] % chars.length];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|