Spaces:
Running
Running
Fix OAuth cookies, HF image handling, and add debugging
Browse files- app/api/auth/callback/route.ts +139 -13
- app/api/hf-process/route.ts +25 -2
- app/api/oauth-config/route.ts +54 -0
- app/page.tsx +90 -40
- next.config.ts +5 -0
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 |
-
//
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
if (!parsed) {
|
|
|
|
| 188 |
return NextResponse.json(
|
| 189 |
-
{ error: "Invalid image format. Please
|
| 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>
|
| 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 |
-
//
|
| 992 |
-
const
|
| 993 |
-
if (
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 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('
|
| 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 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
return;
|
| 1034 |
-
}
|
| 1035 |
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 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>('
|
| 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 |
-
|
| 2164 |
-
|
| 2165 |
-
|
| 2166 |
-
|
| 2167 |
-
|
| 2168 |
-
|
| 2169 |
-
|
| 2170 |
-
|
| 2171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 [
|