📦 EqualifyEverything / equalify-hub

📄 auth.ts · 140 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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140import { 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 || '';

import { getGitHubCache, setGitHubCache } from './db';

// Fetch from GitHub with optional auth and 15-min caching
export async function fetchGitHub(url: string, token?: string | null, skipCache = false) {
    // Check cache first (only for non-user-specific requests)
    if (!skipCache) {
        const cached = await getGitHubCache(url);
        if (cached) {
            return cached;
        }
    }
    
    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 });
    const data = await response.json();
    
    // Cache successful responses (only for public API endpoints, not user-specific)
    if (!skipCache && response.ok && !url.includes('/user')) {
        await setGitHubCache(url, data);
    }
    
    return data;
}

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