ai-toolkit / ui /src /contexts /AuthContext.tsx
multimodalart's picture
Upload 121 files
f555806 verified
raw
history blame
7.21 kB
'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);
}