'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; 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(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('checking'); const [token, setToken] = useState(null); const [namespace, setNamespace] = useState(null); const [method, setMethod] = useState(null); const [error, setError] = useState(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( () => ({ status, token, namespace, method, error, oauthAvailable, loginWithOAuth, setManualToken, logout, }), [status, token, namespace, method, error, oauthAvailable, loginWithOAuth, setManualToken, logout], ); return {children}; } export function useAuth() { return useContext(AuthContext); }