Spaces:
Running
Running
feat: use parent postMessage for likes instead of API calls
Browse filesUse the HF-provided postMessage protocol to like spaces from within
the iframe. The parent frame (huggingface.co) handles auth via session
cookies, bypassing CORS and OAuth token issues entirely.
Protocol: parent.postMessage({type: "LIKE_REPO_REQUEST", repo: {type: "space", name: "..."}})
Response: LIKE_REPO_RESPONSE with status (done/already_liked/not_logged_in) or error
Also removes write-discussions OAuth scope (no longer needed).
Co-authored-by: Cursor <cursoragent@cursor.com>
- README.md +0 -2
- src/context/AuthContext.jsx +68 -35
README.md
CHANGED
|
@@ -6,8 +6,6 @@ colorTo: purple
|
|
| 6 |
sdk: docker
|
| 7 |
pinned: true
|
| 8 |
hf_oauth: true
|
| 9 |
-
hf_oauth_scopes:
|
| 10 |
-
- write-discussions
|
| 11 |
thumbnail: >-
|
| 12 |
https://cdn-uploads.huggingface.co/production/uploads/671faa3a541a76b548647676/XWNDlOu0R4fHXR0kCW3Wd.png
|
| 13 |
short_description: All about Reachy Mini, from building to getting started
|
|
|
|
| 6 |
sdk: docker
|
| 7 |
pinned: true
|
| 8 |
hf_oauth: true
|
|
|
|
|
|
|
| 9 |
thumbnail: >-
|
| 10 |
https://cdn-uploads.huggingface.co/production/uploads/671faa3a541a76b548647676/XWNDlOu0R4fHXR0kCW3Wd.png
|
| 11 |
short_description: All about Reachy Mini, from building to getting started
|
src/context/AuthContext.jsx
CHANGED
|
@@ -5,6 +5,9 @@ const AuthContext = createContext(null);
|
|
| 5 |
|
| 6 |
const HF_API = 'https://huggingface.co';
|
| 7 |
|
|
|
|
|
|
|
|
|
|
| 8 |
/**
|
| 9 |
* Fetch OAuth config (clientId, scopes) from our Express server.
|
| 10 |
* Docker Spaces don't auto-inject window.huggingface.variables like static Spaces,
|
|
@@ -41,13 +44,40 @@ async function fetchUserLikedSpaces(username) {
|
|
| 41 |
}
|
| 42 |
}
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
// Provider component
|
| 45 |
export function AuthProvider({ children }) {
|
| 46 |
const [user, setUser] = useState(null); // { name, avatarUrl }
|
| 47 |
const [likedSpaceIds, setLikedSpaceIds] = useState(new Set());
|
| 48 |
const [isLoading, setIsLoading] = useState(true);
|
| 49 |
const [oauthConfig, setOauthConfig] = useState(null); // { clientId, scopes }
|
| 50 |
-
const pendingLikes = useRef(new Set()); // Track in-flight like
|
| 51 |
|
| 52 |
// On mount: fetch OAuth config + check if user just completed OAuth redirect
|
| 53 |
useEffect(() => {
|
|
@@ -65,12 +95,11 @@ export function AuthProvider({ children }) {
|
|
| 65 |
// Check if user just completed OAuth redirect
|
| 66 |
const oauthResult = await oauthHandleRedirectIfPresent();
|
| 67 |
if (oauthResult) {
|
| 68 |
-
const {
|
| 69 |
const userData = {
|
| 70 |
name: userInfo.name,
|
| 71 |
preferredUsername: userInfo.preferred_username || userInfo.name,
|
| 72 |
avatarUrl: userInfo.picture,
|
| 73 |
-
accessToken: accessToken,
|
| 74 |
};
|
| 75 |
setUser(userData);
|
| 76 |
|
|
@@ -125,65 +154,68 @@ export function AuthProvider({ children }) {
|
|
| 125 |
);
|
| 126 |
|
| 127 |
/**
|
| 128 |
-
*
|
| 129 |
-
*
|
|
|
|
| 130 |
*/
|
| 131 |
const toggleLike = useCallback(
|
| 132 |
async (spaceId) => {
|
| 133 |
-
if (!user) {
|
| 134 |
-
login();
|
| 135 |
-
return;
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
const spaceIdLower = spaceId?.toLowerCase();
|
| 139 |
if (!spaceIdLower) return;
|
| 140 |
|
| 141 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
if (pendingLikes.current.has(spaceIdLower)) return;
|
| 143 |
pendingLikes.current.add(spaceIdLower);
|
| 144 |
|
| 145 |
-
const wasLiked = likedSpaceIds.has(spaceIdLower);
|
| 146 |
-
const method = wasLiked ? 'DELETE' : 'POST';
|
| 147 |
-
|
| 148 |
// Optimistic update
|
| 149 |
setLikedSpaceIds((prev) => {
|
| 150 |
const next = new Set(prev);
|
| 151 |
-
|
| 152 |
-
next.delete(spaceIdLower);
|
| 153 |
-
} else {
|
| 154 |
-
next.add(spaceIdLower);
|
| 155 |
-
}
|
| 156 |
return next;
|
| 157 |
});
|
| 158 |
|
| 159 |
try {
|
| 160 |
-
const
|
| 161 |
-
method,
|
| 162 |
-
headers: {
|
| 163 |
-
Authorization: `Bearer ${user.accessToken}`,
|
| 164 |
-
},
|
| 165 |
-
});
|
| 166 |
|
| 167 |
-
if (
|
| 168 |
-
throw new Error(`
|
| 169 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
} catch (err) {
|
| 171 |
-
console.error(`[Auth] Failed to
|
| 172 |
-
|
|
|
|
| 173 |
setLikedSpaceIds((prev) => {
|
| 174 |
const reverted = new Set(prev);
|
| 175 |
-
|
| 176 |
-
reverted.add(spaceIdLower);
|
| 177 |
-
} else {
|
| 178 |
-
reverted.delete(spaceIdLower);
|
| 179 |
-
}
|
| 180 |
return reverted;
|
| 181 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
} finally {
|
| 183 |
pendingLikes.current.delete(spaceIdLower);
|
| 184 |
}
|
| 185 |
},
|
| 186 |
-
[
|
| 187 |
);
|
| 188 |
|
| 189 |
return (
|
|
@@ -193,6 +225,7 @@ export function AuthProvider({ children }) {
|
|
| 193 |
isLoggedIn: !!user,
|
| 194 |
isLoading,
|
| 195 |
isOAuthAvailable: !!oauthConfig?.clientId,
|
|
|
|
| 196 |
likedSpaceIds,
|
| 197 |
login,
|
| 198 |
logout,
|
|
|
|
| 5 |
|
| 6 |
const HF_API = 'https://huggingface.co';
|
| 7 |
|
| 8 |
+
// Whether we're running inside an iframe (i.e. embedded on huggingface.co)
|
| 9 |
+
const isInIframe = typeof window !== 'undefined' && window.parent !== window;
|
| 10 |
+
|
| 11 |
/**
|
| 12 |
* Fetch OAuth config (clientId, scopes) from our Express server.
|
| 13 |
* Docker Spaces don't auto-inject window.huggingface.variables like static Spaces,
|
|
|
|
| 44 |
}
|
| 45 |
}
|
| 46 |
|
| 47 |
+
/**
|
| 48 |
+
* Send a like request to the HF parent frame via postMessage.
|
| 49 |
+
* Returns a Promise that resolves with the response data.
|
| 50 |
+
* The parent frame (huggingface.co) handles auth via session cookies.
|
| 51 |
+
*/
|
| 52 |
+
function likeViaPostMessage(spaceId) {
|
| 53 |
+
return new Promise((resolve, reject) => {
|
| 54 |
+
const timeout = setTimeout(() => {
|
| 55 |
+
window.removeEventListener('message', handler);
|
| 56 |
+
reject(new Error('Like request timed out'));
|
| 57 |
+
}, 5000);
|
| 58 |
+
|
| 59 |
+
function handler(event) {
|
| 60 |
+
if (event.data?.type !== 'LIKE_REPO_RESPONSE') return;
|
| 61 |
+
clearTimeout(timeout);
|
| 62 |
+
window.removeEventListener('message', handler);
|
| 63 |
+
resolve(event.data);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
window.addEventListener('message', handler);
|
| 67 |
+
window.parent.postMessage(
|
| 68 |
+
{ type: 'LIKE_REPO_REQUEST', repo: { type: 'space', name: spaceId } },
|
| 69 |
+
'*'
|
| 70 |
+
);
|
| 71 |
+
});
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
// Provider component
|
| 75 |
export function AuthProvider({ children }) {
|
| 76 |
const [user, setUser] = useState(null); // { name, avatarUrl }
|
| 77 |
const [likedSpaceIds, setLikedSpaceIds] = useState(new Set());
|
| 78 |
const [isLoading, setIsLoading] = useState(true);
|
| 79 |
const [oauthConfig, setOauthConfig] = useState(null); // { clientId, scopes }
|
| 80 |
+
const pendingLikes = useRef(new Set()); // Track in-flight like requests
|
| 81 |
|
| 82 |
// On mount: fetch OAuth config + check if user just completed OAuth redirect
|
| 83 |
useEffect(() => {
|
|
|
|
| 95 |
// Check if user just completed OAuth redirect
|
| 96 |
const oauthResult = await oauthHandleRedirectIfPresent();
|
| 97 |
if (oauthResult) {
|
| 98 |
+
const { userInfo } = oauthResult;
|
| 99 |
const userData = {
|
| 100 |
name: userInfo.name,
|
| 101 |
preferredUsername: userInfo.preferred_username || userInfo.name,
|
| 102 |
avatarUrl: userInfo.picture,
|
|
|
|
| 103 |
};
|
| 104 |
setUser(userData);
|
| 105 |
|
|
|
|
| 154 |
);
|
| 155 |
|
| 156 |
/**
|
| 157 |
+
* Like a space via the HF parent frame postMessage protocol.
|
| 158 |
+
* The parent (huggingface.co) handles auth via session cookies.
|
| 159 |
+
* Like-only (no unlike) — if already liked, it's a no-op.
|
| 160 |
*/
|
| 161 |
const toggleLike = useCallback(
|
| 162 |
async (spaceId) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
const spaceIdLower = spaceId?.toLowerCase();
|
| 164 |
if (!spaceIdLower) return;
|
| 165 |
|
| 166 |
+
// Already liked → no-op (postMessage only supports like, not unlike)
|
| 167 |
+
if (likedSpaceIds.has(spaceIdLower)) return;
|
| 168 |
+
|
| 169 |
+
// Not in iframe → can't use postMessage, prompt OAuth login as fallback
|
| 170 |
+
if (!isInIframe) {
|
| 171 |
+
console.warn('[Auth] Not in iframe, postMessage unavailable');
|
| 172 |
+
return;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// Prevent duplicate requests
|
| 176 |
if (pendingLikes.current.has(spaceIdLower)) return;
|
| 177 |
pendingLikes.current.add(spaceIdLower);
|
| 178 |
|
|
|
|
|
|
|
|
|
|
| 179 |
// Optimistic update
|
| 180 |
setLikedSpaceIds((prev) => {
|
| 181 |
const next = new Set(prev);
|
| 182 |
+
next.add(spaceIdLower);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
return next;
|
| 184 |
});
|
| 185 |
|
| 186 |
try {
|
| 187 |
+
const result = await likeViaPostMessage(spaceId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
+
if (result.error) {
|
| 190 |
+
throw new Error(`${result.error.code}: ${result.error.message}`);
|
| 191 |
}
|
| 192 |
+
|
| 193 |
+
if (result.status === 'not_logged_in') {
|
| 194 |
+
// User not logged in to HF → revert and prompt login
|
| 195 |
+
throw new Error('not_logged_in');
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// "done" or "already_liked" → success
|
| 199 |
+
console.log(`[Auth] Liked ${spaceId}: ${result.status}`, result.likes != null ? `(${result.likes} likes)` : '');
|
| 200 |
} catch (err) {
|
| 201 |
+
console.error(`[Auth] Failed to like ${spaceId}:`, err.message);
|
| 202 |
+
|
| 203 |
+
// Revert optimistic update
|
| 204 |
setLikedSpaceIds((prev) => {
|
| 205 |
const reverted = new Set(prev);
|
| 206 |
+
reverted.delete(spaceIdLower);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
return reverted;
|
| 208 |
});
|
| 209 |
+
|
| 210 |
+
// If not logged in, prompt OAuth login
|
| 211 |
+
if (err.message === 'not_logged_in') {
|
| 212 |
+
login();
|
| 213 |
+
}
|
| 214 |
} finally {
|
| 215 |
pendingLikes.current.delete(spaceIdLower);
|
| 216 |
}
|
| 217 |
},
|
| 218 |
+
[likedSpaceIds, login]
|
| 219 |
);
|
| 220 |
|
| 221 |
return (
|
|
|
|
| 225 |
isLoggedIn: !!user,
|
| 226 |
isLoading,
|
| 227 |
isOAuthAvailable: !!oauthConfig?.clientId,
|
| 228 |
+
isInIframe,
|
| 229 |
likedSpaceIds,
|
| 230 |
login,
|
| 231 |
logout,
|