tfrere HF Staff Cursor commited on
Commit
b3660cd
Β·
1 Parent(s): 6c87e73

Simplify OAuth: server-side cookies + iframe detection

Browse files

- Removed @huggingface/hub (unnecessary complexity)
- Server-side OAuth via /auth/login (cookies work on direct access)
- In iframe: show "Open ML Agent" button linking to direct Space URL
- In dev mode: bypass auth entirely (no OAUTH_CLIENT_ID)
- 3 states on welcome screen: iframe→link, direct→sign in, authed→start

Co-authored-by: Cursor <cursoragent@cursor.com>

frontend/package-lock.json CHANGED
@@ -10,7 +10,6 @@
10
  "dependencies": {
11
  "@emotion/react": "^11.13.0",
12
  "@emotion/styled": "^11.13.0",
13
- "@huggingface/hub": "^2.8.1",
14
  "@mui/icons-material": "^6.1.0",
15
  "@mui/material": "^6.1.0",
16
  "react": "^18.3.1",
@@ -1020,30 +1019,6 @@
1020
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1021
  }
1022
  },
1023
- "node_modules/@huggingface/hub": {
1024
- "version": "2.8.1",
1025
- "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-2.8.1.tgz",
1026
- "integrity": "sha512-VAsXdMiIHPteXQJhrwaBEiePTWiJ0zBSymHdnX4J+AijjNN0h3RzGfkKemXcu75gu/TmRLFY9l8+2Tkdmpis0w==",
1027
- "license": "MIT",
1028
- "dependencies": {
1029
- "@huggingface/tasks": "^0.19.82"
1030
- },
1031
- "bin": {
1032
- "hfjs": "dist/cli.js"
1033
- },
1034
- "engines": {
1035
- "node": ">=18"
1036
- },
1037
- "optionalDependencies": {
1038
- "cli-progress": "^3.12.0"
1039
- }
1040
- },
1041
- "node_modules/@huggingface/tasks": {
1042
- "version": "0.19.84",
1043
- "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.84.tgz",
1044
- "integrity": "sha512-nn8DtUW7EZb4QcV+AdNbggPDbvaN32+FldkJakDVml0Aa18fSWjxePdxaRejfTlJYX9oGanOjQKj++NDVRYFXw==",
1045
- "license": "MIT"
1046
- },
1047
  "node_modules/@humanfs/core": {
1048
  "version": "0.19.1",
1049
  "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -2242,16 +2217,6 @@
2242
  "url": "https://github.com/sponsors/epoberezkin"
2243
  }
2244
  },
2245
- "node_modules/ansi-regex": {
2246
- "version": "5.0.1",
2247
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
2248
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
2249
- "license": "MIT",
2250
- "optional": true,
2251
- "engines": {
2252
- "node": ">=8"
2253
- }
2254
- },
2255
  "node_modules/ansi-styles": {
2256
  "version": "4.3.0",
2257
  "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -2460,19 +2425,6 @@
2460
  "url": "https://github.com/sponsors/wooorm"
2461
  }
2462
  },
2463
- "node_modules/cli-progress": {
2464
- "version": "3.12.0",
2465
- "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
2466
- "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
2467
- "license": "MIT",
2468
- "optional": true,
2469
- "dependencies": {
2470
- "string-width": "^4.2.3"
2471
- },
2472
- "engines": {
2473
- "node": ">=4"
2474
- }
2475
- },
2476
  "node_modules/clsx": {
2477
  "version": "2.1.1",
2478
  "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -2638,13 +2590,6 @@
2638
  "dev": true,
2639
  "license": "ISC"
2640
  },
2641
- "node_modules/emoji-regex": {
2642
- "version": "8.0.0",
2643
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
2644
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
2645
- "license": "MIT",
2646
- "optional": true
2647
- },
2648
  "node_modules/error-ex": {
2649
  "version": "1.3.4",
2650
  "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
@@ -3325,16 +3270,6 @@
3325
  "node": ">=0.10.0"
3326
  }
3327
  },
3328
- "node_modules/is-fullwidth-code-point": {
3329
- "version": "3.0.0",
3330
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
3331
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
3332
- "license": "MIT",
3333
- "optional": true,
3334
- "engines": {
3335
- "node": ">=8"
3336
- }
3337
- },
3338
  "node_modules/is-glob": {
3339
  "version": "4.0.3",
3340
  "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -5041,21 +4976,6 @@
5041
  "url": "https://github.com/sponsors/wooorm"
5042
  }
