Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
| 'use client'; | |
| import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; | |
| import { loadSettings, persistSettings } from '@/utils/storage/settingsStorage'; | |
| import { oauthClientId } from '@/utils/env'; | |
| type AuthMethod = 'oauth' | 'manual'; | |
| interface StoredAuthState { | |
| token: string; | |
| namespace: string; | |
| method: AuthMethod; | |
| } | |
| export type AuthStatus = 'checking' | 'authenticated' | 'unauthenticated' | 'error'; | |
| interface AuthContextValue { | |
| status: AuthStatus; | |
| token: string | null; | |
| namespace: string | null; | |
| method: AuthMethod | null; | |
| error: string | null; | |
| oauthAvailable: boolean; | |
| loginWithOAuth: () => void; | |
| setManualToken: (token: string) => Promise<void>; | |
| logout: () => void; | |
| } | |
| const STORAGE_KEY = 'HF_AUTH_STATE'; | |
| const defaultValue: AuthContextValue = { | |
| status: 'checking', | |
| token: null, | |
| namespace: null, | |
| method: null, | |
| error: null, | |
| oauthAvailable: Boolean(oauthClientId), | |
| loginWithOAuth: () => {}, | |
| setManualToken: async () => {}, | |
| logout: () => {}, | |
| }; | |
| const AuthContext = createContext<AuthContextValue>(defaultValue); | |
| async function validateToken(token: string) { | |
| const res = await fetch('/api/auth/hf/validate', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ token }), | |
| }); | |
| if (!res.ok) { | |
| const data = await res.json().catch(() => ({})); | |
| throw new Error(data?.error || 'Failed to validate token'); | |
| } | |
| return res.json(); | |
| } | |
| async function syncTokenWithSettings(token: string) { | |
| try { | |
| const current = await loadSettings(); | |
| if (current.HF_TOKEN === token) { | |
| return; | |
| } | |
| current.HF_TOKEN = token; | |
| await persistSettings(current); | |
| } catch (error) { | |
| console.warn('Failed to persist HF token to settings:', error); | |
| } | |
| } | |
| async function clearTokenFromSettings() { | |
| try { | |
| const current = await loadSettings(); | |
| if (current.HF_TOKEN !== '') { | |
| current.HF_TOKEN = ''; | |
| await persistSettings(current); | |
| } | |
| } catch (error) { | |
| console.warn('Failed to clear HF token from settings:', error); | |
| } | |
| } | |
| export function AuthProvider({ children }: { children: React.ReactNode }) { | |
| const [status, setStatus] = useState<AuthStatus>('checking'); | |
| const [token, setToken] = useState<string | null>(null); | |
| const [namespace, setNamespace] = useState<string | null>(null); | |
| const [method, setMethod] = useState<AuthMethod | null>(null); | |
| const [error, setError] = useState<string | null>(null); | |
| const oauthAvailable = Boolean(oauthClientId); | |
| const applyAuthState = useCallback(async ({ token: nextToken, namespace: nextNamespace, method: nextMethod }: StoredAuthState) => { | |
| setToken(nextToken); | |
| setNamespace(nextNamespace); | |
| setMethod(nextMethod); | |
| setStatus('authenticated'); | |
| setError(null); | |
| if (typeof window !== 'undefined') { | |
| window.localStorage.setItem( | |
| STORAGE_KEY, | |
| JSON.stringify({ | |
| token: nextToken, | |
| namespace: nextNamespace, | |
| method: nextMethod, | |
| }), | |
| ); | |
| } | |
| syncTokenWithSettings(nextToken).catch(err => { | |
| console.warn('Failed to sync HF token with settings:', err); | |
| }); | |
| }, []); | |
| const clearAuthState = useCallback(async () => { | |
| setToken(null); | |
| setNamespace(null); | |
| setMethod(null); | |
| setStatus('unauthenticated'); | |
| setError(null); | |
| if (typeof window !== 'undefined') { | |
| window.localStorage.removeItem(STORAGE_KEY); | |
| } | |
| clearTokenFromSettings().catch(err => { | |
| console.warn('Failed to clear HF token from settings:', err); | |
| }); | |
| }, []); | |
| // Restore stored token on mount | |
| useEffect(() => { | |
| if (typeof window === 'undefined') { | |
| return; | |
| } | |
| const restore = async () => { | |
| const raw = window.localStorage.getItem(STORAGE_KEY); | |
| if (!raw) { | |
| setStatus('unauthenticated'); | |
| return; | |
| } | |
| try { | |
| const stored: StoredAuthState = JSON.parse(raw); | |
| if (!stored?.token) { | |
| setStatus('unauthenticated'); | |
| return; | |
| } | |
| setStatus('checking'); | |
| const data = await validateToken(stored.token); | |
| await applyAuthState({ | |
| token: stored.token, | |
| namespace: data?.name || data?.preferred_username || stored.namespace || 'user', | |
| method: stored.method || 'manual', | |
| }); | |
| } catch (err) { | |
| console.warn('Stored HF token invalid:', err); | |
| await clearAuthState(); | |
| } | |
| }; | |
| restore(); | |
| }, [applyAuthState, clearAuthState]); | |
| const setManualToken = useCallback( | |
| async (manualToken: string) => { | |
| if (!manualToken) { | |
| setError('Please provide a token'); | |
| setStatus('error'); | |
| return; | |
| } | |
| setStatus('checking'); | |
| setError(null); | |
| try { | |
| const data = await validateToken(manualToken); | |
| await applyAuthState({ | |
| token: manualToken, | |
| namespace: data?.name || data?.preferred_username || 'user', | |
| method: 'manual', | |
| }); | |
| } catch (err: any) { | |
| setError(err?.message || 'Failed to validate token'); | |
| setStatus('error'); | |
| } | |
| }, | |
| [applyAuthState], | |
| ); | |
| const loginWithOAuth = useCallback(() => { | |
| if (typeof window === 'undefined') { | |
| return; | |
| } | |
| if (!oauthAvailable) { | |
| setError('OAuth is not available on this deployment.'); | |
| setStatus('error'); | |
| return; | |
| } | |
| setStatus('checking'); | |
| setError(null); | |
| const width = 540; | |
| const height = 720; | |
| const left = window.screenX + (window.outerWidth - width) / 2; | |
| const top = window.screenY + (window.outerHeight - height) / 2; | |
| window.open( | |
| '/api/auth/hf/login', | |
| 'hf-oauth-window', | |
| `width=${width},height=${height},left=${left},top=${top},resizable,scrollbars=yes,status=1`, | |
| ); | |
| }, []); | |
| const logout = useCallback(() => { | |
| clearAuthState(); | |
| }, [clearAuthState]); | |
| // Listen for OAuth completion messages | |
| useEffect(() => { | |
| if (typeof window === 'undefined') { | |
| return; | |
| } | |
| const handler = async (event: MessageEvent) => { | |
| if (event.origin !== window.location.origin) { | |
| return; | |
| } | |
| const { type, payload } = event.data || {}; | |
| if (type === 'HF_OAUTH_SUCCESS') { | |
| await applyAuthState({ | |
| token: payload?.token, | |
| namespace: payload?.namespace || 'user', | |
| method: 'oauth', | |
| }); | |
| return; | |
| } | |
| if (type === 'HF_OAUTH_ERROR') { | |
| setStatus('error'); | |
| setError(payload?.message || 'OAuth flow failed'); | |
| } | |
| }; | |
| window.addEventListener('message', handler); | |
| return () => window.removeEventListener('message', handler); | |
| }, [applyAuthState]); | |
| const value = useMemo<AuthContextValue>( | |
| () => ({ | |
| status, | |
| token, | |
| namespace, | |
| method, | |
| error, | |
| oauthAvailable, | |
| loginWithOAuth, | |
| setManualToken, | |
| logout, | |
| }), | |
| [status, token, namespace, method, error, oauthAvailable, loginWithOAuth, setManualToken, logout], | |
| ); | |
| return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; | |
| } | |
| export function useAuth() { | |
| return useContext(AuthContext); | |
| } | |