multimodalart HF Staff commited on
Commit
f48f76d
·
verified ·
1 Parent(s): 896bebc

Upload 102 files

Browse files
ui/src/app/.DS_Store CHANGED
Binary files a/ui/src/app/.DS_Store and b/ui/src/app/.DS_Store differ
 
ui/src/app/api/auth/hf/exchange/route.ts ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ const TOKEN_ENDPOINT = 'https://huggingface.co/oauth/token';
4
+ const USERINFO_ENDPOINT = 'https://huggingface.co/oauth/userinfo';
5
+ const STATE_COOKIE = 'hf_oauth_state';
6
+
7
+ export async function POST(request: NextRequest) {
8
+ const clientId = process.env.HF_OAUTH_CLIENT_ID || process.env.NEXT_PUBLIC_HF_OAUTH_CLIENT_ID;
9
+ const clientSecret = process.env.HF_OAUTH_CLIENT_SECRET;
10
+
11
+ if (!clientId || !clientSecret) {
12
+ return NextResponse.json({ error: 'OAuth application is not configured' }, { status: 500 });
13
+ }
14
+
15
+ const { code, state } = await request.json().catch(() => ({}));
16
+
17
+ if (!code) {
18
+ return NextResponse.json({ error: 'Authorization code is missing' }, { status: 400 });
19
+ }
20
+
21
+ const storedState = request.cookies.get(STATE_COOKIE)?.value;
22
+ if (!storedState || state !== storedState) {
23
+ const response = NextResponse.json({ error: 'Invalid or expired OAuth state' }, { status: 400 });
24
+ response.cookies.delete(STATE_COOKIE);
25
+ return response;
26
+ }
27
+
28
+ const origin = request.nextUrl.origin;
29
+ const redirectUri =
30
+ process.env.HF_OAUTH_REDIRECT_URI ||
31
+ process.env.NEXT_PUBLIC_HF_OAUTH_REDIRECT_URI ||
32
+ `${origin}/auth/hf/callback`;
33
+
34
+ try {
35
+ const tokenResponse = await fetch(TOKEN_ENDPOINT, {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
38
+ body: new URLSearchParams({
39
+ grant_type: 'authorization_code',
40
+ code,
41
+ redirect_uri: redirectUri,
42
+ client_id: clientId,
43
+ client_secret: clientSecret,
44
+ }),
45
+ });
46
+
47
+ if (!tokenResponse.ok) {
48
+ const errorPayload = await tokenResponse.json().catch(() => ({}));
49
+ throw new Error(errorPayload?.error_description || 'Failed to exchange code for token');
50
+ }
51
+
52
+ const tokenData = await tokenResponse.json();
53
+ const accessToken = tokenData?.access_token;
54
+ if (!accessToken) {
55
+ throw new Error('Access token missing in response');
56
+ }
57
+
58
+ const userResponse = await fetch(USERINFO_ENDPOINT, {
59
+ headers: { Authorization: `Bearer ${accessToken}` },
60
+ });
61
+
62
+ if (!userResponse.ok) {
63
+ throw new Error('Failed to fetch user info');
64
+ }
65
+
66
+ const profile = await userResponse.json();
67
+ const namespace = profile?.preferred_username || profile?.name || 'user';
68
+
69
+ const response = NextResponse.json({
70
+ token: accessToken,
71
+ namespace,
72
+ });
73
+ response.cookies.delete(STATE_COOKIE);
74
+ return response;
75
+ } catch (error: any) {
76
+ const response = NextResponse.json({ error: error?.message || 'OAuth flow failed' }, { status: 500 });
77
+ response.cookies.delete(STATE_COOKIE);
78
+ return response;
79
+ }
80
+ }
ui/src/app/api/auth/hf/login/route.ts CHANGED
@@ -12,7 +12,10 @@ export async function GET(request: NextRequest) {
12
 
13
  const state = randomUUID();
14
  const origin = request.nextUrl.origin;
15
- const redirectUri = process.env.HF_OAUTH_REDIRECT_URI || process.env.NEXT_PUBLIC_HF_OAUTH_REDIRECT_URI || `${origin}/api/auth/hf/callback`;
 
 
 
16
 
17
  const authorizeUrl = new URL(HF_AUTHORIZE_URL);
18
  authorizeUrl.searchParams.set('response_type', 'code');
 
12
 
13
  const state = randomUUID();
14
  const origin = request.nextUrl.origin;
15
+ const redirectUri =
16
+ process.env.HF_OAUTH_REDIRECT_URI ||
17
+ process.env.NEXT_PUBLIC_HF_OAUTH_REDIRECT_URI ||
18
+ `${origin}/auth/hf/callback`;
19
 
20
  const authorizeUrl = new URL(HF_AUTHORIZE_URL);
21
  authorizeUrl.searchParams.set('response_type', 'code');
ui/src/app/auth/hf/callback/page.tsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { useRouter, useSearchParams } from 'next/navigation';
5
+ import { useAuth } from '@/contexts/AuthContext';
6
+ import Loading from '@/components/Loading';
7
+
8
+ export default function HFOAuthCallbackPage() {
9
+ const router = useRouter();
10
+ const searchParams = useSearchParams();
11
+ const { exchangeCodeForToken, error } = useAuth();
12
+ const [localError, setLocalError] = useState<string | null>(null);
13
+
14
+ useEffect(() => {
15
+ const code = searchParams.get('code');
16
+ const state = searchParams.get('state');
17
+ const errorParam = searchParams.get('error');
18
+
19
+ const handleAuth = async () => {
20
+ if (errorParam) {
21
+ setLocalError(errorParam);
22
+ router.replace('/settings');
23
+ return;
24
+ }
25
+
26
+ if (code && state) {
27
+ const success = await exchangeCodeForToken(code, state);
28
+ if (success) {
29
+ router.replace('/dashboard');
30
+ } else {
31
+ setLocalError('Authentication failed. Please try again.');
32
+ router.replace('/settings');
33
+ }
34
+ } else {
35
+ setLocalError('Invalid authentication callback.');
36
+ router.replace('/settings');
37
+ }
38
+ };
39
+
40
+ handleAuth();
41
+ }, [exchangeCodeForToken, router, searchParams]);
42
+
43
+ return (
44
+ <div className="min-h-screen flex flex-col items-center justify-center text-gray-200 bg-gray-950 gap-4">
45
+ <Loading />
46
+ <p className="text-lg">Authenticating with Hugging Face…</p>
47
+ {(localError || error) && (
48
+ <p className="text-sm text-red-400 max-w-md text-center">
49
+ {localError || error}
50
+ </p>
51
+ )}
52
+ </div>
53
+ );
54
+ }
ui/src/app/jobs/new/page.tsx CHANGED
@@ -109,7 +109,13 @@ export default function TrainingForm() {
109
  return () => {
110
  isMounted = false;
111
  };
112
- }, [datasets, settings, isSettingsLoaded, datasetFetchStatus]);
 
 
 
 
 
 
113
 
