Reubencf commited on
Commit
b3e7f87
·
1 Parent(s): 0a66940

Fix OAuth cookies, HF image handling, and add debugging

Browse files
app/api/auth/callback/route.ts CHANGED
@@ -1,26 +1,152 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { cookies } from "next/headers";
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  export async function GET(req: NextRequest) {
5
  const url = new URL(req.url);
6
  const code = url.searchParams.get('code');
7
-
 
 
 
 
8
  if (code) {
9
- // This is an OAuth redirect, redirect to main page for client-side handling
10
- return NextResponse.redirect(new URL('/', req.url));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  } else {
12
  // This is a status check request
13
  try {
14
  const cookieStore = await cookies();
15
  const hfToken = cookieStore.get('hf_token');
16
-
17
- return NextResponse.json({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  isLoggedIn: !!hfToken?.value,
19
- hasToken: !!hfToken?.value
 
20
  });
21
  } catch (error) {
22
  console.error('Error checking HF token:', error);
23
- return NextResponse.json({ isLoggedIn: false, hasToken: false });
24
  }
25
  }
26
  }
@@ -28,15 +154,14 @@ export async function GET(req: NextRequest) {
28
  export async function POST(req: NextRequest) {
29
  try {
30
  const { hf_token } = await req.json();
31
-
32
  if (!hf_token || typeof hf_token !== "string") {
33
  return NextResponse.json(
34
  { error: "Invalid or missing HF token" },
35
  { status: 400 }
36
  );
37
  }
38
-
39
- // Store the token in a secure HTTP-only cookie
40
  const cookieStore = await cookies();
41
  cookieStore.set({
42
  name: 'hf_token',
@@ -44,9 +169,9 @@ export async function POST(req: NextRequest) {
44
  httpOnly: true,
45
  secure: process.env.NODE_ENV === 'production',
46
  sameSite: 'lax',
47
- maxAge: 60 * 60 * 24 * 30 // 30 days
48
  });
49
-
50
  return NextResponse.json({ success: true });
51
  } catch (error) {
52
  console.error('Error storing HF token:', error);
@@ -61,7 +186,8 @@ export async function DELETE() {
61
  try {
62
  const cookieStore = await cookies();
63
  cookieStore.delete('hf_token');
64
-
 
65
  return NextResponse.json({ success: true });
66
  } catch (error) {
67
  console.error('Error deleting HF token:', error);
 
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { cookies } from "next/headers";
3
 
4
+ // Determine the correct URL based on environment
5
+ function getSpaceUrl(req: NextRequest): string {
6
+ // Check for HF Space environment
7
+ const spaceHost = process.env.SPACE_HOST;
8
+ if (spaceHost) {
9
+ return `https://${spaceHost}`;
10
+ }
11
+
12
+ // For local development, use the request origin
13
+ const host = req.headers.get('host') || 'localhost:3000';
14
+ const protocol = req.headers.get('x-forwarded-proto') || (host.includes('localhost') ? 'http' : 'https');
15
+ return `${protocol}://${host}`;
16
+ }
17
+
18
  export async function GET(req: NextRequest) {
19
  const url = new URL(req.url);
20
  const code = url.searchParams.get('code');
21
+ const SPACE_URL = getSpaceUrl(req);
22
+ const REDIRECT_URI = `${SPACE_URL}/api/auth/callback`;
23
+
24
+ console.log('Auth callback - SPACE_URL:', SPACE_URL, 'has code:', !!code);
25
+
26
  if (code) {
27
+ // Exchange authorization code for access token
28
+ try {
29
+ const clientId = process.env.OAUTH_CLIENT_ID;
30
+ const clientSecret = process.env.OAUTH_CLIENT_SECRET;
31
+
32
+ console.log('OAuth credentials:', {
33
+ clientId: clientId ? 'present' : 'MISSING',
34
+ clientSecret: clientSecret ? 'present' : 'MISSING'
35
+ });
36
+
37
+ if (!clientId || !clientSecret) {
38
+ console.error('OAuth credentials not configured');
39
+ return NextResponse.redirect(`${SPACE_URL}/?error=oauth_not_configured`);
40
+ }
41
+
42
+ // Exchange code for token
43
+ console.log('Exchanging code for token with redirect_uri:', REDIRECT_URI);
44
+ const tokenResponse = await fetch('https://huggingface.co/oauth/token', {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Content-Type': 'application/x-www-form-urlencoded',
48
+ },
49
+ body: new URLSearchParams({
50
+ grant_type: 'authorization_code',
51
+ client_id: clientId,
52
+ client_secret: clientSecret,
53
+ code: code,
54
+ redirect_uri: REDIRECT_URI,
55
+ }),
56
+ });
57
+
58
+ if (!tokenResponse.ok) {
59
+ const errorText = await tokenResponse.text();
60
+ console.error('Token exchange failed:', errorText);
61
+ return NextResponse.redirect(`${SPACE_URL}/?error=token_exchange_failed`);
62
+ }
63
+
64
+ const tokenData = await tokenResponse.json();
65
+ const accessToken = tokenData.access_token;
66
+ console.log('Token exchange successful, got access token');
67
+
68
+ // Get user info
69
+ const userResponse = await fetch('https://huggingface.co/api/whoami-v2', {
70
+ headers: {
71
+ 'Authorization': `Bearer ${accessToken}`,
72
+ },
73
+ });
74
+
75
+ let userInfo = null;
76
+ if (userResponse.ok) {
77
+ userInfo = await userResponse.json();
78
+ console.log('Got user info:', { name: userInfo?.name, username: userInfo?.name });
79
+ } else {
80
+ console.error('Failed to get user info:', await userResponse.text());
81
+ }
82
+
83
+ // Create redirect response and set cookies ON THE RESPONSE
84
+ // This is critical - cookies().set() doesn't work with redirects!
85
+ const response = NextResponse.redirect(`${SPACE_URL}/`);
86
+
87
+ // Set token cookie (HTTP-only for security)
88
+ response.cookies.set('hf_token', accessToken, {
89
+ httpOnly: true,
90
+ secure: true,
91
+ sameSite: 'none',
92
+ maxAge: 60 * 60 * 24 * 30, // 30 days
93
+ path: '/',
94
+ });
95
+
96
+ // Set user info cookie (readable by client for UI)
97
+ if (userInfo) {
98
+ response.cookies.set('hf_user', JSON.stringify({
99
+ name: userInfo.name || userInfo.fullname,
100
+ username: userInfo.name,
101
+ avatarUrl: userInfo.avatarUrl,
102
+ }), {
103
+ httpOnly: false,
104
+ secure: true,
105
+ sameSite: 'none',
106
+ maxAge: 60 * 60 * 24 * 30,
107
+ path: '/',
108
+ });
109
+ }
110
+
111
+ console.log('OAuth successful, cookies set (SameSite=None), redirecting to:', SPACE_URL);
112
+ return response;
113
+
114
+ } catch (error) {
115
+ console.error('OAuth callback error:', error);
116
+ return NextResponse.redirect(`${SPACE_URL}/?error=oauth_failed`);
117
+ }
118
  } else {
119
  // This is a status check request
120
  try {
121
  const cookieStore = await cookies();
122
  const hfToken = cookieStore.get('hf_token');
123
+ const hfUser = cookieStore.get('hf_user');
124
+
125
+ // Debug cookies availability
126
+ const allCookieNames = cookieStore.getAll().map(c => c.name);
127
+ console.log('Auth check - All cookies:', allCookieNames);
128
+
129
+ let user = null;
130
+ if (hfUser?.value) {
131
+ try {
132
+ user = JSON.parse(hfUser.value);
133
+ } catch { }
134
+ }
135
+
136
+ console.log('Auth status check:', {
137
+ isLoggedIn: !!hfToken?.value,
138
+ hasUser: !!user,
139
+ tokenLength: hfToken?.value?.length
140
+ });
141
+
142
+ return NextResponse.json({
143
  isLoggedIn: !!hfToken?.value,
144
+ hasToken: !!hfToken?.value,
145
+ user,
146
  });
147
  } catch (error) {
148
  console.error('Error checking HF token:', error);
149
+ return NextResponse.json({ isLoggedIn: false, hasToken: false, user: null });
150
  }
151
  }
152
  }
 
154
  export async function POST(req: NextRequest) {
155
  try {
156
  const { hf_token } = await req.json();
157
+
158
  if (!hf_token || typeof hf_token !== "string") {
159
  return NextResponse.json(
160
  { error: "Invalid or missing HF token" },
161
  { status: 400 }
162
  );
163
  }
164
+
 
165
  const cookieStore = await cookies();
166
  cookieStore.set({
167
  name: 'hf_token',
 
169
  httpOnly: true,
170
  secure: process.env.NODE_ENV === 'production',
171
  sameSite: 'lax',
172
+ maxAge: 60 * 60 * 24 * 30
173
  });
174
+
175
  return NextResponse.json({ success: true });
176
  } catch (error) {
177
  console.error('Error storing HF token:', error);
 
186
  try {
187
  const cookieStore = await cookies();
188
  cookieStore.delete('hf_token');
189
+ cookieStore.delete('hf_user');
190
+
191
  return NextResponse.json({ success: true });
192
  } catch (error) {
193
  console.error('Error deleting HF token:', error);
app/api/hf-process/route.ts CHANGED
@@ -183,10 +183,33 @@ export async function POST(req: NextRequest) {
183
  );
184
  }
185
 
186
- const parsed = parseDataUrl(body.image);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  if (!parsed) {
 
188
  return NextResponse.json(
189
- { error: "Invalid image format. Please use a valid image." },
190
  { status: 400 }
191
  );
192
  }
 
183
  );
184
  }
185
 
186
+ // Handle different image formats
187
+ let parsed: { mimeType: string; data: string } | null = null;
188
+
189
+ // Try parsing as Data URL first
190
+ parsed = parseDataUrl(body.image);
191
+
192
+ // If not a data URL, check if it's an HTTP URL and fetch it
193
+ if (!parsed && (body.image.startsWith('http://') || body.image.startsWith('https://'))) {
194
+ try {
195
+ console.log('[HF-API] Fetching image from URL:', body.image.substring(0, 100));
196
+ const imageResponse = await fetch(body.image);
197
+ if (!imageResponse.ok) {
198
+ throw new Error(`Failed to fetch image: ${imageResponse.status}`);
199
+ }
200
+ const imageBuffer = await imageResponse.arrayBuffer();
201
+ const contentType = imageResponse.headers.get('content-type') || 'image/png';
202
+ const base64 = Buffer.from(imageBuffer).toString('base64');
203
+ parsed = { mimeType: contentType, data: base64 };
204
+ } catch (fetchErr) {
205
+ console.error('[HF-API] Failed to fetch image URL:', fetchErr);
206
+ }
207
+ }
208
+
209
  if (!parsed) {
210
+ console.error('[HF-API] Invalid image format. Image starts with:', body.image?.substring(0, 50));
211
  return NextResponse.json(
212
+ { error: "Invalid image format. Expected a data URL (data:image/...) or HTTP URL. Please re-upload or reconnect your image." },
213
  { status: 400 }
214
  );
215
  }
app/api/oauth-config/route.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import crypto from 'crypto';
3
+
4
+ /**
5
+ * API endpoint to get OAuth configuration at runtime
6
+ */
7
+ export async function GET(req: NextRequest) {
8
+ const clientId = process.env.OAUTH_CLIENT_ID;
9
+ const scopes = 'email inference-api';
10
+
11
+ // Determine redirect URL based on environment
12
+ const spaceHost = process.env.SPACE_HOST;
13
+ let redirectUrl: string;
14
+
15
+ if (spaceHost) {
16
+ // Production: use HF Space URL
17
+ redirectUrl = `https://${spaceHost}/api/auth/callback`;
18
+ } else {
19
+ // Local dev: use request host
20
+ const host = req.headers.get('host') || 'localhost:3000';
21
+ const protocol = host.includes('localhost') ? 'http' : 'https';
22
+ redirectUrl = `${protocol}://${host}/api/auth/callback`;
23
+ }
24
+
25
+ // Generate OAuth state for CSRF protection
26
+ const state = crypto.randomBytes(16).toString('hex');
27
+
28
+ // Build the complete OAuth login URL
29
+ let loginUrl: string | null = null;
30
+ if (clientId) {
31
+ const params = new URLSearchParams({
32
+ client_id: clientId,
33
+ redirect_uri: redirectUrl,
34
+ scope: scopes,
35
+ response_type: 'code',
36
+ state: state,
37
+ });
38
+ loginUrl = `https://huggingface.co/oauth/authorize?${params.toString()}`;
39
+ }
40
+
41
+ console.log('OAuth Config:', {
42
+ OAUTH_CLIENT_ID: clientId ? 'present' : 'missing',
43
+ redirectUrl,
44
+ SPACE_HOST: spaceHost || 'not set (local dev)',
45
+ });
46
+
47
+ return NextResponse.json({
48
+ clientId: clientId || null,
49
+ isConfigured: !!clientId,
50
+ redirectUrl,
51
+ loginUrl,
52
+ state,
53
+ });
54
+ }
app/page.tsx CHANGED
@@ -946,8 +946,7 @@ function MergeNodeView({
946
  <p>To fix this:</p>
947
  <ol className="list-decimal list-inside space-y-1">
948
  <li>Get key from: <a href="https://aistudio.google.com/app/apikey" target="_blank" className="text-blue-400 hover:underline">Google AI Studio</a></li>
949
- <li>Edit .env.local file in project root</li>
950
- <li>Replace placeholder with your key</li>
951
  <li>Restart server (Ctrl+C, npm run dev)</li>
952
  </ol>
953
  </div>
@@ -988,26 +987,17 @@ export default function EditorPage() {
988
  (async () => {
989
  setIsCheckingAuth(true);
990
  try {
991
- // Handle OAuth redirect if present
992
- const oauth = await oauthHandleRedirectIfPresent();
993
- if (oauth) {
994
- // Store the token server-side
995
- await fetch('/api/auth/callback', {
996
- method: 'POST',
997
- body: JSON.stringify({ hf_token: oauth.accessToken }),
998
- headers: { 'Content-Type': 'application/json' }
999
- });
1000
- setIsHfProLoggedIn(true);
1001
- } else {
1002
- // Check if already logged in
1003
- const response = await fetch('/api/auth/callback', { method: 'GET' });
1004
- if (response.ok) {
1005
- const data = await response.json();
1006
- setIsHfProLoggedIn(data.isLoggedIn);
1007
  }
1008
  }
1009
  } catch (error) {
1010
- console.error('OAuth error:', error);
1011
  } finally {
1012
  setIsCheckingAuth(false);
1013
  }
@@ -1021,22 +1011,36 @@ export default function EditorPage() {
1021
  try {
1022
  await fetch('/api/auth/callback', { method: 'DELETE' });
1023
  setIsHfProLoggedIn(false);
 
1024
  } catch (error) {
1025
  console.error('Logout error:', error);
1026
  }
1027
  } else {
1028
  // Login with HF OAuth
1029
- const clientId = process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID;
1030
- if (!clientId) {
1031
- console.error('OAuth client ID not configured');
1032
- alert('OAuth client ID not configured. Please check environment variables.');
1033
- return;
1034
- }
1035
 
1036
- window.location.href = await oauthLoginUrl({
1037
- clientId,
1038
- redirectUrl: `${window.location.origin}/api/auth/callback`
1039
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1040
  }
1041
  };
1042
 
@@ -1050,7 +1054,7 @@ export default function EditorPage() {
1050
 
1051
  // Processing Mode: 'nanobananapro' uses Gemini API, 'huggingface' uses HF models
1052
  type ProcessingMode = 'nanobananapro' | 'huggingface';
1053
- const [processingMode, setProcessingMode] = useState<ProcessingMode>('nanobananapro');
1054
 
1055
  // Available HF models
1056
  const HF_MODELS = {
@@ -1080,6 +1084,7 @@ export default function EditorPage() {
1080
  // HF PRO AUTHENTICATION
1081
  const [isHfProLoggedIn, setIsHfProLoggedIn] = useState(false);
1082
  const [isCheckingAuth, setIsCheckingAuth] = useState(true);
 
1083
 
1084
 
1085
  const characters = nodes.filter((n) => n.type === "CHARACTER") as CharacterNode[];
@@ -1536,6 +1541,22 @@ export default function EditorPage() {
1536
 
1537
  // Log request details for debugging
1538
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1539
  // Conditionally use HuggingFace or Gemini API based on processing mode
1540
  let res: Response;
1541
 
@@ -1545,6 +1566,14 @@ export default function EditorPage() {
1545
  throw new Error("Please login with HuggingFace to use HF models. Click 'Login with HuggingFace' in the header.");
1546
  }
1547
 
 
 
 
 
 
 
 
 
1548
  res = await fetch("/api/hf-process", {
1549
  method: "POST",
1550
  headers: { "Content-Type": "application/json" },
@@ -2159,16 +2188,37 @@ export default function EditorPage() {
2159
  ) : (
2160
  <>
2161
  <div className="h-6 w-px bg-border" />
2162
- {/* HF Login Button */}
2163
- <Button
2164
- variant={isHfProLoggedIn ? "outline" : "default"}
2165
- size="sm"
2166
- className="h-8"
2167
- onClick={handleHfProLogin}
2168
- disabled={isCheckingAuth}
2169
- >
2170
- {isCheckingAuth ? "Checking..." : isHfProLoggedIn ? "✓ HF Connected" : "Login with HuggingFace"}
2171
- </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2172
 
2173
  {/* Model Selector - only show when logged in */}
2174
  {isHfProLoggedIn && (
 
946
  <p>To fix this:</p>
947
  <ol className="list-decimal list-inside space-y-1">
948
  <li>Get key from: <a href="https://aistudio.google.com/app/apikey" target="_blank" className="text-blue-400 hover:underline">Google AI Studio</a></li>
949
+ <li>Replace APi key placeholder with your key</li>
 
950
  <li>Restart server (Ctrl+C, npm run dev)</li>
951
  </ol>
952
  </div>
 
987
  (async () => {
988
  setIsCheckingAuth(true);
989
  try {
990
+ // Check if already logged in (callback handles token exchange)
991
+ const response = await fetch('/api/auth/callback', { method: 'GET' });
992
+ if (response.ok) {
993
+ const data = await response.json();
994
+ setIsHfProLoggedIn(data.isLoggedIn);
995
+ if (data.user) {
996
+ setHfUser(data.user);
 
 
 
 
 
 
 
 
 
997
  }
998
  }
999
  } catch (error) {
1000
+ console.error('Auth check error:', error);
1001
  } finally {
1002
  setIsCheckingAuth(false);
1003
  }
 
1011
  try {
1012
  await fetch('/api/auth/callback', { method: 'DELETE' });
1013
  setIsHfProLoggedIn(false);
1014
+ setHfUser(null);
1015
  } catch (error) {
1016
  console.error('Logout error:', error);
1017
  }
1018
  } else {
1019
  // Login with HF OAuth
1020
+ // Fetch OAuth login URL from server-side API (ensures correct redirect URL)
1021
+ try {
1022
+ const response = await fetch('/api/oauth-config');
1023
+ const { isConfigured, loginUrl, redirectUrl } = await response.json();
 
 
1024
 
1025
+ console.log('OAuth Config from API:', {
1026
+ isConfigured,
1027
+ loginUrl: loginUrl ? 'present' : 'missing',
1028
+ redirectUrl
1029
+ });
1030
+
1031
+ if (!isConfigured || !loginUrl) {
1032
+ console.error('OAuth not configured on server. Check Space settings.');
1033
+ alert('OAuth is not configured for this Space. Please ensure:\n1. hf_oauth: true is set in README.md\n2. Space has been rebuilt\n3. Check Space logs for OAuth configuration');
1034
+ return;
1035
+ }
1036
+
1037
+ // Use the server-generated login URL directly
1038
+ // This ensures the redirect_uri uses the correct public Space URL
1039
+ window.location.href = loginUrl;
1040
+ } catch (error) {
1041
+ console.error('Failed to get OAuth config:', error);
1042
+ alert('Failed to initialize OAuth login. Please try again.');
1043
+ }
1044
  }
1045
  };
1046
 
 
1054
 
1055
  // Processing Mode: 'nanobananapro' uses Gemini API, 'huggingface' uses HF models
1056
  type ProcessingMode = 'nanobananapro' | 'huggingface';
1057
+ const [processingMode, setProcessingMode] = useState<ProcessingMode>('huggingface');
1058
 
1059
  // Available HF models
1060
  const HF_MODELS = {
 
1084
  // HF PRO AUTHENTICATION
1085
  const [isHfProLoggedIn, setIsHfProLoggedIn] = useState(false);
1086
  const [isCheckingAuth, setIsCheckingAuth] = useState(true);
1087
+ const [hfUser, setHfUser] = useState<{ name?: string; username?: string; avatarUrl?: string } | null>(null);
1088
 
1089
 
1090
  const characters = nodes.filter((n) => n.type === "CHARACTER") as CharacterNode[];
 
1541
 
1542
  // Log request details for debugging
1543
 
1544
+ // Ensure inputImage is a Data URL (convert Blob URL if needed)
1545
+ // This fixes "invalid image url" errors when passing blob: URLs to server
1546
+ if (inputImage && inputImage.startsWith('blob:')) {
1547
+ try {
1548
+ const blobRes = await fetch(inputImage);
1549
+ const blob = await blobRes.blob();
1550
+ inputImage = await new Promise<string>((resolve) => {
1551
+ const reader = new FileReader();
1552
+ reader.onloadend = () => resolve(reader.result as string);
1553
+ reader.readAsDataURL(blob);
1554
+ });
1555
+ } catch (e) {
1556
+ console.error("Failed to convert blob URL:", e);
1557
+ }
1558
+ }
1559
+
1560
  // Conditionally use HuggingFace or Gemini API based on processing mode
1561
  let res: Response;
1562
 
 
1566
  throw new Error("Please login with HuggingFace to use HF models. Click 'Login with HuggingFace' in the header.");
1567
  }
1568
 
1569
+ // Debug: Log what we're sending
1570
+ console.log('[HF Debug] Sending to /api/hf-process:', {
1571
+ hasImage: !!inputImage,
1572
+ imageType: inputImage ? (inputImage.startsWith('data:') ? 'dataURL' : inputImage.startsWith('blob:') ? 'blobURL' : inputImage.startsWith('http') ? 'httpURL' : 'unknown') : 'null',
1573
+ imagePreview: inputImage?.substring(0, 80),
1574
+ model: selectedHfModel,
1575
+ });
1576
+
1577
  res = await fetch("/api/hf-process", {
1578
  method: "POST",
1579
  headers: { "Content-Type": "application/json" },
 
2188
  ) : (
2189
  <>
2190
  <div className="h-6 w-px bg-border" />
2191
+ {/* HF Login Button / User Info */}
2192
+ {isHfProLoggedIn && hfUser ? (
2193
+ <div className="flex items-center gap-2">
2194
+ {hfUser.avatarUrl && (
2195
+ <img
2196
+ src={hfUser.avatarUrl}
2197
+ alt={hfUser.name || 'User'}
2198
+ className="w-6 h-6 rounded-full"
2199
+ />
2200
+ )}
2201
+ <span className="text-sm font-medium">{hfUser.name || hfUser.username}</span>
2202
+ <Button
2203
+ variant="ghost"
2204
+ size="sm"
2205
+ className="h-6 px-2 text-xs"
2206
+ onClick={handleHfProLogin}
2207
+ >
2208
+ Logout
2209
+ </Button>
2210
+ </div>
2211
+ ) : (
2212
+ <Button
2213
+ variant="default"
2214
+ size="sm"
2215
+ className="h-8"
2216
+ onClick={handleHfProLogin}
2217
+ disabled={isCheckingAuth}
2218
+ >
2219
+ {isCheckingAuth ? "Checking..." : "Login with HuggingFace"}
2220
+ </Button>
2221
+ )}
2222
 
2223
  {/* Model Selector - only show when logged in */}
2224
  {isHfProLoggedIn && (
next.config.ts CHANGED
@@ -8,6 +8,11 @@ const nextConfig: NextConfig = {
8
  serverRuntimeConfig: {
9
  bodySizeLimit: '50mb',
10
  },
 
 
 
 
 
11
  // Redirect /editor to main page
12
  async redirects() {
13
  return [
 
8
  serverRuntimeConfig: {
9
  bodySizeLimit: '50mb',
10
  },
11
+ // Expose HuggingFace OAuth environment variables to the client
12
+ // HF injects OAUTH_CLIENT_ID when hf_oauth: true is set in README
13
+ env: {
14
+ NEXT_PUBLIC_OAUTH_CLIENT_ID: process.env.OAUTH_CLIENT_ID || process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID || '',
15
+ },
16
  // Redirect /editor to main page
17
  async redirects() {
18
  return [