📦 EqualifyEverything / equalify-hub

📄 auth.ts · 123 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123import { event } from './event';

const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || '';
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || '';

// Parse cookies from request headers
export function parseCookies(): Record<string, string> {
    const cookies: Record<string, string> = {};
    
    // Lambda function URLs send cookies as an array
    if (Array.isArray((event as any).cookies)) {
        (event as any).cookies.forEach((cookie: string) => {
            const [name, ...rest] = cookie.split('=');
            if (name) {
                cookies[name] = rest.join('=');
            }
        });
        return cookies;
    }
    
    // Fallback to header parsing for API Gateway
    const cookieHeader = event.headers?.cookie || event.headers?.Cookie || '';
    cookieHeader.split(';').forEach(cookie => {
        const [name, ...rest] = cookie.trim().split('=');
        if (name) {
            cookies[name] = rest.join('=');
        }
    });
    
    return cookies;
}

// Get GitHub token from cookie
export function getGitHubToken(): string | null {
    const cookies = parseCookies();
    return cookies['gh_token'] || null;
}

// Get current user info from cookie
export function getCurrentUser(): { login: string; avatar_url: string; isPro?: boolean } | null {
    const cookies = parseCookies();
    const userCookie = cookies['gh_user'];
    if (!userCookie) return null;
    
    try {
        return JSON.parse(decodeURIComponent(userCookie));
    } catch {
        return null;
    }
}

// Check if user has pro status (survives logout)
export function hasProStatus(): boolean {
    const cookies = parseCookies();
    return cookies['gc_pro'] === '1';
}

// Create secure cookie string
export function createCookie(name: string, value: string, maxAge: number = 86400 * 30): string {
    return `${name}=${value}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=${maxAge}`;
}

// Create cookie deletion string
export function deleteCookie(name: string): string {
    return `${name}=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0`;
}

// GitHub OAuth URLs - pro users get private repo access
export function getGitHubAuthUrl(isPro: boolean = false): string {
    // Pro users get repo + read:org scope for private repo and org access
    // GitHub accepts space-separated scopes (URL encoded as %20)
    const scopes = isPro ? 'read:user repo read:org' : 'read:user';
    return `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&scope=${encodeURIComponent(scopes)}`;
}

// Get pro auth URL (for re-auth after subscribing)
export function getGitHubProAuthUrl(): string {
    return getGitHubAuthUrl(true);
}

// Exchange code for token
export async function exchangeCodeForToken(code: string): Promise<{ access_token: string; error?: string }> {
    const response = await fetch('https://github.com/login/oauth/access_token', {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            client_id: GITHUB_CLIENT_ID,
            client_secret: GITHUB_CLIENT_SECRET,
            code,
        }),
    });
    
    return response.json();
}

// Fallback token for unauthenticated users (5000 req/hour vs 60)
const GITHUB_FALLBACK_TOKEN = process.env.GITHUB_FALLBACK_TOKEN || '';

// Fetch from GitHub with optional auth
export async function fetchGitHub(url: string, token?: string | null) {
    const headers: Record<string, string> = {
        'Accept': 'application/vnd.github.v3+json',
        'User-Agent': 'EqualifyOpenSource'
    };
    
    // Use user's token, or fallback token for unauthenticated users
    const authToken = token || GITHUB_FALLBACK_TOKEN;
    if (authToken) {
        headers['Authorization'] = `Bearer ${authToken}`;
    }
    
    const response = await fetch(url, { headers });
    return response.json();
}

// Fetch current user from GitHub
export async function fetchCurrentUser(token: string) {
    return fetchGitHub('https://api.github.com/user', token);
}