Spaces:
Running
Running
File size: 7,546 Bytes
0fe543d 279ae3b 638248e 45671b3 279ae3b 638248e 279ae3b 9ce28e1 279ae3b 45671b3 638248e 279ae3b 45671b3 279ae3b 45671b3 279ae3b 45671b3 279ae3b 638248e 279ae3b 45671b3 279ae3b 45671b3 279ae3b 45671b3 279ae3b 45671b3 279ae3b 45671b3 279ae3b 45671b3 279ae3b 9ce28e1 638248e 9ce28e1 0fe543d 638248e 0fe543d 638248e 0fe543d 638248e 0fe543d 638248e 0fe543d 638248e 0fe543d 638248e 0fe543d 638248e 0fe543d 638248e 0fe543d 279ae3b 638248e 279ae3b 45671b3 638248e 279ae3b 0fe543d 279ae3b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | import { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
const AuthContext = createContext(null);
const HF_API = 'https://huggingface.co';
// Whether we're running inside an iframe (i.e. embedded on huggingface.co)
const isInIframe = typeof window !== 'undefined' && window.parent !== window;
/**
* Fetch OAuth config (clientId, scopes) from our Express server.
* Docker Spaces don't auto-inject window.huggingface.variables like static Spaces,
* so we expose OAUTH_CLIENT_ID via a server endpoint.
*/
async function fetchOAuthConfig() {
try {
const response = await fetch('/api/oauth-config');
if (!response.ok) return null;
return await response.json();
} catch {
return null;
}
}
/**
* Fetch all space IDs liked by a user.
* Returns a Set of lowercase space IDs (e.g. "pollen-robotics/reachy-mini-teleop").
*/
async function fetchUserLikedSpaces(username) {
try {
const response = await fetch(`${HF_API}/api/users/${username}/likes`);
if (!response.ok) return new Set();
const likes = await response.json();
// Filter only spaces and return their repo names as a Set
return new Set(
likes
.filter((item) => item.repo?.type === 'space')
.map((item) => item.repo.name.toLowerCase())
);
} catch (err) {
console.error('[Auth] Failed to fetch user likes:', err);
return new Set();
}
}
/**
* Send a like request to the HF parent frame via postMessage.
* Returns a Promise that resolves with the response data.
* The parent frame (huggingface.co) handles auth via session cookies.
*/
function likeViaPostMessage(spaceId) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
window.removeEventListener('message', handler);
reject(new Error('Like request timed out'));
}, 5000);
function handler(event) {
if (event.data?.type !== 'LIKE_REPO_RESPONSE') return;
clearTimeout(timeout);
window.removeEventListener('message', handler);
resolve(event.data);
}
window.addEventListener('message', handler);
window.parent.postMessage(
{ type: 'LIKE_REPO_REQUEST', repo: { type: 'space', name: spaceId } },
'*'
);
});
}
// Provider component
export function AuthProvider({ children }) {
const [user, setUser] = useState(null); // { name, avatarUrl }
const [likedSpaceIds, setLikedSpaceIds] = useState(new Set());
const [isLoading, setIsLoading] = useState(true);
const [oauthConfig, setOauthConfig] = useState(null); // { clientId, scopes }
const pendingLikes = useRef(new Set()); // Track in-flight like requests
// On mount: fetch OAuth config + check if user just completed OAuth redirect
useEffect(() => {
async function init() {
try {
// Fetch OAuth config from server (needed for Docker Spaces)
const config = await fetchOAuthConfig();
if (config?.clientId) {
setOauthConfig(config);
console.log('[Auth] OAuth config loaded (clientId available)');
} else {
console.log('[Auth] OAuth not available (no clientId from server)');
}
// Check if user just completed OAuth redirect
const oauthResult = await oauthHandleRedirectIfPresent();
if (oauthResult) {
const { userInfo } = oauthResult;
const userData = {
name: userInfo.name,
preferredUsername: userInfo.preferred_username || userInfo.name,
avatarUrl: userInfo.picture,
};
setUser(userData);
// Fetch user's liked spaces
const likes = await fetchUserLikedSpaces(userData.preferredUsername);
setLikedSpaceIds(likes);
console.log(
`[Auth] Logged in as ${userData.preferredUsername}, ${likes.size} liked spaces`
);
}
} catch (err) {
console.error('[Auth] Init error:', err);
} finally {
setIsLoading(false);
}
}
init();
}, []);
// Login: redirect to HF OAuth (passing clientId from server config)
const login = useCallback(async () => {
if (!oauthConfig?.clientId) {
console.warn('[Auth] Cannot login: OAuth not configured');
return;
}
try {
const loginUrl = await oauthLoginUrl({
clientId: oauthConfig.clientId,
scopes: oauthConfig.scopes || 'openid profile',
});
window.location.href = loginUrl;
} catch (err) {
console.error('[Auth] Failed to get OAuth URL:', err);
}
}, [oauthConfig]);
// Logout: clear state
const logout = useCallback(() => {
setUser(null);
setLikedSpaceIds(new Set());
}, []);
// Check if a space is liked
const isSpaceLiked = useCallback(
(spaceId) => {
return likedSpaceIds.has(spaceId?.toLowerCase());
},
[likedSpaceIds]
);
/**
* Like a space via the HF parent frame postMessage protocol.
* The parent (huggingface.co) handles auth via session cookies.
* Like-only (no unlike) — if already liked, it's a no-op.
*/
const toggleLike = useCallback(
async (spaceId) => {
const spaceIdLower = spaceId?.toLowerCase();
if (!spaceIdLower) return;
// Already liked → no-op (postMessage only supports like, not unlike)
if (likedSpaceIds.has(spaceIdLower)) return;
// Not in iframe → can't use postMessage, prompt OAuth login as fallback
if (!isInIframe) {
console.warn('[Auth] Not in iframe, postMessage unavailable');
return;
}
// Prevent duplicate requests
if (pendingLikes.current.has(spaceIdLower)) return;
pendingLikes.current.add(spaceIdLower);
// Optimistic update
setLikedSpaceIds((prev) => {
const next = new Set(prev);
next.add(spaceIdLower);
return next;
});
try {
const result = await likeViaPostMessage(spaceId);
if (result.error) {
throw new Error(`${result.error.code}: ${result.error.message}`);
}
if (result.status === 'not_logged_in') {
// User not logged in to HF → revert and prompt login
throw new Error('not_logged_in');
}
// "done" or "already_liked" → success
console.log(`[Auth] Liked ${spaceId}: ${result.status}`, result.likes != null ? `(${result.likes} likes)` : '');
} catch (err) {
console.error(`[Auth] Failed to like ${spaceId}:`, err.message);
// Revert optimistic update
setLikedSpaceIds((prev) => {
const reverted = new Set(prev);
reverted.delete(spaceIdLower);
return reverted;
});
// If not logged in, prompt OAuth login
if (err.message === 'not_logged_in') {
login();
}
} finally {
pendingLikes.current.delete(spaceIdLower);
}
},
[likedSpaceIds, login]
);
return (
<AuthContext.Provider
value={{
user,
isLoggedIn: !!user,
isLoading,
isOAuthAvailable: !!oauthConfig?.clientId,
isInIframe,
likedSpaceIds,
login,
logout,
isSpaceLiked,
toggleLike,
}}
>
{children}
</AuthContext.Provider>
);
}
// Hook to use auth context
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
|