114
  useEffect(() => {
115
  if (runId) {
@@ -299,8 +305,3 @@ export default function TrainingForm() {
299
  </>
300
  );
301
  }
302
- useEffect(() => {
303
- if (!isAuthenticated) {
304
- setDatasetOptions([]);
305
- }
306
- }, [isAuthenticated]);
 
109
  return () => {
110
  isMounted = false;
111
  };
112
+ }, [datasets, settings, isSettingsLoaded, datasetFetchStatus, isAuthenticated]);
113
+
114
+ useEffect(() => {
115
+ if (!isAuthenticated) {
116
+ setDatasetOptions([]);
117
+ }
118
+ }, [isAuthenticated]);
119
 
120
  useEffect(() => {
121
  if (runId) {
 
305
  </>
306
  );
307
  }
 
 
 
 
 
ui/src/contexts/AuthContext.tsx CHANGED
@@ -22,6 +22,7 @@ interface AuthContextValue {
22
  error: string | null;
23
  oauthAvailable: boolean;
24
  loginWithOAuth: () => void;
 
25
  setManualToken: (token: string) => Promise<void>;
26
  logout: () => void;
27
  }
@@ -36,6 +37,7 @@ const defaultValue: AuthContextValue = {
36
  error: null,
37
  oauthAvailable: Boolean(oauthClientId),
38
  loginWithOAuth: () => {},
 
39
  setManualToken: async () => {},
40
  logout: () => {},
41
  };
@@ -191,6 +193,46 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
191
  [applyAuthState],
192
  );
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  const loginWithOAuth = useCallback(() => {
195
  if (typeof window === 'undefined') {
196
  return;
@@ -203,54 +245,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
203
  setStatus('checking');
204
  setError(null);
205
 
206
- const width = 540;
207
- const height = 720;
208
- const left = window.screenX + (window.outerWidth - width) / 2;
209
- const top = window.screenY + (window.outerHeight - height) / 2;
210
-
211
- window.open(
212
- '/api/auth/hf/login',
213
- 'hf-oauth-window',
214
- `width=${width},height=${height},left=${left},top=${top},resizable,scrollbars=yes,status=1`,
215
- );
216
  }, []);
217
 
218
  const logout = useCallback(() => {
219
  clearAuthState();
220
  }, [clearAuthState]);
221
 
222
- // Listen for OAuth completion messages
223
- useEffect(() => {
224
- if (typeof window === 'undefined') {
225
- return;
226
- }
227
-
228
- const handler = async (event: MessageEvent) => {
229
- if (event.origin !== window.location.origin) {
230
- return;
231
- }
232
-
233
- const { type, payload } = event.data || {};
234
-
235
- if (type === 'HF_OAUTH_SUCCESS') {
236
- await applyAuthState({
237
- token: payload?.token,
238
- namespace: payload?.namespace || 'user',
239
- method: 'oauth',
240
- });
241
- return;
242
- }
243
-
244
- if (type === 'HF_OAUTH_ERROR') {
245
- setStatus('error');
246
- setError(payload?.message || 'OAuth flow failed');
247
- }
248
- };
249
-
250
- window.addEventListener('message', handler);
251
- return () => window.removeEventListener('message', handler);
252
- }, [applyAuthState]);
253
-
254
  const value = useMemo<AuthContextValue>(
255
  () => ({
256
  status,
@@ -260,10 +261,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
260
  error,
261
  oauthAvailable,
262
  loginWithOAuth,
 
263
  setManualToken,
264
  logout,
265
  }),
266
- [status, token, namespace, method, error, oauthAvailable, loginWithOAuth, setManualToken, logout],
267
  );
268
 
269
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
 
22
  error: string | null;
23
  oauthAvailable: boolean;
24
  loginWithOAuth: () => void;
25
+ exchangeCodeForToken: (code: string, state: string) => Promise<boolean>;
26
  setManualToken: (token: string) => Promise<void>;
27
  logout: () => void;
28
  }
 