5043
  },
5044
- "node_modules/string-width": {
5045
- "version": "4.2.3",
5046
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
5047
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
5048
- "license": "MIT",
5049
- "optional": true,
5050
- "dependencies": {
5051
- "emoji-regex": "^8.0.0",
5052
- "is-fullwidth-code-point": "^3.0.0",
5053
- "strip-ansi": "^6.0.1"
5054
- },
5055
- "engines": {
5056
- "node": ">=8"
5057
- }
5058
- },
5059
  "node_modules/stringify-entities": {
5060
  "version": "4.0.4",
5061
  "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
@@ -5070,19 +4990,6 @@
5070
  "url": "https://github.com/sponsors/wooorm"
5071
  }
5072
  },
5073
- "node_modules/strip-ansi": {
5074
- "version": "6.0.1",
5075
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
5076
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
5077
- "license": "MIT",
5078
- "optional": true,
5079
- "dependencies": {
5080
- "ansi-regex": "^5.0.1"
5081
- },
5082
- "engines": {
5083
- "node": ">=8"
5084
- }
5085
- },
5086
  "node_modules/strip-json-comments": {
5087
  "version": "3.1.1",
5088
  "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
 
10
  "dependencies": {
11
  "@emotion/react": "^11.13.0",
12
  "@emotion/styled": "^11.13.0",
 
13
  "@mui/icons-material": "^6.1.0",
14
  "@mui/material": "^6.1.0",
15
  "react": "^18.3.1",
 
1019
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1020
  }
1021
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1022
  "node_modules/@humanfs/core": {
1023
  "version": "0.19.1",
1024
  "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
 
2217
  "url": "https://github.com/sponsors/epoberezkin"
2218
  }
2219
  },
 
 
 
 
 
 
 
 
 
 
2220
  "node_modules/ansi-styles": {
2221
  "version": "4.3.0",
2222
  "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
 
2425
  "url": "https://github.com/sponsors/wooorm"
2426
  }
2427
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
2428
  "node_modules/clsx": {
2429
  "version": "2.1.1",
2430
  "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
 
2590
  "dev": true,
2591
  "license": "ISC"
2592
  },
 
 
 
 
 
 
 
2593
  "node_modules/error-ex": {
2594
  "version": "1.3.4",
2595
  "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
 
3270
  "node": ">=0.10.0"
3271
  }
3272
  },
 
 
 
 
 
 
 
 
 
 
3273
  "node_modules/is-glob": {
3274
  "version": "4.0.3",
3275
  "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
 
4976
  "url": "https://github.com/sponsors/wooorm"
4977
  }
4978
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4979
  "node_modules/stringify-entities": {
4980
  "version": "4.0.4",
4981
  "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
 
4990
  "url": "https://github.com/sponsors/wooorm"
4991
  }
