alpha
Login
or
Join now
atpota.to
/
cred.blue
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
This repository has no description
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
switch to client side auth
author
damedotblog
date
1 year ago
(Apr 22, 2025, 10:15 AM -0400)
commit
a7ab42ea
a7ab42ea83ad17d3cc5e840230495ac33abcf6ce
parent
e8f0c6da
e8f0c6dac90892fd6a9ee1775fe17072ee50ed9e
+317
-715
5 changed files
Expand all
Collapse all
Unified
Split
package-lock.json
package.json
src
components
Login
Login.js
LoginCallback.js
contexts
AuthContext.js
+87
-59
package-lock.json
Reviewed
···
11
11
"@atcute/client": "^2.0.6",
12
12
"@atcute/oauth-browser-client": "^1.0.7",
13
13
"@atproto/api": "^0.13.22",
14
14
-
"@atproto/oauth-client-browser": "^0.3.12",
14
14
+
"@atproto/oauth-client-browser": "^0.3.15",
15
15
"@atproto/oauth-client-node": "^0.2.4",
16
16
"@fortawesome/free-solid-svg-icons": "^6.7.2",
17
17
"@fortawesome/react-fontawesome": "^0.2.2",
···
201
201
}
202
202
},
203
203
"node_modules/@atproto/common-web": {
204
204
-
"version": "0.4.0",
205
205
-
"resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.0.tgz",
206
206
-
"integrity": "sha512-ZYL0P9myHybNgwh/hBY0HaBzqiLR1B5/ie5bJpLQAg0whRzNA28t8/nU2vh99tbsWcAF0LOD29M8++LyENJLNQ==",
204
204
+
"version": "0.4.1",
205
205
+
"resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.1.tgz",
206
206
+
"integrity": "sha512-Ghh+djHYMAUCktLKwr2IuGgtjcwSWGudp+K7+N7KBA9pDDloOXUEY8Agjc5SHSo9B1QIEFkegClU5n+apn2e0w==",
207
207
"license": "MIT",
208
208
"dependencies": {
209
209
"graphemer": "^1.4.0",
···
253
253
}
254
254
},
255
255
"node_modules/@atproto/lexicon": {
256
256
-
"version": "0.4.9",
257
257
-
"resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.9.tgz",
258
258
-
"integrity": "sha512-/tmEuHQFr51V2V7EAVJzaA40sqJ7ylAZpR962VbOsPtmcdOHvezbjVHYEMXgfb927hS+xqbVyzBTbu5w9v8prA==",
256
256
+
"version": "0.4.10",
257
257
+
"resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.10.tgz",
258
258
+
"integrity": "sha512-uDbP20vetBgtXPuxoyRcvOGBt2gNe1dFc9yYKcb6jWmXfseHiGTnIlORJOLBXIT2Pz15Eap4fLxAu6zFAykD5A==",
259
259
"license": "MIT",
260
260
"dependencies": {
261
261
-
"@atproto/common-web": "^0.4.0",
261
261
+
"@atproto/common-web": "^0.4.1",
262
262
"@atproto/syntax": "^0.4.0",
263
263
"iso-datestring-validator": "^2.2.2",
264
264
"multiformats": "^9.9.0",
···
292
292
}
293
293
},
294
294
"node_modules/@atproto/oauth-client-browser": {
295
295
-
"version": "0.3.12",
296
296
-
"resolved": "https://registry.npmjs.org/@atproto/oauth-client-browser/-/oauth-client-browser-0.3.12.tgz",
297
297
-
"integrity": "sha512-VmpBoNIMOzfRZyAlimhBZ5WZftIf/Ysyi77cytxL9gHKIyB/tTUgB1oB4zSv3Oid01bPQHx73sMMIVfYEZ1Fpw==",
295
295
+
"version": "0.3.15",
296
296
+
"resolved": "https://registry.npmjs.org/@atproto/oauth-client-browser/-/oauth-client-browser-0.3.15.tgz",
297
297
+
"integrity": "sha512-sYfjpBsiiWiQTJbPZU234uOKI3613Pi5X4Z0KLIbDpBdH+HtvcjmsVRrYH4d6WBJUqezxsPCuW/oj5uA87yk1Q==",
298
298
"license": "MIT",
299
299
"dependencies": {
300
300
-
"@atproto-labs/did-resolver": "0.1.11",
301
301
-
"@atproto-labs/handle-resolver": "0.1.7",
302
302
-
"@atproto-labs/simple-store": "0.1.2",
300
300
+
"@atproto-labs/did-resolver": "0.1.12",
301
301
+
"@atproto-labs/handle-resolver": "0.1.8",
302
302
+
"@atproto-labs/simple-store": "0.2.0",
303
303
"@atproto/did": "0.1.5",
304
304
-
"@atproto/jwk": "0.1.4",
305
305
-
"@atproto/jwk-webcrypto": "0.1.5",
306
306
-
"@atproto/oauth-client": "0.3.12",
307
307
-
"@atproto/oauth-types": "0.2.4"
304
304
+
"@atproto/jwk": "0.1.5",
305
305
+
"@atproto/jwk-webcrypto": "0.1.6",
306
306
+
"@atproto/oauth-client": "0.3.15",
307
307
+
"@atproto/oauth-types": "0.2.6"
308
308
}
309
309
},
310
310
"node_modules/@atproto/oauth-client-browser/node_modules/@atproto-labs/did-resolver": {
311
311
-
"version": "0.1.11",
312
312
-
"resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.1.11.tgz",
313
313
-
"integrity": "sha512-qXNzIX2GPQnxT1gl35nv/8ErDdc4Fj/+RlJE7oyE7JGkFAPUyuY03TvKJ79SmWFsWE8wyTXEpLuphr9Da1Vhkw==",
311
311
+
"version": "0.1.12",
312
312
+
"resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.1.12.tgz",
313
313
+
"integrity": "sha512-criWN7o21C5TFsauB+bGTlkqqerOU6gT2TbxdQVgZUWqNcfazUmUjT4gJAY02i+O4d3QmZa27fv9CcaRKWkSug==",
314
314
"license": "MIT",
315
315
"dependencies": {
316
316
"@atproto-labs/fetch": "0.2.2",
317
317
"@atproto-labs/pipe": "0.1.0",
318
318
-
"@atproto-labs/simple-store": "0.1.2",
319
319
-
"@atproto-labs/simple-store-memory": "0.1.2",
318
318
+
"@atproto-labs/simple-store": "0.2.0",
319
319
+
"@atproto-labs/simple-store-memory": "0.1.3",
320
320
"@atproto/did": "0.1.5",
321
321
"zod": "^3.23.8"
322
322
}
···
330
330
"@atproto-labs/pipe": "0.1.0"
331
331
}
332
332
},
333
333
+
"node_modules/@atproto/oauth-client-browser/node_modules/@atproto-labs/handle-resolver": {
334
334
+
"version": "0.1.8",
335
335
+
"resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.1.8.tgz",
336
336
+
"integrity": "sha512-Y0ckccoCGDo/3g4thPkgp9QcORmc+qqEaCBCYCZYtfLIQp4775u22wd+4fyEyJP4DqoReKacninkICgRGfs3dQ==",
337
337
+
"license": "MIT",
338
338
+
"dependencies": {
339
339
+
"@atproto-labs/simple-store": "0.2.0",
340
340
+
"@atproto-labs/simple-store-memory": "0.1.3",
341
341
+
"@atproto/did": "0.1.5",
342
342
+
"zod": "^3.23.8"
343
343
+
}
344
344
+
},
333
345
"node_modules/@atproto/oauth-client-browser/node_modules/@atproto-labs/identity-resolver": {
334
334
-
"version": "0.1.15",
335
335
-
"resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.1.15.tgz",
336
336
-
"integrity": "sha512-3ABob5iUDoFL85I8/pJE4wncz3148fADoxNVAdksyACxxjpH1GNhSYNyIpRpdMCJ/kjj69DM9rggumTHqnD/Xg==",
346
346
+
"version": "0.1.16",
347
347
+
"resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.1.16.tgz",
348
348
+
"integrity": "sha512-pFrtKT49cYBhCDd2U1t/CcUBiMmQzaNQxh8oSkDUlGs/K3P8rJFTAGAMm8UjokfGEKwF4hX9oo7O8Kn+GkyExw==",
337
349
"license": "MIT",
338
350
"dependencies": {
339
339
-
"@atproto-labs/did-resolver": "0.1.11",
340
340
-
"@atproto-labs/handle-resolver": "0.1.7",
351
351
+
"@atproto-labs/did-resolver": "0.1.12",
352
352
+
"@atproto-labs/handle-resolver": "0.1.8",
341
353
"@atproto/syntax": "0.4.0"
342
354
}
343
355
},
356
356
+
"node_modules/@atproto/oauth-client-browser/node_modules/@atproto-labs/simple-store": {
357
357
+
"version": "0.2.0",
358
358
+
"resolved": "https://registry.npmjs.org/@atproto-labs/simple-store/-/simple-store-0.2.0.tgz",
359
359
+
"integrity": "sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA==",
360
360
+
"license": "MIT"
361
361
+
},
362
362
+
"node_modules/@atproto/oauth-client-browser/node_modules/@atproto-labs/simple-store-memory": {
363
363
+
"version": "0.1.3",
364
364
+
"resolved": "https://registry.npmjs.org/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.3.tgz",
365
365
+
"integrity": "sha512-jkitT9+AtU+0b28DoN92iURLaCt/q/q4yX8q6V+9LSwYlUTqKoj/5NFKvF7x6EBuG+gpUdlcycbH7e60gjOhRQ==",
366
366
+
"license": "MIT",
367
367
+
"dependencies": {
368
368
+
"@atproto-labs/simple-store": "0.2.0",
369
369
+
"lru-cache": "^10.2.0"
370
370
+
}
371
371
+
},
344
372
"node_modules/@atproto/oauth-client-browser/node_modules/@atproto/jwk": {
345
345
-
"version": "0.1.4",
346
346
-
"resolved": "https://registry.npmjs.org/@atproto/jwk/-/jwk-0.1.4.tgz",
347
347
-
"integrity": "sha512-dSRuEi0FbxL5ln6hEFHp5ZW01xbQH9yJi5odZaEYpcA6beZHf/bawlU12CQy/CDsbC3FxSqrBw7Q2t7mvdSBqw==",
373
373
+
"version": "0.1.5",
374
374
+
"resolved": "https://registry.npmjs.org/@atproto/jwk/-/jwk-0.1.5.tgz",
375
375
+
"integrity": "sha512-OzZFLhX41TOcMeanP3aZlL5bLeaUIZT15MI4aU5cwflNq/rwpGOpz3uwDjZc8ytgUjuTQ8LabSz5jMmwoTSWFg==",
348
376
"license": "MIT",
349
377
"dependencies": {
350
378
"multiformats": "^9.9.0",
···
352
380
}
353
381
},
354
382
"node_modules/@atproto/oauth-client-browser/node_modules/@atproto/jwk-jose": {
355
355
-
"version": "0.1.5",
356
356
-
"resolved": "https://registry.npmjs.org/@atproto/jwk-jose/-/jwk-jose-0.1.5.tgz",
357
357
-
"integrity": "sha512-piYZ3ohKhRiGlD6/bZCV/Ed3lIi7CVd6txbofEHik22EkYWK0nWKoEriCUSTssSylwFzeOq2r31Ut16WcJoghw==",
383
383
+
"version": "0.1.6",
384
384
+
"resolved": "https://registry.npmjs.org/@atproto/jwk-jose/-/jwk-jose-0.1.6.tgz",
385
385
+
"integrity": "sha512-r4DGMvvmazy6CxqAcnplpUxvp6Vd8UwKxQBZRpmm1aNsVonf5qj1yeDkECTiwoe/FPbvtdamlzClB3UZc7Yb5w==",
358
386
"license": "MIT",
359
387
"dependencies": {
360
360
-
"@atproto/jwk": "0.1.4",
388
388
+
"@atproto/jwk": "0.1.5",
361
389
"jose": "^5.2.0"
362
390
}
363
391
},
364
392
"node_modules/@atproto/oauth-client-browser/node_modules/@atproto/jwk-webcrypto": {
365
365
-
"version": "0.1.5",
366
366
-
"resolved": "https://registry.npmjs.org/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.1.5.tgz",
367
367
-
"integrity": "sha512-xsX8cJO6rBakLz8zNKenuKIbjoTeaiMi/ETOFFYGtlMlk1grdxDOe6OGpCmUDXaOiYWu3x5K5Hc4J9ooI/4nRg==",
393
393
+
"version": "0.1.6",
394
394
+
"resolved": "https://registry.npmjs.org/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.1.6.tgz",
395
395
+
"integrity": "sha512-mxWHOvlg+HGohldfiaon1fNsr7iDvKrTrkV0/ZvymWRzxsDFPCon1hu8OtKLXUVgLh+IzDJT1D79I4fBSo4pog==",
368
396
"license": "MIT",
369
397
"dependencies": {
370
370
-
"@atproto/jwk": "0.1.4",
371
371
-
"@atproto/jwk-jose": "0.1.5",
398
398
+
"@atproto/jwk": "0.1.5",
399
399
+
"@atproto/jwk-jose": "0.1.6",
372
400
"zod": "^3.23.8"
373
401
}
374
402
},
375
403
"node_modules/@atproto/oauth-client-browser/node_modules/@atproto/oauth-client": {
376
376
-
"version": "0.3.12",
377
377
-
"resolved": "https://registry.npmjs.org/@atproto/oauth-client/-/oauth-client-0.3.12.tgz",
378
378
-
"integrity": "sha512-tDQzq/6PNspWeHBrYGZ+LU7t7PM/+0Ealk8V2PUfncGObLz0iPDS4RbWAzy0GH1foee1qIfRdiiKiEymMGIZDw==",
404
404
+
"version": "0.3.15",
405
405
+
"resolved": "https://registry.npmjs.org/@atproto/oauth-client/-/oauth-client-0.3.15.tgz",
406
406
+
"integrity": "sha512-kBv+sk2P5nqGjVSxF26ZswsGKbCfWmxCRpd90TRlNtxo29ZJrYxWySTZrnReXjMellMrTqJEUuTBIasGrXk5Jg==",
379
407
"license": "MIT",
380
408
"dependencies": {
381
381
-
"@atproto-labs/did-resolver": "0.1.11",
409
409
+
"@atproto-labs/did-resolver": "0.1.12",
382
410
"@atproto-labs/fetch": "0.2.2",
383
383
-
"@atproto-labs/handle-resolver": "0.1.7",
384
384
-
"@atproto-labs/identity-resolver": "0.1.15",
385
385
-
"@atproto-labs/simple-store": "0.1.2",
386
386
-
"@atproto-labs/simple-store-memory": "0.1.2",
411
411
+
"@atproto-labs/handle-resolver": "0.1.8",
412
412
+
"@atproto-labs/identity-resolver": "0.1.16",
413
413
+
"@atproto-labs/simple-store": "0.2.0",
414
414
+
"@atproto-labs/simple-store-memory": "0.1.3",
387
415
"@atproto/did": "0.1.5",
388
388
-
"@atproto/jwk": "0.1.4",
389
389
-
"@atproto/oauth-types": "0.2.4",
390
390
-
"@atproto/xrpc": "0.6.11",
416
416
+
"@atproto/jwk": "0.1.5",
417
417
+
"@atproto/oauth-types": "0.2.6",
418
418
+
"@atproto/xrpc": "0.6.12",
391
419
"multiformats": "^9.9.0",
392
420
"zod": "^3.23.8"
393
421
}
394
422
},
395
423
"node_modules/@atproto/oauth-client-browser/node_modules/@atproto/oauth-types": {
396
396
-
"version": "0.2.4",
397
397
-
"resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.2.4.tgz",
398
398
-
"integrity": "sha512-V2LnlXi1CSmBQWTQgDm8l4oN7xYxlftVwM7hrvYNP+Jxo3Ozfe0QLK1Wy/CH6/ZqzrBBhYvcbf4DJYTUwPA+hw==",
424
424
+
"version": "0.2.6",
425
425
+
"resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.2.6.tgz",
426
426
+
"integrity": "sha512-6rUmV7T1YKCgVYLLjm+FGv+dYC8S0+0AHji/azVGDEhTsiadSrlC0H9Pgxix1y89zI1FIf0piBqecBcPewdrJg==",
399
427
"license": "MIT",
400
428
"dependencies": {
401
401
-
"@atproto/jwk": "0.1.4",
429
429
+
"@atproto/jwk": "0.1.5",
402
430
"zod": "^3.23.8"
403
431
}
404
432
},
···
409
437
"license": "MIT"
410
438
},
411
439
"node_modules/@atproto/oauth-client-browser/node_modules/@atproto/xrpc": {
412
412
-
"version": "0.6.11",
413
413
-
"resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.11.tgz",
414
414
-
"integrity": "sha512-J2cZP8FjoDN0UkyTYBlCvKvxwBbDm4dld47u6FQK30RJy9YpSiUkdxJJ10NYqpi7JVny3M0qWQgpWJDV94+PdA==",
440
440
+
"version": "0.6.12",
441
441
+
"resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.12.tgz",
442
442
+
"integrity": "sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w==",
415
443
"license": "MIT",
416
444
"dependencies": {
417
417
-
"@atproto/lexicon": "^0.4.9",
445
445
+
"@atproto/lexicon": "^0.4.10",
418
446
"zod": "^3.23.8"
419
447
}
420
448
},
+1
-1
package.json
Reviewed
···
6
6
"@atcute/client": "^2.0.6",
7
7
"@atcute/oauth-browser-client": "^1.0.7",
8
8
"@atproto/api": "^0.13.22",
9
9
-
"@atproto/oauth-client-browser": "^0.3.12",
9
9
+
"@atproto/oauth-client-browser": "^0.3.15",
10
10
"@atproto/oauth-client-node": "^0.2.4",
11
11
"@fortawesome/free-solid-svg-icons": "^6.7.2",
12
12
"@fortawesome/react-fontawesome": "^0.2.2",
+40
-92
src/components/Login/Login.js
Reviewed
···
1
1
-
import React, { useState, useEffect, useRef } from 'react';
2
2
-
import { useNavigate, useLocation } from 'react-router-dom';
1
1
+
import React, { useState, useEffect } from 'react';
2
2
+
import { useLocation, useNavigate } from 'react-router-dom';
3
3
import { useAuth } from '../../contexts/AuthContext';
4
4
import './Login.css';
5
5
6
6
const Login = () => {
7
7
const [handle, setHandle] = useState('');
8
8
-
const [isLoading, setIsLoading] = useState(false);
9
9
-
const [error, setError] = useState('');
10
10
-
const [returnUrl, setReturnUrl] = useState('');
11
11
-
const { login, isAuthenticated, loading } = useAuth();
8
8
+
const { login, loading, error, isAuthenticated } = useAuth();
12
9
const navigate = useNavigate();
13
10
const location = useLocation();
14
14
-
const hasRedirected = useRef(false);
15
11
16
16
-
// Extract returnUrl from query params
17
17
-
useEffect(() => {
18
18
-
const searchParams = new URLSearchParams(location.search);
19
19
-
const returnPath = searchParams.get('returnUrl');
20
20
-
if (returnPath) {
21
21
-
setReturnUrl(returnPath);
22
22
-
}
23
23
-
}, [location]);
12
12
+
const queryParams = new URLSearchParams(location.search);
13
13
+
const returnUrl = queryParams.get('returnUrl') || '/';
24
14
25
25
-
// Redirect if already authenticated
26
15
useEffect(() => {
27
27
-
// Skip redirection if loading is still true
28
28
-
if (loading) return;
29
29
-
30
30
-
// Only redirect once to prevent loop
31
31
-
if (isAuthenticated && !hasRedirected.current) {
32
32
-
hasRedirected.current = true;
33
33
-
// Navigate to return URL if it exists, otherwise to home
34
34
-
navigate(returnUrl || '/');
35
35
-
}
36
36
-
}, [isAuthenticated, navigate, returnUrl, loading]);
37
37
-
38
38
-
const handleSubmit = async (e) => {
39
39
-
e.preventDefault();
40
40
-
41
41
-
// Don't allow login attempt if already authenticated
42
16
if (isAuthenticated) {
43
43
-
navigate(returnUrl || '/');
44
44
-
return;
17
17
+
console.log('Already authenticated, redirecting from Login page to:', returnUrl);
18
18
+
navigate(returnUrl);
45
19
}
46
46
-
47
47
-
if (!handle) {
48
48
-
setError('Please enter your Bluesky handle');
49
49
-
return;
50
50
-
}
51
51
-
52
52
-
// Don't allow multiple concurrent login attempts
53
53
-
if (isLoading) return;
54
54
-
55
55
-
setIsLoading(true);
56
56
-
setError('');
57
57
-
58
58
-
try {
59
59
-
// Pass returnUrl to login function
60
60
-
await login(handle, returnUrl);
61
61
-
// Note: This code won't run because login redirects to Bluesky OAuth page
62
62
-
} catch (err) {
63
63
-
setError('Authentication failed. Please try again.');
64
64
-
setIsLoading(false);
65
65
-
}
20
20
+
}, [isAuthenticated, navigate, returnUrl]);
21
21
+
22
22
+
const handleInputChange = (event) => {
23
23
+
setHandle(event.target.value);
66
24
};
67
25
26
26
+
const handleSubmit = async (event) => {
27
27
+
event.preventDefault();
28
28
+
console.log(`Login attempt for handle: ${handle || 'default PDS'}, returnUrl: ${returnUrl}`);
29
29
+
await login(handle || null, returnUrl);
30
30
+
};
31
31
+
32
32
+
if (isAuthenticated) {
33
33
+
return <div>Redirecting...</div>;
34
34
+
}
35
35
+
68
36
return (
69
37
<div className="login-container">
70
70
-
<div className="login-card">
71
71
-
<h2>Login with Bluesky</h2>
72
72
-
<p>Sign in with your Bluesky handle to access protected features.</p>
73
73
-
74
74
-
{returnUrl && (
75
75
-
<div className="return-notice">
76
76
-
<p>You'll be redirected back to the page you were trying to access after logging in.</p>
77
77
-
</div>
78
78
-
)}
79
79
-
80
80
-
{error && <div className="login-error">{error}</div>}
81
81
-
82
82
-
<form onSubmit={handleSubmit} className="login-form">
83
83
-
<input
84
84
-
id="handle"
85
85
-
type="text"
86
86
-
value={handle}
87
87
-
onChange={(e) => setHandle(e.target.value)}
88
88
-
placeholder="yourhandle.bsky.social"
89
89
-
disabled={isLoading || isAuthenticated}
90
90
-
autoFocus
91
91
-
/>
92
92
-
93
93
-
<button
94
94
-
type="submit"
95
95
-
className="login-button"
96
96
-
disabled={isLoading || isAuthenticated}
97
97
-
>
98
98
-
{isLoading ? 'Connecting...' : 'Login with Bluesky'}
99
99
-
</button>
100
100
-
</form>
101
101
-
102
102
-
<div className="login-info">
103
103
-
<p>
104
104
-
We use Bluesky's authentication service to verify your identity.
105
105
-
No passwords are stored by cred.blue.
106
106
-
</p>
107
107
-
</div>
108
108
-
</div>
38
38
+
<h1>Login to Cred Blue</h1>
39
39
+
<p>Enter your Bluesky handle (e.g., yourname.bsky.social) or leave blank to use bsky.social.</p>
40
40
+
<form onSubmit={handleSubmit}>
41
41
+
<input
42
42
+
type="text"
43
43
+
value={handle}
44
44
+
onChange={handleInputChange}
45
45
+
placeholder="yourname.bsky.social (optional)"
46
46
+
aria-label="Bluesky Handle (optional)"
47
47
+
/>
48
48
+
{loading && <p>Processing...</p>}
49
49
+
{error && <p className="error-message">Login failed: {error}</p>}
50
50
+
<button type="submit" disabled={loading}>
51
51
+
{loading ? 'Processing...' : 'Login with Bluesky'}
52
52
+
</button>
53
53
+
</form>
54
54
+
<p className="privacy-note">
55
55
+
We use official Bluesky authentication. We don't see or store your password.
56
56
+
</p>
109
57
</div>
110
58
);
111
59
};
+112
-136
src/components/Login/LoginCallback.js
Reviewed
···
1
1
-
import React, { useEffect, useState, useRef } from 'react';
2
2
-
import { Navigate, useLocation } from 'react-router-dom';
1
1
+
import React, { useEffect, useState } from 'react';
2
2
+
import { useNavigate } from 'react-router-dom';
3
3
import { useAuth } from '../../contexts/AuthContext';
4
4
-
import Loading from '../Loading/Loading';
4
4
+
import './LoginCallback.css'; // Optional: Add styles if needed
5
5
6
6
-
// This component handles the callback redirect from the Bluesky OAuth process
7
6
const LoginCallback = () => {
8
8
-
const { loading, isAuthenticated, session, checkAuthStatus } = useAuth();
7
7
+
const { client } = useAuth(); // Get the client instance from context
8
8
+
const navigate = useNavigate();
9
9
+
const [status, setStatus] = useState('Processing login...');
9
10
const [error, setError] = useState(null);
10
10
-
const [returnUrl, setReturnUrl] = useState('/');
11
11
-
const [processingComplete, setProcessingComplete] = useState(false);
12
12
-
const [isRedirecting, setIsRedirecting] = useState(false);
13
13
-
const location = useLocation();
14
14
-
const callbackAttempts = useRef(0);
15
15
-
const maxAttempts = 2;
16
16
-
17
17
-
// First effect: Check if we're already authenticated
11
11
+
18
12
useEffect(() => {
19
19
-
// If we already have authentication, we can skip the processing
20
20
-
if (isAuthenticated && session) {
21
21
-
console.log('Already authenticated in initial check, preparing immediate redirect');
22
22
-
setProcessingComplete(true);
23
23
-
setIsRedirecting(true);
24
24
-
}
25
25
-
}, []);
26
26
-
27
27
-
// Second effect: Process the callback if needed
28
28
-
useEffect(() => {
29
29
-
// Skip processing if:
30
30
-
// 1. We've already determined we should redirect
31
31
-
// 2. Processing is already complete
32
32
-
// 3. We've reached the maximum number of attempts
33
33
-
if (isRedirecting || processingComplete || callbackAttempts.current >= maxAttempts) {
34
34
-
return;
35
35
-
}
36
36
-
37
37
-
callbackAttempts.current += 1;
38
38
-
console.log(`Processing callback attempt ${callbackAttempts.current}`);
39
39
-
40
13
const handleCallback = async () => {
14
14
+
// Client might not be initialized immediately on page load
15
15
+
if (!client) {
16
16
+
setStatus('Waiting for authentication client...');
17
17
+
// Optionally add a small delay and retry or rely on AuthContext re-render
18
18
+
const timeoutId = setTimeout(() => {
19
19
+
if (!client) { // Check again after delay
20
20
+
console.error('OAuth client not available after delay.');
21
21
+
setError('Authentication client failed to load. Please try logging in again.');
22
22
+
setStatus(''); // Clear status message
23
23
+
}
24
24
+
// If client became available, the effect will re-run anyway
25
25
+
}, 2000); // Wait 2 seconds
26
26
+
return () => clearTimeout(timeoutId);
27
27
+
}
28
28
+
41
29
try {
42
42
-
// Extract return URL from state or session storage
43
43
-
const sessionReturnUrl = sessionStorage.getItem('returnUrl');
44
44
-
const params = new URLSearchParams(location.search);
45
45
-
const stateParam = params.get('state');
46
46
-
47
47
-
// Attempt to decode state if available
48
48
-
if (stateParam) {
49
49
-
try {
50
50
-
const decodedState = JSON.parse(atob(stateParam));
51
51
-
if (decodedState && decodedState.returnUrl) {
52
52
-
setReturnUrl(decodedState.returnUrl);
53
53
-
}
54
54
-
} catch (e) {
55
55
-
console.error('Failed to decode state parameter:', e);
56
56
-
}
57
57
-
}
58
58
-
59
59
-
// Use sessionStorage return URL if available (it takes precedence)
60
60
-
if (sessionReturnUrl) {
61
61
-
setReturnUrl(sessionReturnUrl);
62
62
-
sessionStorage.removeItem('returnUrl');
63
63
-
}
64
64
-
65
65
-
// Check for error in URL parameters
66
66
-
const errorParam = params.get('error');
67
67
-
if (errorParam) {
68
68
-
// Only set error if we don't have a session
69
69
-
if (!isAuthenticated || !session) {
70
70
-
setError(errorParam);
71
71
-
setProcessingComplete(true);
72
72
-
} else {
73
73
-
// We have a session despite the error param, so redirect
74
74
-
setIsRedirecting(true);
75
75
-
setProcessingComplete(true);
76
76
-
}
77
77
-
return;
78
78
-
}
30
30
+
console.log('(LoginCallback) Client available, attempting to handle callback...');
31
31
+
// The client.init() in AuthContext likely already handled the callback.
32
32
+
// We might not need client.callback() here if init handles it.
33
33
+
// However, keeping client.callback() as a fallback or direct handler can be robust.
34
34
+
// Check if init() already processed it by seeing if session exists
35
35
+
// This check might be complex depending on AuthContext's exact init timing.
79
36
80
80
-
// If already authenticated, skip further processing
81
81
-
if (isAuthenticated && session) {
82
82
-
console.log('Already authenticated during callback processing, skipping auth check');
83
83
-
setIsRedirecting(true);
84
84
-
setProcessingComplete(true);
85
85
-
return;
86
86
-
}
37
37
+
// Let's assume AuthContext's init() handles the primary callback logic.
38
38
+
// This component might just need to redirect based on the resulting session state.
87
39
88
88
-
// If we get here, we need to check server authentication
89
89
-
const authResult = await checkAuthStatus();
90
90
-
91
91
-
if (authResult || isAuthenticated) {
92
92
-
console.log('Auth check successful, preparing redirect');
93
93
-
setIsRedirecting(true);
94
94
-
setProcessingComplete(true);
95
95
-
} else {
96
96
-
console.error('Auth check failed, but checking for session one more time');
97
97
-
98
98
-
// Double-check session state before showing error
99
99
-
if (session) {
100
100
-
console.log('Found session after auth check failed, still redirecting');
101
101
-
setIsRedirecting(true);
102
102
-
setProcessingComplete(true);
103
103
-
} else {
104
104
-
setError('Authentication failed. Could not establish a valid session.');
105
105
-
setProcessingComplete(true);
106
106
-
}
107
107
-
}
40
40
+
// Check AuthContext state directly (may require exposing session explicitly)
41
41
+
// Or simply redirect - AuthContext should have set the session if successful
42
42
+
43
43
+
setStatus('Login successful! Redirecting...');
44
44
+
45
45
+
// Attempt to retrieve the original intended URL from state if passed during login
46
46
+
// Note: client.init() doesn't directly return state here. We might need
47
47
+
// AuthContext to store the state temporarily or parse it from the URL hash/query.
48
48
+
// For simplicity, we'll redirect to home. Implement state parsing if needed.
49
49
+
50
50
+
// Retrieve returnUrl from state saved during login (requires state handling)
51
51
+
// const stateParam = new URLSearchParams(window.location.hash.substring(1)).get('state') || new URLSearchParams(window.location.search).get('state');
52
52
+
let returnUrl = '/';
53
53
+
// if (stateParam) {
54
54
+
// try {
55
55
+
// const decodedState = JSON.parse(atob(stateParam)); // Adjust decoding if needed
56
56
+
// returnUrl = decodedState.returnUrl || '/';
57
57
+
// } catch (e) {
58
58
+
// console.error("Error parsing state parameter:", e);
59
59
+
// }
60
60
+
// }
61
61
+
62
62
+
// Redirect after a short delay to show the success message
63
63
+
setTimeout(() => {
64
64
+
navigate(returnUrl);
65
65
+
}, 1500);
66
66
+
108
67
} catch (err) {
109
109
-
console.error('Error handling login callback:', err);
110
110
-
111
111
-
// If we have a session despite the error, still redirect
112
112
-
if (session && isAuthenticated) {
113
113
-
console.log('Error occurred but session exists, proceeding with redirect');
114
114
-
setIsRedirecting(true);
115
115
-
setProcessingComplete(true);
116
116
-
} else {
117
117
-
setError('Failed to complete login process');
118
118
-
setProcessingComplete(true);
119
119
-
}
68
68
+
console.error('(LoginCallback) Error processing callback:', err);
69
69
+
setError(`Login failed: ${err.message || 'Unknown error'}`);
70
70
+
setStatus('');
120
71
}
121
72
};
122
73
123
74
handleCallback();
124
124
-
}, [location, checkAuthStatus, isAuthenticated, session, processingComplete, isRedirecting]);
125
75
126
126
-
// Always prioritize redirecting if we're authenticated
127
127
-
if (isAuthenticated && session) {
128
128
-
console.log(`Redirecting to ${returnUrl} with valid session`);
129
129
-
return <Navigate to={returnUrl} replace />;
130
130
-
}
131
131
-
132
132
-
// Show loading while still processing
133
133
-
if (loading || (!processingComplete && !isRedirecting)) {
134
134
-
return <Loading message="Processing login..." />;
135
135
-
}
136
136
-
137
137
-
// Show error only if we've completed processing, have an error, and don't have a valid session
138
138
-
if (processingComplete && error && (!isAuthenticated || !session)) {
139
139
-
return (
140
140
-
<div className="login-callback">
141
141
-
<div className="error">
142
142
-
<h3>Authentication Error</h3>
143
143
-
<p>{error}</p>
144
144
-
<a href="/login">Return to login</a>
145
145
-
</div>
146
146
-
</div>
147
147
-
);
148
148
-
}
76
76
+
}, [client, navigate]); // Depend on client availability
149
77
150
150
-
// Fallback redirect when processing is complete
151
151
-
return <Navigate to={returnUrl} replace />;
78
78
+
return (
79
79
+
<div className="login-callback-container">
80
80
+
<h2>Authentication Callback</h2>
81
81
+
{status && <p className="status-message">{status}</p>}
82
82
+
{error && <p className="error-message">{error}</p>}
83
83
+
{!status && !error && <p>Verifying authentication...</p>}
84
84
+
{/* Add a button to retry or go home if stuck */}
85
85
+
{(error || (!client && !status && !error)) && (
86
86
+
<button onClick={() => navigate('/')} className="home-button">Go to Homepage</button>
87
87
+
)}
88
88
+
</div>
89
89
+
);
152
90
};
153
91
154
154
-
export default LoginCallback;
92
92
+
export default LoginCallback;
93
93
+
94
94
+
// --- Basic CSS (LoginCallback.css) ---
95
95
+
/*
96
96
+
.login-callback-container {
97
97
+
padding: 40px;
98
98
+
text-align: center;
99
99
+
max-width: 600px;
100
100
+
margin: 40px auto;
101
101
+
background-color: var(--navbar-bg);
102
102
+
border: 1px solid var(--card-border);
103
103
+
border-radius: 8px;
104
104
+
color: var(--text);
105
105
+
}
106
106
+
107
107
+
.status-message {
108
108
+
color: var(--text-muted); // Adjust variable if needed
109
109
+
font-weight: bold;
110
110
+
}
111
111
+
112
112
+
.error-message {
113
113
+
color: var(--error); // Use theme error color
114
114
+
font-weight: bold;
115
115
+
}
116
116
+
117
117
+
.home-button {
118
118
+
margin-top: 20px;
119
119
+
background-color: var(--button-bg);
120
120
+
color: var(--button-text);
121
121
+
border: none;
122
122
+
padding: 10px 20px;
123
123
+
border-radius: 5px;
124
124
+
cursor: pointer;
125
125
+
}
126
126
+
127
127
+
.home-button:hover {
128
128
+
background-color: var(--button-hover-bg); // Adjust variable if needed
129
129
+
}
130
130
+
*/
+77
-427
src/contexts/AuthContext.js
Reviewed
···
30
30
const [session, setSession] = useState(null);
31
31
const [loading, setLoading] = useState(true);
32
32
const [error, setError] = useState(null);
33
33
-
const lastAuthCheck = useRef(0);
34
34
-
const authCheckInProgress = useRef(false);
35
35
-
const didInitialCheck = useRef(false);
33
33
+
const initializing = useRef(false);
36
34
37
37
-
// Initialize the OAuth client
35
35
+
// Updated initializeAuth for BrowserOAuthClient
38
36
useEffect(() => {
39
37
const initializeAuth = async () => {
40
40
-
if (didInitialCheck.current) return;
41
41
-
didInitialCheck.current = true;
42
42
-
38
38
+
if (initializing.current || client) return; // Prevent multiple initializations
39
39
+
initializing.current = true;
40
40
+
setLoading(true);
41
41
+
setError(null);
42
42
+
console.log('(AuthProvider) Initializing BrowserOAuthClient...');
43
43
+
43
44
try {
44
44
-
// First check server-side authentication status
45
45
-
console.log('Checking server authentication status');
46
46
-
const serverAuthResponse = await fetch('/api/auth/status', {
47
47
-
credentials: 'include'
48
48
-
});
49
49
-
50
50
-
if (!serverAuthResponse.ok) {
51
51
-
console.error('Server auth check failed with status:', serverAuthResponse.status);
52
52
-
} else {
53
53
-
const serverAuthData = await serverAuthResponse.json();
54
54
-
console.log('Server auth status:', serverAuthData);
55
55
-
56
56
-
if (serverAuthData.isAuthenticated || serverAuthData.authenticated) {
57
57
-
console.log('Already authenticated on server, setting session');
58
58
-
setSession(serverAuthData.user);
59
59
-
setLoading(false);
60
60
-
return;
61
61
-
}
62
62
-
}
63
63
-
64
64
-
// If not authenticated on the server, check client OAuth
65
65
-
console.log('Not authenticated on server, initializing OAuth client');
45
45
+
// Create the client instance
66
46
const oauthClient = new BrowserOAuthClient({
67
67
-
clientMetadata,
68
68
-
handleResolver: 'https://bsky.social',
47
47
+
clientMetadata: clientMetadata,
48
48
+
handleResolver: 'https://bsky.social', // Use Bluesky's resolver or your own
69
49
});
70
50
71
71
-
try {
72
72
-
// Initialize the client and check for existing sessions
73
73
-
const result = await oauthClient.init();
74
74
-
console.log('OAuth client initialized:', oauthClient);
75
75
-
setClient(oauthClient);
76
76
-
77
77
-
if (result?.session) {
78
78
-
console.log('Found existing OAuth session:', result.session);
79
79
-
80
80
-
// Check if atproto_session exists in localStorage as a backup
81
81
-
const atprotoSession = localStorage.getItem('atproto_session');
82
82
-
console.log('atproto_session in localStorage:', atprotoSession ? 'exists' : 'not found');
83
83
-
84
84
-
// Try to extract handle from session
85
85
-
let handle = result.session.handle;
86
86
-
87
87
-
// If handle is missing, try to extract it from other sources
88
88
-
if (!handle) {
89
89
-
try {
90
90
-
// Try server login data
91
91
-
if (result.session.server && result.session.server.login && result.session.server.login.handle) {
92
92
-
handle = result.session.server.login.handle;
93
93
-
}
94
94
-
// Try atproto_session in localStorage if it exists
95
95
-
else if (atprotoSession) {
96
96
-
try {
97
97
-
const parsedSession = JSON.parse(atprotoSession);
98
98
-
if (parsedSession && parsedSession.handle) {
99
99
-
handle = parsedSession.handle;
100
100
-
}
101
101
-
} catch (e) {
102
102
-
console.error('Error parsing atproto_session:', e);
103
103
-
}
104
104
-
}
105
105
-
} catch (e) {
106
106
-
console.error('Error extracting handle:', e);
107
107
-
}
108
108
-
}
109
109
-
110
110
-
// Format session data for our internal use and sync with server
111
111
-
const sessionData = {
112
112
-
did: result.session.sub,
113
113
-
handle: handle || 'unknown' // Ensure we always have a handle value
114
114
-
};
115
115
-
116
116
-
console.log('Syncing session with server:', sessionData);
117
117
-
118
118
-
// Try to sync with server
119
119
-
try {
120
120
-
const syncResponse = await fetch('/api/sync-session', {
121
121
-
method: 'POST',
122
122
-
headers: {
123
123
-
'Content-Type': 'application/json'
124
124
-
},
125
125
-
body: JSON.stringify(sessionData),
126
126
-
credentials: 'include'
127
127
-
});
128
128
-
129
129
-
if (syncResponse.ok) {
130
130
-
const syncData = await syncResponse.json();
131
131
-
console.log('Initial session sync successful:', syncData);
132
132
-
setSession(syncData.user);
133
133
-
} else {
134
134
-
console.warn('Initial session sync failed:', await syncResponse.text());
135
135
-
136
136
-
// If sync fails, extract what we can from the result.session
137
137
-
const handle = result.session.handle;
138
138
-
139
139
-
// Try to get the handle from other properties if undefined
140
140
-
let fallbackHandle = handle;
141
141
-
if (!fallbackHandle) {
142
142
-
// Check if we can extract handle from other properties
143
143
-
try {
144
144
-
// If we have additional properties in the session that might contain the handle
145
145
-
if (result.session.server && result.session.server.login && result.session.server.login.handle) {
146
146
-
fallbackHandle = result.session.server.login.handle;
147
147
-
} else if (result.session.displayName && result.session.displayName.includes('@')) {
148
148
-
// Sometimes displayName has the handle
149
149
-
fallbackHandle = result.session.displayName.replace('@', '');
150
150
-
} else {
151
151
-
// Fallback to 'unknown' if we can't find a handle
152
152
-
fallbackHandle = 'unknown';
153
153
-
}
154
154
-
} catch (e) {
155
155
-
console.error('Error extracting handle from session:', e);
156
156
-
fallbackHandle = 'unknown';
157
157
-
}
158
158
-
}
159
159
-
160
160
-
// Log what we're doing
161
161
-
console.log(`Using client session as fallback with handle: ${fallbackHandle}`);
162
162
-
163
163
-
// Use client session in our internal format
164
164
-
setSession({
165
165
-
did: result.session.sub,
166
166
-
handle: fallbackHandle,
167
167
-
displayName: result.session.displayName || fallbackHandle
168
168
-
});
169
169
-
}
170
170
-
} catch (syncError) {
171
171
-
console.error('Error syncing initial session:', syncError);
172
172
-
173
173
-
// Extract handle with fallbacks similar to above
174
174
-
const handle = result.session.handle;
175
175
-
let fallbackHandle = handle;
176
176
-
177
177
-
if (!fallbackHandle) {
178
178
-
try {
179
179
-
if (result.session.server && result.session.server.login && result.session.server.login.handle) {
180
180
-
fallbackHandle = result.session.server.login.handle;
181
181
-
} else if (result.session.displayName && result.session.displayName.includes('@')) {
182
182
-
fallbackHandle = result.session.displayName.replace('@', '');
183
183
-
} else {
184
184
-
fallbackHandle = 'unknown';
185
185
-
}
186
186
-
} catch (e) {
187
187
-
console.error('Error extracting handle from session:', e);
188
188
-
fallbackHandle = 'unknown';
189
189
-
}
190
190
-
}
191
191
-
192
192
-
console.log(`Using client session after error with handle: ${fallbackHandle}`);
193
193
-
194
194
-
// If sync fails, still use client session in our internal format
195
195
-
setSession({
196
196
-
did: result.session.sub,
197
197
-
handle: fallbackHandle,
198
198
-
displayName: result.session.displayName || fallbackHandle
199
199
-
});
200
200
-
}
201
201
-
} else {
202
202
-
console.log('No existing OAuth session found');
51
51
+
setClient(oauthClient); // Store the client instance
52
52
+
53
53
+
// Initialize the client - this handles callbacks and session restoration
54
54
+
const initResult = await oauthClient.init();
55
55
+
56
56
+
if (initResult?.session) {
57
57
+
setSession(initResult.session);
58
58
+
console.log(`(AuthProvider) Session ${initResult.state ? 'established via callback' : 'restored'}:`, initResult.session.did);
59
59
+
if (initResult.state) {
60
60
+
console.log('(AuthProvider) Original state from callback:', initResult.state);
61
61
+
// Optionally, redirect based on state if needed, e.g., using navigate
62
62
+
// const returnUrl = initResult.state.returnUrl || '/'; // Example state usage
63
63
+
// window.location.href = returnUrl; // Or use React Router navigate
203
64
}
204
204
-
205
205
-
// Listen for session deletion events
206
206
-
oauthClient.addEventListener('deleted', (event) => {
207
207
-
console.log('Session deletion event received:', event.data);
208
208
-
209
209
-
// Get current session DID at the time of event
210
210
-
const currentSession = session;
211
211
-
const sessionDid = currentSession?.did || currentSession?.sub;
212
212
-
213
213
-
if (event.data.did === sessionDid) {
214
214
-
console.log('Current session was deleted, logging out');
215
215
-
setSession(null);
216
216
-
217
217
-
// Also logout from server
218
218
-
fetch('/api/logout', {
219
219
-
method: 'POST',
220
220
-
credentials: 'include'
221
221
-
}).catch(err => {
222
222
-
console.error('Error during server logout after deletion:', err);
223
223
-
});
224
224
-
}
225
225
-
});
226
226
-
} catch (oauthError) {
227
227
-
console.error('OAuth client initialization error:', oauthError);
65
65
+
} else {
66
66
+
setSession(null);
67
67
+
console.log('(AuthProvider) No active session found or callback processed.');
228
68
}
229
229
-
230
230
-
setLoading(false);
231
69
} catch (err) {
232
232
-
console.error('Auth initialization error:', err);
233
233
-
setError(err.message);
70
70
+
console.error('(AuthProvider) Error initializing client or handling callback:', err);
71
71
+
setError('Initialization failed. Please try refreshing.');
72
72
+
setSession(null);
73
73
+
} finally {
234
74
setLoading(false);
75
75
+
initializing.current = false;
76
76
+
console.log('(AuthProvider) Initialization complete.');
235
77
}
236
78
};
237
79
238
80
initializeAuth();
239
239
-
}, []);
81
81
+
}, [client]); // Dependency on client ensures it runs only once after client is potentially set
240
82
241
241
-
// Initiate the login process
242
242
-
const login = async (handle, returnUrl) => {
243
243
-
if (!client) return;
244
244
-
245
245
-
try {
246
246
-
// Save returnUrl to session storage if provided
247
247
-
if (returnUrl) {
248
248
-
sessionStorage.setItem('returnUrl', returnUrl);
249
249
-
}
250
250
-
251
251
-
// Create state parameter with returnUrl
252
252
-
const state = returnUrl ?
253
253
-
btoa(JSON.stringify({ returnUrl })) :
254
254
-
undefined;
255
255
-
256
256
-
// Pass state parameter to signIn method
257
257
-
await client.signIn(handle, { state });
258
258
-
} catch (err) {
259
259
-
console.error('Login failed:', err);
260
260
-
setError(err.message);
83
83
+
// Updated login function - uses client.signIn()
84
84
+
const login = useCallback(async (handle, returnUrl = '/') => {
85
85
+
if (!client) {
86
86
+
setError("Client not initialized.");
87
87
+
return;
261
88
}
262
262
-
};
263
263
-
264
264
-
// Logout the user
265
265
-
const logout = async () => {
89
89
+
console.log(`(AuthProvider) Initiating client-side login for handle: ${handle || 'none specified'}`);
266
90
try {
267
267
-
console.log('Starting logout process');
268
268
-
269
269
-
// First, try to logout from the server
270
270
-
try {
271
271
-
const response = await fetch('/api/logout', {
272
272
-
method: 'POST',
273
273
-
credentials: 'include'
274
274
-
});
275
275
-
276
276
-
if (response.ok) {
277
277
-
console.log('Server logout successful');
278
278
-
} else {
279
279
-
console.warn('Server logout failed, continuing with client logout');
280
280
-
}
281
281
-
} catch (serverLogoutErr) {
282
282
-
console.error('Error during server logout:', serverLogoutErr);
283
283
-
// Continue with client-side logout even if server logout fails
284
284
-
}
285
285
-
286
286
-
// Clear the session state immediately
287
287
-
setSession(null);
288
288
-
289
289
-
// If we have a client, try to clear its session too
290
290
-
if (client) {
291
291
-
try {
292
292
-
console.log('Attempting to clear OAuth client session');
293
293
-
294
294
-
// Clear OAuth-specific storage items
295
295
-
localStorage.removeItem('atproto_session');
296
296
-
localStorage.removeItem('atproto_state');
297
297
-
localStorage.removeItem('atproto_refresh_token');
298
298
-
299
299
-
// Check if there are any other localStorage items with 'atproto' in the key
300
300
-
Object.keys(localStorage).forEach(key => {
301
301
-
if (key.includes('atproto')) {
302
302
-
console.log(`Removing localStorage item: ${key}`);
303
303
-
localStorage.removeItem(key);
304
304
-
}
305
305
-
});
306
306
-
} catch (clientErr) {
307
307
-
console.error('Error clearing client storage:', clientErr);
308
308
-
}
309
309
-
} else {
310
310
-
console.warn('No OAuth client available for logout');
311
311
-
}
312
312
-
313
313
-
// Force a reload to ensure all state is cleared
314
314
-
console.log('Completing logout - reloading page');
315
315
-
window.location.href = '/';
91
91
+
// The state can be used to pass information through the redirect, like the return URL
92
92
+
const stateData = JSON.stringify({ returnUrl });
93
93
+
// signIn redirects the browser, so code execution stops here if successful
94
94
+
await client.signIn(handle || 'https://bsky.social', { // Use handle or default PDS
95
95
+
state: stateData,
96
96
+
// prompt: 'none', // Uncomment for silent sign-in attempt
97
97
+
});
316
98
} catch (err) {
317
317
-
console.error('Logout process error:', err);
318
318
-
// Still try to reload even if there are errors
319
319
-
window.location.href = '/';
320
320
-
}
321
321
-
};
322
322
-
323
323
-
// Check server-side authentication status with debounce
324
324
-
const checkAuthStatus = useCallback(async () => {
325
325
-
// Avoid concurrent auth checks
326
326
-
if (authCheckInProgress.current) {
327
327
-
return !!session;
328
328
-
}
329
329
-
330
330
-
// Rate limiting - prevent checks more frequently than every 3 seconds
331
331
-
const now = Date.now();
332
332
-
if (now - lastAuthCheck.current < 3000) {
333
333
-
return !!session; // Return current auth state if called too frequently
99
99
+
// This catch might run if the user navigates back or cancels
100
100
+
console.error('(AuthProvider) Error during signIn initiation or cancellation:', err);
101
101
+
setError('Login initiation failed or was cancelled.');
334
102
}
335
335
-
336
336
-
authCheckInProgress.current = true;
337
337
-
lastAuthCheck.current = now;
103
103
+
}, [client]);
338
104
339
339
-
try {
340
340
-
console.log('Checking auth status...');
341
341
-
342
342
-
const controller = new AbortController();
343
343
-
// Set a timeout for the fetch to prevent hanging requests
344
344
-
const timeoutId = setTimeout(() => controller.abort(), 5000);
345
345
-
346
346
-
const response = await fetch('/api/auth/status', {
347
347
-
credentials: 'include',
348
348
-
signal: controller.signal
349
349
-
});
350
350
-
351
351
-
clearTimeout(timeoutId);
352
352
-
353
353
-
if (!response.ok) {
354
354
-
console.error('Auth status check failed with status:', response.status);
355
355
-
// If we get a server error, we should still rely on client-side session
356
356
-
// to prevent users from getting logged out due to temporary server issues
357
357
-
authCheckInProgress.current = false;
358
358
-
return !!session;
359
359
-
}
360
360
-
361
361
-
const data = await response.json();
362
362
-
console.log('Auth status check response:', data);
363
363
-
364
364
-
const isAuthenticated = data.isAuthenticated || data.authenticated;
365
365
-
366
366
-
if (isAuthenticated && data.user) {
367
367
-
// If server session is different from current session, update it
368
368
-
const currentSessionJSON = session ? JSON.stringify(session) : '';
369
369
-
const newSessionJSON = JSON.stringify(data.user);
370
370
-
371
371
-
if (currentSessionJSON !== newSessionJSON) {
372
372
-
console.log('Updating session from server data');
373
373
-
setSession(data.user);
374
374
-
}
375
375
-
376
376
-
authCheckInProgress.current = false;
377
377
-
return true;
378
378
-
} else {
379
379
-
// If server says not authenticated but we have a client session,
380
380
-
// try to synchronize sessions
381
381
-
if (session && client) {
382
382
-
try {
383
383
-
console.log('Server says not authenticated but we have a client session, trying to sync');
384
384
-
385
385
-
// Extract handle from current session
386
386
-
let handle = session.handle;
387
387
-
if (!handle) {
388
388
-
// If our session doesn't have a handle, try to extract from other properties
389
389
-
try {
390
390
-
if (session.displayName && typeof session.displayName === 'string') {
391
391
-
// Sometimes the handle might be in the displayName
392
392
-
handle = session.displayName.includes('@') ?
393
393
-
session.displayName.replace('@', '') :
394
394
-
session.displayName;
395
395
-
}
396
396
-
} catch (e) {
397
397
-
console.error('Error extracting handle from session:', e);
398
398
-
}
399
399
-
}
400
400
-
401
401
-
// Format session data properly
402
402
-
const sessionData = {
403
403
-
did: session.did || session.sub,
404
404
-
handle: handle || 'unknown' // Always provide a handle value
405
405
-
};
406
406
-
407
407
-
console.log('Syncing with data:', sessionData);
408
408
-
409
409
-
// Add a timeout for sync request
410
410
-
const syncController = new AbortController();
411
411
-
const syncTimeoutId = setTimeout(() => syncController.abort(), 5000);
412
412
-
413
413
-
try {
414
414
-
// Try to sync one more time
415
415
-
const syncResponse = await fetch('/api/sync-session', {
416
416
-
method: 'POST',
417
417
-
headers: {
418
418
-
'Content-Type': 'application/json'
419
419
-
},
420
420
-
body: JSON.stringify(sessionData),
421
421
-
credentials: 'include',
422
422
-
signal: syncController.signal
423
423
-
});
424
424
-
425
425
-
clearTimeout(syncTimeoutId);
426
426
-
427
427
-
if (syncResponse.ok) {
428
428
-
console.log('Session sync successful during status check');
429
429
-
const syncData = await syncResponse.json();
430
430
-
setSession(syncData.user);
431
431
-
authCheckInProgress.current = false;
432
432
-
return true;
433
433
-
} else {
434
434
-
console.error('Sync response was not ok:', syncResponse.status);
435
435
-
// If server explicitly rejects our sync attempt, we should clear session
436
436
-
setSession(null);
437
437
-
authCheckInProgress.current = false;
438
438
-
return false;
439
439
-
}
440
440
-
} catch (syncFetchError) {
441
441
-
clearTimeout(syncTimeoutId);
442
442
-
console.error('Network error during sync request:', syncFetchError);
443
443
-
444
444
-
// On network errors, we should keep the current session state to prevent
445
445
-
// users from being logged out due to temporary connectivity issues
446
446
-
authCheckInProgress.current = false;
447
447
-
return !!session;
448
448
-
}
449
449
-
} catch (syncError) {
450
450
-
console.error('Error in sync logic during status check:', syncError);
451
451
-
// If there's an error in our sync logic, keep current session
452
452
-
authCheckInProgress.current = false;
453
453
-
return !!session;
454
454
-
}
455
455
-
}
456
456
-
457
457
-
// If all attempts failed and the server says we're not authenticated
458
458
-
console.log('Server says not authenticated, clearing session');
105
105
+
// Updated Logout function - uses session.signOut()
106
106
+
const logout = useCallback(async () => {
107
107
+
if (!session) return;
108
108
+
console.log('(AuthProvider) Logging out...');
109
109
+
try {
110
110
+
await session.signOut(); // Use session's signOut method
459
111
setSession(null);
460
460
-
authCheckInProgress.current = false;
461
461
-
return false;
462
462
-
}
463
463
-
} catch (err) {
464
464
-
console.error('Error checking auth status:', err);
465
465
-
// For network errors, don't log out the user
466
466
-
if (err.name === 'AbortError') {
467
467
-
console.warn('Auth status check timed out');
468
468
-
} else if (err.name === 'TypeError' && err.message.includes('Network request failed')) {
469
469
-
console.warn('Network request failed during auth check - keeping current session state');
470
470
-
}
471
471
-
472
472
-
authCheckInProgress.current = false;
473
473
-
return !!session; // Fall back to current session state on errors
474
474
-
}
475
475
-
}, [session, client]);
112
112
+
// Optionally clear other app state here
113
113
+
console.log('(AuthProvider) Logout complete.');
114
114
+
// Redirect to home or login page
115
115
+
window.location.href = '/'; // Force reload to ensure clean state
116
116
+
} catch (err) {
117
117
+
console.error('(AuthProvider) Error during logout:', err);
118
118
+
// Still attempt to clear local session on error
119
119
+
setSession(null);
120
120
+
window.location.href = '/'; // Force reload
121
121
+
}
122
122
+
}, [session]);
476
123
477
124
return (
478
478
-
<AuthContext.Provider
479
479
-
value={{
480
480
-
session,
481
481
-
loading,
125
125
+
<AuthContext.Provider
126
126
+
value={{
127
127
+
client, // Export the client instance if needed by components (e.g., LoginCallback)
128
128
+
session,
129
129
+
loading, // Renamed from authLoading for clarity
482
130
error,
483
131
isAuthenticated: !!session,
484
484
-
login,
132
132
+
login,
485
133
logout,
486
486
-
checkAuthStatus
134
134
+
// Remove checkAuthStatus export
487
135
}}
488
136
>
489
137
{children}
···
497
145
if (context === null) {
498
146
throw new Error('useAuth must be used within an AuthProvider');
499
147
}
148
148
+
// Ensure components using the hook get the updated context value
149
149
+
// (React handles this, but good to be mindful)
500
150
return context;
501
151
};