37
  error: null,
38
  oauthAvailable: Boolean(oauthClientId),
39
  loginWithOAuth: () => {},
40
+ exchangeCodeForToken: async () => false,
41
  setManualToken: async () => {},
42
  logout: () => {},
43
  };
 
193
  [applyAuthState],
194
  );
195
 
196
+ const exchangeCodeForToken = useCallback(
197
+ async (code: string, state: string) => {
198
+ if (!code || !state) {
199
+ setError('Invalid authorization response.');
200
+ setStatus('error');
201
+ return false;
202
+ }
203
+ setStatus('checking');
204
+ setError(null);
205
+ try {
206
+ const res = await fetch('/api/auth/hf/exchange', {
207
+ method: 'POST',
208
+ headers: {
209
+ 'Content-Type': 'application/json',
210
+ },
211
+ credentials: 'same-origin',
212
+ body: JSON.stringify({ code, state }),
213
+ });
214
+
215
+ if (!res.ok) {
216
+ const data = await res.json().catch(() => ({}));
217
+ throw new Error(data?.error || 'Failed to exchange authorization code');
218
+ }
219
+
220
+ const data = await res.json();
221
+ await applyAuthState({
222
+ token: data.token,
223
+ namespace: data.namespace || 'user',
224
+ method: 'oauth',
225
+ });
226
+ return true;
227
+ } catch (err: any) {
228
+ setError(err?.message || 'Failed to authenticate with Hugging Face');
229
+ setStatus('error');
230
+ return false;
231
+ }
232
+ },
233
+ [applyAuthState],
234
+ );
235
+
236
  const loginWithOAuth = useCallback(() => {
237
  if (typeof window === 'undefined') {
238
  return;
 
245
  setStatus('checking');
246
  setError(null);
247
 
248
+ window.location.href = '/api/auth/hf/login';
 
 
 
 
 
 
 
 
 
249
  }, []);
250
 
251
  const logout = useCallback(() => {
252
  clearAuthState();
253
  }, [clearAuthState]);
254
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  const value = useMemo<AuthContextValue>(
256
  () => ({
257
  status,
 
261
  error,
262
  oauthAvailable,
263
  loginWithOAuth,
264
+ exchangeCodeForToken,
265
  setManualToken,
266
  logout,
267
  }),
268
+ [status, token, namespace, method, error, oauthAvailable, loginWithOAuth, exchangeCodeForToken, setManualToken, logout],
269
  );
270
 
271
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;