4992
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
4993
  "node_modules/strip-json-comments": {
4994
  "version": "3.1.1",
4995
  "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
frontend/package.json CHANGED
@@ -12,7 +12,6 @@
12
  "dependencies": {
13
  "@emotion/react": "^11.13.0",
14
  "@emotion/styled": "^11.13.0",
15
- "@huggingface/hub": "^2.8.1",
16
  "@mui/icons-material": "^6.1.0",
17
  "@mui/material": "^6.1.0",
18
  "react": "^18.3.1",
 
12
  "dependencies": {
13
  "@emotion/react": "^11.13.0",
14
  "@emotion/styled": "^11.13.0",
 
15
  "@mui/icons-material": "^6.1.0",
16
  "@mui/material": "^6.1.0",
17
  "react": "^18.3.1",
frontend/src/components/WelcomeScreen/WelcomeScreen.tsx CHANGED
@@ -5,12 +5,13 @@ import {
5
  Button,
6
  CircularProgress,
7
  Alert,
 
8
  } from '@mui/material';
 
9
  import { useSessionStore } from '@/store/sessionStore';
10
  import { useAgentStore } from '@/store/agentStore';
11
  import { apiFetch } from '@/utils/api';
12
- import { getStoredToken, triggerLogin } from '@/hooks/useAuth';
13
- import { logger } from '@/utils/logger';
14
 
15
  /** HF brand orange */
16
  const HF_ORANGE = '#FF9D00';
@@ -21,15 +22,20 @@ export default function WelcomeScreen() {
21
  const [isCreating, setIsCreating] = useState(false);
22
  const [error, setError] = useState<string | null>(null);
23
 
 
 
 
 
24
  const handleStart = useCallback(async () => {
25
  if (isCreating) return;
26
 
27
- // In production (OAuth enabled): check for stored token, trigger login if missing
28
- // In dev mode (user already set by useAuth): skip login, go straight to session
29
- const isDevUser = user?.username === 'dev';
30
- if (!isDevUser && !getStoredToken()) {
31
- logger.log('No token β€” triggering OAuth login');
32
- await triggerLogin();
 
33
  return;
34
  }
35
 
@@ -44,8 +50,7 @@ export default function WelcomeScreen() {
44
  return;
45
  }
46
  if (response.status === 401) {
47
- // Token expired β€” trigger re-login
48
- await triggerLogin();
49
  return;
50
  }
51
  if (!response.ok) {
@@ -57,11 +62,18 @@ export default function WelcomeScreen() {
57
  setPlan([]);
58
  setPanelContent(null);
59
  } catch {
60
- // triggerLogin may redirect β€” don't show error
61
  } finally {
62
  setIsCreating(false);
63
  }
64
- }, [isCreating, createSession, setPlan, setPanelContent, user]);
 
 
 
 
 
 
 
65
 
66
  return (
67
  <Box
@@ -76,17 +88,12 @@ export default function WelcomeScreen() {
76
  py: 8,
77
  }}
78
  >
79
- {/* HF Logo β€” large, centered */}
80
  <Box
81
  component="img"
82
  src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
83
  alt="Hugging Face"
84
- sx={{
85
- width: 96,
86
- height: 96,
87
- mb: 3,
88
- display: 'block',
89
- }}
90
  />
91
 
92
  {/* Title */}
@@ -114,10 +121,7 @@ export default function WelcomeScreen() {
114
  fontSize: '0.95rem',
115
  textAlign: 'center',
116
  px: 2,
117
- '& strong': {
118
- color: 'var(--text)',
119
- fontWeight: 600,
120
- },
121
  }}
122
  >
123
  A general-purpose AI agent for <strong>machine learning engineering</strong>.
@@ -126,38 +130,93 @@ export default function WelcomeScreen() {
126
  and explores <strong>datasets</strong> β€” all through natural conversation.
127
  </Typography>
128
 
129
- {/* Start Button */}
130
- <Button
131
- variant="contained"
132
- size="large"
133
- onClick={handleStart}
134
- disabled={isCreating}
135
- startIcon={
136
- isCreating ? <CircularProgress size={20} color="inherit" /> : null
137
- }
138
- sx={{
139
- px: 5,
140
- py: 1.5,
141
- fontSize: '1rem',
142
- fontWeight: 700,
143
- textTransform: 'none',
144
- borderRadius: '12px',
145
- bgcolor: HF_ORANGE,
146
- color: '#000',
147
- boxShadow: '0 4px 24px rgba(255, 157, 0, 0.3)',
148
- transition: 'all 0.2s ease',
149
- '&:hover': {
150
- bgcolor: '#FFB340',
151
- boxShadow: '0 6px 32px rgba(255, 157, 0, 0.45)',
152
- },
153
- '&.Mui-disabled': {
154
- bgcolor: 'rgba(255, 157, 0, 0.35)',
155
- color: 'rgba(0,0,0,0.45)',
156
- },
157
- }}
158
- >
159
- {isCreating ? 'Initializing...' : 'Start Session'}
160
- </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
  {/* Error */}
163
  {error && (
@@ -180,12 +239,7 @@ export default function WelcomeScreen() {
180
  {/* Footnote */}
181
  <Typography
182
  variant="caption"
183
- sx={{
184
- mt: 5,
185
- color: 'var(--muted-text)',
186
- opacity: 0.5,
187
- fontSize: '0.7rem',
188
- }}
189
  >
190
  Conversations are stored locally in your browser.
191
  </Typography>
 
5
  Button,
6
  CircularProgress,
7
  Alert,
8
+ Link,
9
  } from '@mui/material';
10
+ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
11
  import { useSessionStore } from '@/store/sessionStore';
12
  import { useAgentStore } from '@/store/agentStore';
13
  import { apiFetch } from '@/utils/api';
14
+ import { isInIframe, triggerLogin } from '@/hooks/useAuth';
 
15
 
16
  /** HF brand orange */
17
  const HF_ORANGE = '#FF9D00';
 
22
  const [isCreating, setIsCreating] = useState(false);
23
  const [error, setError] = useState<string | null>(null);
24
 
25
+ const inIframe = isInIframe();
26
+ const isAuthenticated = user?.authenticated;
27
+ const isDevUser = user?.username === 'dev';
28
+
29
  const handleStart = useCallback(async () => {
30
  if (isCreating) return;
31
 
32
+ // Not authenticated and not dev β†’ need to login
33
+ if (!isAuthenticated && !isDevUser) {
34
+ // In iframe: can't redirect (cookies blocked) β€” user needs to open in new tab
35
+ // This shouldn't happen because we show a different button in iframe
36
+ // But just in case:
37
+ if (inIframe) return;
38
+ triggerLogin();
39
  return;
40
  }
41
 
 
50
  return;
51
  }
52
  if (response.status === 401) {
53
+ triggerLogin();
 
54
  return;
55
  }
56
  if (!response.ok) {
 
62
  setPlan([]);
63
  setPanelContent(null);
64
  } catch {
65
+ // Redirect may throw β€” ignore
66
  } finally {
67
  setIsCreating(false);
68
  }
69
+ }, [isCreating, createSession, setPlan, setPanelContent, isAuthenticated, isDevUser, inIframe]);
70
+
71
+ // Build the direct Space URL for the "open in new tab" link
72
+ const spaceHost = typeof window !== 'undefined'
73
+ ? window.location.hostname.includes('.hf.space')
74
+ ? window.location.origin
75
+ : `https://smolagents-ml-agent.hf.space`
76
+ : '';
77
 
78
  return (
79
  <Box
 
88
  py: 8,
89
  }}
90
  >
91
+ {/* HF Logo */}
92
  <Box
93
  component="img"
94
  src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
95
  alt="Hugging Face"
96
+ sx={{ width: 96, height: 96, mb: 3, display: 'block' }}
 
 
 
 
 
97
  />
98
 
99
  {/* Title */}
 
121
  fontSize: '0.95rem',
122
  textAlign: 'center',
123
  px: 2,
124
+ '& strong': { color: 'var(--text)', fontWeight: 600 },
 
 
 
125
  }}
126
  >
127
  A general-purpose AI agent for <strong>machine learning engineering</strong>.
 
130
  and explores <strong>datasets</strong> β€” all through natural conversation.
131
  </Typography>
132
 
133
+ {/* Action button β€” depends on context */}
134
+ {inIframe && !isAuthenticated && !isDevUser ? (
135
+ // In iframe + not logged in β†’ link to open Space directly
136
+ <Button
137
+ variant="contained"
138
+ size="large"
139
+ component="a"
140
+ href={spaceHost}
141
+ target="_blank"
142
+ rel="noopener noreferrer"
143
+ endIcon={<OpenInNewIcon />}
144
+ sx={{
145
+ px: 5,
146
+ py: 1.5,
147
+ fontSize: '1rem',
148
+ fontWeight: 700,
149
+ textTransform: 'none',
150
+ borderRadius: '12px',
151
+ bgcolor: HF_ORANGE,
152
+ color: '#000',
153
+ boxShadow: '0 4px 24px rgba(255, 157, 0, 0.3)',
154
+ textDecoration: 'none',
155
+ '&:hover': {
156
+ bgcolor: '#FFB340',
157
+ boxShadow: '0 6px 32px rgba(255, 157, 0, 0.45)',
158
+ },
159
+ }}
160
+ >
161
+ Open ML Agent
162
+ </Button>
163
+ ) : !isAuthenticated && !isDevUser ? (
164
+ // Direct access + not logged in β†’ sign in button
165
+ <Button
166
+ variant="contained"
167
+ size="large"
168
+ onClick={() => triggerLogin()}
169
+ sx={{
170
+ px: 5,
171
+ py: 1.5,
172
+ fontSize: '1rem',
173
+ fontWeight: 700,
174
+ textTransform: 'none',
175
+ borderRadius: '12px',
176
+ bgcolor: HF_ORANGE,
177
+ color: '#000',
178
+ boxShadow: '0 4px 24px rgba(255, 157, 0, 0.3)',
179
+ '&:hover': {
180
+ bgcolor: '#FFB340',
181
+ boxShadow: '0 6px 32px rgba(255, 157, 0, 0.45)',
182
+ },
183
+ }}
184
+ >
185
+ Sign in with Hugging Face
186
+ </Button>
187
+ ) : (
188
+ // Authenticated or dev β†’ start session
189
+ <Button
190
+ variant="contained"
191
+ size="large"
192
+ onClick={handleStart}
193
+ disabled={isCreating}
194
+ startIcon={
195
+ isCreating ? <CircularProgress size={20} color="inherit" /> : null
196
+ }
197
+ sx={{
198
+ px: 5,
199
+ py: 1.5,
200
+ fontSize: '1rem',
201
+ fontWeight: 700,
202
+ textTransform: 'none',
203
+ borderRadius: '12px',
204
+ bgcolor: HF_ORANGE,
205
+ color: '#000',
206
+ boxShadow: '0 4px 24px rgba(255, 157, 0, 0.3)',
207
+ '&:hover': {
208
+ bgcolor: '#FFB340',
209
+ boxShadow: '0 6px 32px rgba(255, 157, 0, 0.45)',
210
+ },
211
+ '&.Mui-disabled': {
212
+ bgcolor: 'rgba(255, 157, 0, 0.35)',
213
+ color: 'rgba(0,0,0,0.45)',
214
+ },
215
+ }}
216
+ >
217
+ {isCreating ? 'Initializing...' : 'Start Session'}
218
+ </Button>
219
+ )}
220
 
221
  {/* Error */}
222
  {error && (
 
239
  {/* Footnote */}
240
  <Typography
241
  variant="caption"
242
+ sx={{ mt: 5, color: 'var(--muted-text)', opacity: 0.5, fontSize: '0.7rem' }}
 
 
 
 
 
243
  >
244
  Conversations are stored locally in your browser.
245
  </Typography>
frontend/src/hooks/useAuth.ts CHANGED
@@ -1,71 +1,34 @@
1
  /**
2
- * Client-side OAuth using @huggingface/hub.
3
  *
4
- * Works inside HF Spaces iframes (no third-party cookies needed).
5
- * Token is stored in localStorage and sent via Authorization header.
 
 
 
6
  */
7
 
8
  import { useEffect } from 'react';
9
- import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
10
  import { useAgentStore } from '@/store/agentStore';
11
  import { logger } from '@/utils/logger';
12
 
13
- const TOKEN_KEY = 'hf_oauth_token';
14
-
15
- /** Get the stored HF access token (or null). */
16
- export function getStoredToken(): string | null {
17
- try {
18
- return localStorage.getItem(TOKEN_KEY);
19
- } catch {
20
- return null;
21
- }
22
- }
23
-
24
- /** Clear the stored token (logout). */
25
- export function clearStoredToken(): void {
26
  try {
27
- localStorage.removeItem(TOKEN_KEY);
28
  } catch {
29
- // Ignore
30
  }
31
  }
32
 
33
- /** Redirect to HF OAuth login.
34
- * Strategy:
35
- * 1. Try @huggingface/hub client-side OAuth (works in HF iframe with window.huggingface)
36
- * 2. Fall back to server-side /auth/login (works on direct access, handles cookies)
37
- * 3. In iframe context, open in new tab to avoid cookie issues */
38
- export async function triggerLogin(): Promise<void> {
39
- let url: string;
40
-
41
- try {
42
- // Client-side OAuth β€” needs window.huggingface (HF iframe only)
43
- url = await oauthLoginUrl({
44
- scopes: 'openid profile read-repos write-repos manage-repos inference-api jobs',
45
- });
46
- } catch {
47
- // Fallback: server-side OAuth (works on direct access)
48
- url = '/auth/login';
49
- }
50
-
51
- // In an iframe, open in a new tab (cookies blocked otherwise)
52
- let inIframe = false;
53
- try {
54
- inIframe = window.top !== window.self;
55
- } catch {
56
- inIframe = true; // SecurityError = cross-origin iframe
57
- }
58
-
59
- if (inIframe) {
60
- window.open(url, '_blank');
61
- } else {
62
- window.location.href = url;
63
- }
64
  }
65
 
66
  /**
67
- * Hook: on mount, check for OAuth redirect result or existing token.
68
- * Sets the user in the agent store when authenticated.
69
  */
70
  export function useAuth() {
71
  const setUser = useAgentStore((s) => s.setUser);
@@ -73,41 +36,12 @@ export function useAuth() {
73
  useEffect(() => {
74
  let cancelled = false;
75
 
76
- async function init() {
77
- // 1. Check if we're returning from an OAuth redirect
78
- const oauthResult = await oauthHandleRedirectIfPresent();
79
-
80
- if (oauthResult) {
81
- // Store the access token
82
- localStorage.setItem(TOKEN_KEY, oauthResult.accessToken);
83
- logger.log('OAuth login successful:', oauthResult.userInfo?.name);
84
-
85
- if (!cancelled) {
86
- setUser({
87
- authenticated: true,
88
- username: oauthResult.userInfo?.name || oauthResult.userInfo?.preferred_username || 'user',
89
- name: oauthResult.userInfo?.name,
90
- picture: oauthResult.userInfo?.picture,
91
- });
92
- }
93
- return;
94
- }
95
-
96
- // 2. Check for existing token in localStorage
97
- const token = getStoredToken();
98
- if (!token) {
99
- // Not logged in β€” welcome screen will handle login trigger
100
- if (!cancelled) setUser(null);
101
- return;
102
- }
103
-
104
- // 3. Validate the stored token
105
  try {
106
- const res = await fetch('/auth/me', {
107
- headers: { Authorization: `Bearer ${token}` },
108
- });
109
- if (res.ok) {
110
- const data = await res.json();
111
  if (!cancelled && data.authenticated) {
112
  setUser({
113
  authenticated: true,
@@ -115,19 +49,29 @@ export function useAuth() {
115
  name: data.name,
116
  picture: data.picture,
117
  });
 
118
  return;
119
  }
120
  }
121
- // Token invalid β€” clear it
122
- clearStoredToken();
 
 
 
 
 
 
 
 
 
123
  if (!cancelled) setUser(null);
124
  } catch {
125
- // Backend unreachable in dev β€” set dev user
126
  if (!cancelled) setUser({ authenticated: true, username: 'dev' });
127
  }
128
  }
129
 
130
- init();
131
  return () => { cancelled = true; };
132
  }, [setUser]);
133
  }
 
1
  /**
2
+ * Authentication hook β€” simple server-side OAuth.
3
  *
4
+ * - Hors iframe: /auth/login redirect (cookies work fine)
5
+ * - Dans iframe: show "Open in full page" link
6
+ *
7
+ * Token is stored via HttpOnly cookie by the backend.
8
+ * In dev mode (no OAUTH_CLIENT_ID), auth is bypassed.
9
  */
10
 
11
  import { useEffect } from 'react';
 
12
  import { useAgentStore } from '@/store/agentStore';
13
  import { logger } from '@/utils/logger';
14
 
15
+ /** Check if we're running inside an iframe. */
16
+ export function isInIframe(): boolean {
 
 
 
 
 
 
 
 
 
 
 
17
  try {
18
+ return window.top !== window.self;
19
  } catch {
20
+ return true; // SecurityError = cross-origin iframe
21
  }
22
  }
23
 
24
+ /** Redirect to the server-side OAuth login. */
25
+ export function triggerLogin(): void {
26
+ window.location.href = '/auth/login';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
 
29
  /**
30
+ * Hook: on mount, check if user is authenticated.
31
+ * Sets user in the agent store.
32
  */
33
  export function useAuth() {
34
  const setUser = useAgentStore((s) => s.setUser);
 
36
  useEffect(() => {
37
  let cancelled = false;
38
 
39
+ async function checkAuth() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  try {
41
+ // Check if user is already authenticated (cookie-based)
42
+ const response = await fetch('/auth/me', { credentials: 'include' });
43
+ if (response.ok) {
44
+ const data = await response.json();
 
45
  if (!cancelled && data.authenticated) {
46
  setUser({
47
  authenticated: true,
 
49
  name: data.name,
50
  picture: data.picture,
51
  });
52
+ logger.log('Authenticated as', data.username);
53
  return;
54
  }
55
  }
56
+
57
+ // Not authenticated β€” check if auth is enabled
58
+ const statusRes = await fetch('/auth/status', { credentials: 'include' });
59
+ const statusData = await statusRes.json();
60
+ if (!statusData.auth_enabled) {
61
+ // Dev mode β€” no OAuth configured
62
+ if (!cancelled) setUser({ authenticated: true, username: 'dev' });
63
+ return;
64
+ }
65
+
66
+ // Auth enabled but not logged in β€” welcome screen will handle it
67
  if (!cancelled) setUser(null);
68
  } catch {
69
+ // Backend unreachable β€” assume dev mode
70
  if (!cancelled) setUser({ authenticated: true, username: 'dev' });
71
  }
72
  }
73
 
74
+ checkAuth();
75
  return () => { cancelled = true; };
76
  }, [setUser]);
77
  }
frontend/src/utils/api.ts CHANGED
@@ -1,14 +1,13 @@
1
  /**
2
  * Centralized API utilities.
3
  *
4
- * Reads the HF OAuth token from localStorage and injects it as
5
- * an Authorization: Bearer header on every request.
6
- * WebSocket URLs include the token as a query parameter.
7
  */
8
 
9
- import { getStoredToken, triggerLogin } from '@/hooks/useAuth';
10
 
11
- /** Wrapper around fetch that includes auth and common headers. */
12
  export async function apiFetch(
13
  path: string,
14
  options: RequestInit = {}
@@ -18,45 +17,31 @@ export async function apiFetch(
18
  ...(options.headers as Record<string, string>),
19
  };
20
 
21
- // Inject Bearer token if available
22
- const token = getStoredToken();
23
- if (token) {
24
- headers['Authorization'] = `Bearer ${token}`;
25
- }
26
-
27
  const response = await fetch(path, {
28
  ...options,
29
  headers,
30
- credentials: 'include', // Still send cookies for backward compat
31
  });
32
 
33
- // Handle 401 β€” trigger login if auth is required
34
  if (response.status === 401) {
35
  try {
36
- const authStatus = await fetch('/auth/status');
37
  const data = await authStatus.json();
38
  if (data.auth_enabled) {
39
- await triggerLogin();
40
  throw new Error('Authentication required β€” redirecting to login.');
41
  }
42
  } catch (e) {
43
  if (e instanceof Error && e.message.includes('redirecting')) throw e;
44
- // auth/status failed β€” ignore
45
  }
46
  }
47
 
48
  return response;
49
  }
50
 
51
- /** Build the WebSocket URL for a session, including auth token. */
52
  export function getWebSocketUrl(sessionId: string): string {
53
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
54
- const base = `${protocol}//${window.location.host}/api/ws/${sessionId}`;
55
-
56
- // Pass token as query param (WebSocket can't set custom headers from browser)
57
- const token = getStoredToken();
58
- if (token) {
59
- return `${base}?token=${encodeURIComponent(token)}`;
60
- }
61
- return base;
62
  }
 
1
  /**
2
  * Centralized API utilities.
3
  *
4
+ * In production: HttpOnly cookie (hf_access_token) is sent automatically.
5
+ * In development: auth is bypassed on the backend.
 
6
  */
7
 
8
+ import { triggerLogin } from '@/hooks/useAuth';
9
 
10
+ /** Wrapper around fetch with credentials and common headers. */
11
  export async function apiFetch(
12
  path: string,
13
  options: RequestInit = {}
 
17
  ...(options.headers as Record<string, string>),
18
  };
19
 
 
 
 
 
 
 
20
  const response = await fetch(path, {
21
  ...options,
22
  headers,
23
+ credentials: 'include', // Send cookies with every request
24
  });
25
 
26
+ // Handle 401 β€” redirect to login
27
  if (response.status === 401) {
28
  try {
29
+ const authStatus = await fetch('/auth/status', { credentials: 'include' });
30
  const data = await authStatus.json();
31
  if (data.auth_enabled) {
32
+ triggerLogin();
33
  throw new Error('Authentication required β€” redirecting to login.');
34
  }
35
  } catch (e) {
36
  if (e instanceof Error && e.message.includes('redirecting')) throw e;
 
37
  }
38
  }
39
 
40
  return response;
41
  }
42
 
43
+ /** Build the WebSocket URL for a session. */
44
  export function getWebSocketUrl(sessionId: string): string {
45
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
46
+ return `${protocol}//${window.location.host}/api/ws/${sessionId}`;
 
 
 
 
 
 
 
47
  }