File size: 3,432 Bytes
d140e69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2ae6728
 
 
 
 
 
 
 
 
 
 
 
d140e69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { NextResponse } from 'next/server';

/**
 * GET /api/auth/callback
 * Handles the OAuth callback from HuggingFace.
 * Exchanges code for access token, fetches userinfo, sets session cookie.
 */
export async function GET(request) {
    const { searchParams } = new URL(request.url);
    const code = searchParams.get('code');
    const state = searchParams.get('state');

    if (!code) {
        return NextResponse.json({ error: 'Missing code parameter' }, { status: 400 });
    }

    const clientId = process.env.OAUTH_CLIENT_ID;
    const clientSecret = process.env.OAUTH_CLIENT_SECRET;

    if (!clientId || !clientSecret) {
        return NextResponse.json({ error: 'OAuth not configured' }, { status: 500 });
    }

    const host = process.env.SPACE_HOST
        ? `https://${process.env.SPACE_HOST}`
        : 'http://localhost:3000';
    const redirectUri = `${host}/api/auth/callback`;

    try {
        // Exchange code for access token
        const tokenRes = await fetch('https://huggingface.co/oauth/token', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
            },
            body: new URLSearchParams({
                grant_type: 'authorization_code',
                code,
                redirect_uri: redirectUri,
            }),
        });

        if (!tokenRes.ok) {
            const errText = await tokenRes.text();
            console.error('Token exchange failed:', errText);
            return NextResponse.json({ error: 'Failed to exchange code for token' }, { status: 500 });
        }

        const tokenData = await tokenRes.json();

        // Fetch user info
        const userRes = await fetch('https://huggingface.co/oauth/userinfo', {
            headers: { 'Authorization': `Bearer ${tokenData.access_token}` },
        });

        if (!userRes.ok) {
            return NextResponse.json({ error: 'Failed to fetch user info' }, { status: 500 });
        }

        const userInfo = await userRes.json();
        const username = userInfo.preferred_username || userInfo.name || 'user';

        // Check allowlist (if ALLOWED_USERS is set)
        const allowedUsers = process.env.ALLOWED_USERS;
        if (allowedUsers) {
            const allowlist = allowedUsers.split(',').map(u => u.trim().toLowerCase());
            if (!allowlist.includes(username.toLowerCase())) {
                return NextResponse.json(
                    { error: `Access denied. User "${username}" is not in the allowed list.` },
                    { status: 403 }
                );
            }
        }

        // Set session cookie with username
        const response = NextResponse.redirect(host);
        response.cookies.set('hf_user', JSON.stringify({
            username,
            name: userInfo.name,
            picture: userInfo.picture,
        }), {
            httpOnly: false, // readable by client JS
            secure: process.env.NODE_ENV === 'production',
            sameSite: 'lax',
            maxAge: 60 * 60 * 8, // 8 hours
            path: '/',
        });


        return response;
    } catch (error) {
        console.error('OAuth callback error:', error);
        return NextResponse.json({ error: 'OAuth callback failed: ' + error.message }, { status: 500 });
    }
}