+48
oauthdev.mts
+48
oauthdev.mts
···
1
+
import fs from 'fs';
2
+
import path from 'path';
3
+
//import { generateClientMetadata } from './src/helpers/oauthClient'
4
+
export const generateClientMetadata = (appOrigin: string) => {
5
+
const callbackPath = '/callback';
6
+
7
+
return {
8
+
"client_id": `${appOrigin}/client-metadata.json`,
9
+
"client_name": "ForumTest",
10
+
"client_uri": appOrigin,
11
+
"logo_uri": `${appOrigin}/logo192.png`,
12
+
"tos_uri": `${appOrigin}/terms-of-service`,
13
+
"policy_uri": `${appOrigin}/privacy-policy`,
14
+
"redirect_uris": [`${appOrigin}${callbackPath}`] as [string, ...string[]],
15
+
"scope": "atproto transition:generic",
16
+
"grant_types": ["authorization_code", "refresh_token"] as ["authorization_code", "refresh_token"],
17
+
"response_types": ["code"] as ["code"],
18
+
"token_endpoint_auth_method": "none" as "none",
19
+
"application_type": "web" as "web",
20
+
"dpop_bound_access_tokens": true
21
+
};
22
+
}
23
+
24
+
25
+
export function generateMetadataPlugin({prod, dev}:{prod: string, dev: string}) {
26
+
return {
27
+
name: 'vite-plugin-generate-metadata',
28
+
config(_config: any, { mode }: any) {
29
+
let appOrigin;
30
+
if (mode === 'production') {
31
+
appOrigin = prod
32
+
if (!appOrigin || !appOrigin.startsWith('https://')) {
33
+
throw new Error('VITE_APP_ORIGIN environment variable must be set to a valid HTTPS URL for production build.');
34
+
}
35
+
} else {
36
+
appOrigin = dev;
37
+
}
38
+
39
+
40
+
const metadata = generateClientMetadata(appOrigin);
41
+
const outputPath = path.resolve(process.cwd(), 'public', 'client-metadata.json');
42
+
43
+
fs.writeFileSync(outputPath, JSON.stringify(metadata, null, 2));
44
+
45
+
console.log(`✅ Generated client-metadata.json for ${appOrigin}`);
46
+
},
47
+
};
48
+
}
+292
-4
package-lock.json
+292
-4
package-lock.json
···
6
6
"": {
7
7
"name": "forumtest",
8
8
"dependencies": {
9
-
"@atproto/api": "^0.16.0",
9
+
"@atproto/api": "^0.16.2",
10
+
"@atproto/oauth-client-browser": "^0.3.27",
10
11
"@radix-ui/react-dialog": "^1.1.14",
12
+
"@radix-ui/react-dropdown-menu": "^2.1.15",
11
13
"@radix-ui/react-icons": "^1.3.2",
12
14
"@radix-ui/react-popover": "^1.1.14",
13
15
"@radix-ui/react-select": "^2.2.5",
···
26
28
"devDependencies": {
27
29
"@testing-library/dom": "^10.4.0",
28
30
"@testing-library/react": "^16.2.0",
31
+
"@types/node": "^24.2.1",
29
32
"@types/react": "^19.0.8",
30
33
"@types/react-dom": "^19.0.3",
31
34
"@vitejs/plugin-react": "^4.3.4",
···
70
73
"dev": true,
71
74
"license": "ISC"
72
75
},
76
+
"node_modules/@atproto-labs/did-resolver": {
77
+
"version": "0.2.0",
78
+
"resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.2.0.tgz",
79
+
"integrity": "sha512-y9GOx2gUETynDKmANnBrU5DTf+u0AwKBJpGns1vDDOYMdLdRCFIeYy3UH+TI8YOkcEazjgF5Q3m+LjwriE1KqQ==",
80
+
"license": "MIT",
81
+
"dependencies": {
82
+
"@atproto-labs/fetch": "0.2.3",
83
+
"@atproto-labs/pipe": "0.1.1",
84
+
"@atproto-labs/simple-store": "0.2.0",
85
+
"@atproto-labs/simple-store-memory": "0.1.3",
86
+
"@atproto/did": "0.1.5",
87
+
"zod": "^3.23.8"
88
+
}
89
+
},
90
+
"node_modules/@atproto-labs/fetch": {
91
+
"version": "0.2.3",
92
+
"resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.2.3.tgz",
93
+
"integrity": "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==",
94
+
"license": "MIT",
95
+
"dependencies": {
96
+
"@atproto-labs/pipe": "0.1.1"
97
+
}
98
+
},
99
+
"node_modules/@atproto-labs/handle-resolver": {
100
+
"version": "0.3.0",
101
+
"resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.3.0.tgz",
102
+
"integrity": "sha512-TREelvXB6P2eHxx6QjINRkBzUZu/aXWrdY9iN57shQe3C8rzsHNEHHuTVvRa33Hc7vFdQbZN0TnCgKveoyiL/A==",
103
+
"license": "MIT",
104
+
"dependencies": {
105
+
"@atproto-labs/simple-store": "0.2.0",
106
+
"@atproto-labs/simple-store-memory": "0.1.3",
107
+
"@atproto/did": "0.1.5",
108
+
"zod": "^3.23.8"
109
+
}
110
+
},
111
+
"node_modules/@atproto-labs/identity-resolver": {
112
+
"version": "0.3.0",
113
+
"resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.3.0.tgz",
114
+
"integrity": "sha512-ZmmRV6m17kIaX4WllYrFIa7d23lNng0fIk6pLyepRGZobQhM5d4wDezICTESAG+RoD0e5fisWs+Tamdvx3mx/Q==",
115
+
"license": "MIT",
116
+
"dependencies": {
117
+
"@atproto-labs/did-resolver": "0.2.0",
118
+
"@atproto-labs/handle-resolver": "0.3.0"
119
+
}
120
+
},
121
+
"node_modules/@atproto-labs/pipe": {
122
+
"version": "0.1.1",
123
+
"resolved": "https://registry.npmjs.org/@atproto-labs/pipe/-/pipe-0.1.1.tgz",
124
+
"integrity": "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==",
125
+
"license": "MIT"
126
+
},
127
+
"node_modules/@atproto-labs/simple-store": {
128
+
"version": "0.2.0",
129
+
"resolved": "https://registry.npmjs.org/@atproto-labs/simple-store/-/simple-store-0.2.0.tgz",
130
+
"integrity": "sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA==",
131
+
"license": "MIT"
132
+
},
133
+
"node_modules/@atproto-labs/simple-store-memory": {
134
+
"version": "0.1.3",
135
+
"resolved": "https://registry.npmjs.org/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.3.tgz",
136
+
"integrity": "sha512-jkitT9+AtU+0b28DoN92iURLaCt/q/q4yX8q6V+9LSwYlUTqKoj/5NFKvF7x6EBuG+gpUdlcycbH7e60gjOhRQ==",
137
+
"license": "MIT",
138
+
"dependencies": {
139
+
"@atproto-labs/simple-store": "0.2.0",
140
+
"lru-cache": "^10.2.0"
141
+
}
142
+
},
143
+
"node_modules/@atproto-labs/simple-store-memory/node_modules/lru-cache": {
144
+
"version": "10.4.3",
145
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
146
+
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
147
+
"license": "ISC"
148
+
},
73
149
"node_modules/@atproto/api": {
74
-
"version": "0.16.0",
75
-
"resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.16.0.tgz",
76
-
"integrity": "sha512-PQHeae6mz/L1YirUslfci7bknfg3RrSZjXpYwzLICxIOvqGKIkOi0+qukC2Py238RhXRo8YZ9dCuole9HQBXDw==",
150
+
"version": "0.16.2",
151
+
"resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.16.2.tgz",
152
+
"integrity": "sha512-sSTg31J8ws8DNaoiizp+/uJideRxRaJsq+Nyl8rnSxGw0w3oCvoeRU19iRWh2t0jZEmiRJAGkveGu23NKmPYEQ==",
77
153
"license": "MIT",
78
154
"dependencies": {
79
155
"@atproto/common-web": "^0.4.2",
···
98
174
"zod": "^3.23.8"
99
175
}
100
176
},
177
+
"node_modules/@atproto/did": {
178
+
"version": "0.1.5",
179
+
"resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.1.5.tgz",
180
+
"integrity": "sha512-8+1D08QdGE5TF0bB0vV8HLVrVZJeLNITpRTUVEoABNMRaUS7CoYSVb0+JNQDeJIVmqMjOL8dOjvCUDkp3gEaGQ==",
181
+
"license": "MIT",
182
+
"dependencies": {
183
+
"zod": "^3.23.8"
184
+
}
185
+
},
186
+
"node_modules/@atproto/jwk": {
187
+
"version": "0.4.0",
188
+
"resolved": "https://registry.npmjs.org/@atproto/jwk/-/jwk-0.4.0.tgz",
189
+
"integrity": "sha512-tvp4iZrzqEzKCeTOKz50/o6WdsZzOuWmWjF6On5QAp04fLwLpsFu2Hixgx/lA1KBO0O4sns7YSGcAqSSX6Rdog==",
190
+
"license": "MIT",
191
+
"dependencies": {
192
+
"multiformats": "^9.9.0",
193
+
"zod": "^3.23.8"
194
+
}
195
+
},
196
+
"node_modules/@atproto/jwk-jose": {
197
+
"version": "0.1.9",
198
+
"resolved": "https://registry.npmjs.org/@atproto/jwk-jose/-/jwk-jose-0.1.9.tgz",
199
+
"integrity": "sha512-HT9GcUe6htDxI5OSYXWdeS6QZ9lpuDDvJk508ppi8a48E/1f8eumoM0QhgbFRF9IKAnnFrtnZDOAvljQzFKwwQ==",
200
+
"license": "MIT",
201
+
"dependencies": {
202
+
"@atproto/jwk": "0.4.0",
203
+
"jose": "^5.2.0"
204
+
}
205
+
},
206
+
"node_modules/@atproto/jwk-webcrypto": {
207
+
"version": "0.1.9",
208
+
"resolved": "https://registry.npmjs.org/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.1.9.tgz",
209
+
"integrity": "sha512-ecciePHT0JEDZNAbMKSkdqoBYsjvhwuVno0jsS600SZmuvi2fAMhGraDZ5ZOO5M0hHHBiDbN7Ar/qcnIwyoxsA==",
210
+
"license": "MIT",
211
+
"dependencies": {
212
+
"@atproto/jwk": "0.4.0",
213
+
"@atproto/jwk-jose": "0.1.9",
214
+
"zod": "^3.23.8"
215
+
}
216
+
},
101
217
"node_modules/@atproto/lexicon": {
102
218
"version": "0.4.12",
103
219
"resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.12.tgz",
···
111
227
"zod": "^3.23.8"
112
228
}
113
229
},
230
+
"node_modules/@atproto/oauth-client": {
231
+
"version": "0.5.1",
232
+
"resolved": "https://registry.npmjs.org/@atproto/oauth-client/-/oauth-client-0.5.1.tgz",
233
+
"integrity": "sha512-wNC9RdfH1LGyZKF+UOmY+z4TFNx1gBur3fx91MCCrNaU0aTHBzgEH9UquL2031J7VNXhBsKJnHfEB5ZYy0AEHQ==",
234
+
"license": "MIT",
235
+
"dependencies": {
236
+
"@atproto-labs/did-resolver": "0.2.0",
237
+
"@atproto-labs/fetch": "0.2.3",
238
+
"@atproto-labs/handle-resolver": "0.3.0",
239
+
"@atproto-labs/identity-resolver": "0.3.0",
240
+
"@atproto-labs/simple-store": "0.2.0",
241
+
"@atproto-labs/simple-store-memory": "0.1.3",
242
+
"@atproto/did": "0.1.5",
243
+
"@atproto/jwk": "0.4.0",
244
+
"@atproto/oauth-types": "0.4.0",
245
+
"@atproto/xrpc": "0.7.1",
246
+
"multiformats": "^9.9.0",
247
+
"zod": "^3.23.8"
248
+
}
249
+
},
250
+
"node_modules/@atproto/oauth-client-browser": {
251
+
"version": "0.3.27",
252
+
"resolved": "https://registry.npmjs.org/@atproto/oauth-client-browser/-/oauth-client-browser-0.3.27.tgz",
253
+
"integrity": "sha512-sUZP27KjlS3qJVPMC+RgWNARQZo7n6CWCXN55+QqLnHTfh+dLCXDS9jMUreXUGMQkVETEogDZ/v0Pb0xHQwBsg==",
254
+
"license": "MIT",
255
+
"dependencies": {
256
+
"@atproto-labs/did-resolver": "0.2.0",
257
+
"@atproto-labs/handle-resolver": "0.3.0",
258
+
"@atproto-labs/simple-store": "0.2.0",
259
+
"@atproto/did": "0.1.5",
260
+
"@atproto/jwk": "0.4.0",
261
+
"@atproto/jwk-webcrypto": "0.1.9",
262
+
"@atproto/oauth-client": "0.5.1",
263
+
"@atproto/oauth-types": "0.4.0"
264
+
}
265
+
},
266
+
"node_modules/@atproto/oauth-types": {
267
+
"version": "0.4.0",
268
+
"resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.4.0.tgz",
269
+
"integrity": "sha512-FrRH9JsPw9H4JxfPDrbrI+pB102tbHTygajfHay7xwz78HPOjSbWPRgWW2hYS4w8vDYdB3PYbBj1jPoKetW7LA==",
270
+
"license": "MIT",
271
+
"dependencies": {
272
+
"@atproto/jwk": "0.4.0",
273
+
"zod": "^3.23.8"
274
+
}
275
+
},
114
276
"node_modules/@atproto/syntax": {
115
277
"version": "0.4.0",
116
278
"resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.0.tgz",
···
1359
1521
}
1360
1522
}
1361
1523
},
1524
+
"node_modules/@radix-ui/react-dropdown-menu": {
1525
+
"version": "2.1.15",
1526
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz",
1527
+
"integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==",
1528
+
"license": "MIT",
1529
+
"dependencies": {
1530
+
"@radix-ui/primitive": "1.1.2",
1531
+
"@radix-ui/react-compose-refs": "1.1.2",
1532
+
"@radix-ui/react-context": "1.1.2",
1533
+
"@radix-ui/react-id": "1.1.1",
1534
+
"@radix-ui/react-menu": "2.1.15",
1535
+
"@radix-ui/react-primitive": "2.1.3",
1536
+
"@radix-ui/react-use-controllable-state": "1.2.2"
1537
+
},
1538
+
"peerDependencies": {
1539
+
"@types/react": "*",
1540
+
"@types/react-dom": "*",
1541
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1542
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1543
+
},
1544
+
"peerDependenciesMeta": {
1545
+
"@types/react": {
1546
+
"optional": true
1547
+
},
1548
+
"@types/react-dom": {
1549
+
"optional": true
1550
+
}
1551
+
}
1552
+
},
1362
1553
"node_modules/@radix-ui/react-focus-guards": {
1363
1554
"version": "1.1.2",
1364
1555
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
···
1426
1617
}
1427
1618
}
1428
1619
},
1620
+
"node_modules/@radix-ui/react-menu": {
1621
+
"version": "2.1.15",
1622
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz",
1623
+
"integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==",
1624
+
"license": "MIT",
1625
+
"dependencies": {
1626
+
"@radix-ui/primitive": "1.1.2",
1627
+
"@radix-ui/react-collection": "1.1.7",
1628
+
"@radix-ui/react-compose-refs": "1.1.2",
1629
+
"@radix-ui/react-context": "1.1.2",
1630
+
"@radix-ui/react-direction": "1.1.1",
1631
+
"@radix-ui/react-dismissable-layer": "1.1.10",
1632
+
"@radix-ui/react-focus-guards": "1.1.2",
1633
+
"@radix-ui/react-focus-scope": "1.1.7",
1634
+
"@radix-ui/react-id": "1.1.1",
1635
+
"@radix-ui/react-popper": "1.2.7",
1636
+
"@radix-ui/react-portal": "1.1.9",
1637
+
"@radix-ui/react-presence": "1.1.4",
1638
+
"@radix-ui/react-primitive": "2.1.3",
1639
+
"@radix-ui/react-roving-focus": "1.1.10",
1640
+
"@radix-ui/react-slot": "1.2.3",
1641
+
"@radix-ui/react-use-callback-ref": "1.1.1",
1642
+
"aria-hidden": "^1.2.4",
1643
+
"react-remove-scroll": "^2.6.3"
1644
+
},
1645
+
"peerDependencies": {
1646
+
"@types/react": "*",
1647
+
"@types/react-dom": "*",
1648
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1649
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1650
+
},
1651
+
"peerDependenciesMeta": {
1652
+
"@types/react": {
1653
+
"optional": true
1654
+
},
1655
+
"@types/react-dom": {
1656
+
"optional": true
1657
+
}
1658
+
}
1659
+
},
1429
1660
"node_modules/@radix-ui/react-popover": {
1430
1661
"version": "1.1.14",
1431
1662
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
···
1566
1797
}
1567
1798
}
1568
1799
},
1800
+
"node_modules/@radix-ui/react-roving-focus": {
1801
+
"version": "1.1.10",
1802
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",
1803
+
"integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==",
1804
+
"license": "MIT",
1805
+
"dependencies": {
1806
+
"@radix-ui/primitive": "1.1.2",
1807
+
"@radix-ui/react-collection": "1.1.7",
1808
+
"@radix-ui/react-compose-refs": "1.1.2",
1809
+
"@radix-ui/react-context": "1.1.2",
1810
+
"@radix-ui/react-direction": "1.1.1",
1811
+
"@radix-ui/react-id": "1.1.1",
1812
+
"@radix-ui/react-primitive": "2.1.3",
1813
+
"@radix-ui/react-use-callback-ref": "1.1.1",
1814
+
"@radix-ui/react-use-controllable-state": "1.2.2"
1815
+
},
1816
+
"peerDependencies": {
1817
+
"@types/react": "*",
1818
+
"@types/react-dom": "*",
1819
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1820
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1821
+
},
1822
+
"peerDependenciesMeta": {
1823
+
"@types/react": {
1824
+
"optional": true
1825
+
},
1826
+
"@types/react-dom": {
1827
+
"optional": true
1828
+
}
1829
+
}
1830
+
},
1569
1831
"node_modules/@radix-ui/react-select": {
1570
1832
"version": "2.2.5",
1571
1833
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz",
···
2745
3007
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
2746
3008
"license": "MIT"
2747
3009
},
3010
+
"node_modules/@types/node": {
3011
+
"version": "24.2.1",
3012
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
3013
+
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
3014
+
"devOptional": true,
3015
+
"license": "MIT",
3016
+
"dependencies": {
3017
+
"undici-types": "~7.10.0"
3018
+
}
3019
+
},
2748
3020
"node_modules/@types/react": {
2749
3021
"version": "19.1.9",
2750
3022
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
···
3648
3920
"license": "MIT",
3649
3921
"bin": {
3650
3922
"jiti": "lib/jiti-cli.mjs"
3923
+
}
3924
+
},
3925
+
"node_modules/jose": {
3926
+
"version": "5.10.0",
3927
+
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
3928
+
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
3929
+
"license": "MIT",
3930
+
"funding": {
3931
+
"url": "https://github.com/sponsors/panva"
3651
3932
}
3652
3933
},
3653
3934
"node_modules/jotai": {
···
4801
5082
"dependencies": {
4802
5083
"multiformats": "^9.4.2"
4803
5084
}
5085
+
},
5086
+
"node_modules/undici-types": {
5087
+
"version": "7.10.0",
5088
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
5089
+
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
5090
+
"devOptional": true,
5091
+
"license": "MIT"
4804
5092
},
4805
5093
"node_modules/unplugin": {
4806
5094
"version": "2.3.5",
+6
-3
package.json
+6
-3
package.json
···
3
3
"private": true,
4
4
"type": "module",
5
5
"scripts": {
6
-
"dev": "vite --port 3000",
7
-
"start": "vite --port 3000",
6
+
"dev": "vite --port 3768",
7
+
"start": "vite --port 3768",
8
8
"build": "vite build && tsc",
9
9
"serve": "vite preview",
10
10
"test": "vitest run"
11
11
},
12
12
"dependencies": {
13
-
"@atproto/api": "^0.16.0",
13
+
"@atproto/api": "^0.16.2",
14
+
"@atproto/oauth-client-browser": "^0.3.27",
14
15
"@radix-ui/react-dialog": "^1.1.14",
16
+
"@radix-ui/react-dropdown-menu": "^2.1.15",
15
17
"@radix-ui/react-icons": "^1.3.2",
16
18
"@radix-ui/react-popover": "^1.1.14",
17
19
"@radix-ui/react-select": "^2.2.5",
···
30
32
"devDependencies": {
31
33
"@testing-library/dom": "^10.4.0",
32
34
"@testing-library/react": "^16.2.0",
35
+
"@types/node": "^24.2.1",
33
36
"@types/react": "^19.0.8",
34
37
"@types/react-dom": "^19.0.3",
35
38
"@vitejs/plugin-react": "^4.3.4",
+22
public/client-metadata.json
+22
public/client-metadata.json
···
1
+
{
2
+
"client_id": "https://forumtest.whey.party/client-metadata.json",
3
+
"client_name": "ForumTest",
4
+
"client_uri": "https://forumtest.whey.party",
5
+
"logo_uri": "https://forumtest.whey.party/logo192.png",
6
+
"tos_uri": "https://forumtest.whey.party/terms-of-service",
7
+
"policy_uri": "https://forumtest.whey.party/privacy-policy",
8
+
"redirect_uris": [
9
+
"https://forumtest.whey.party/callback"
10
+
],
11
+
"scope": "atproto transition:generic",
12
+
"grant_types": [
13
+
"authorization_code",
14
+
"refresh_token"
15
+
],
16
+
"response_types": [
17
+
"code"
18
+
],
19
+
"token_endpoint_auth_method": "none",
20
+
"application_type": "web",
21
+
"dpop_bound_access_tokens": true
22
+
}
+51
-10
src/components/Header.tsx
+51
-10
src/components/Header.tsx
···
1
1
import { Link } from "@tanstack/react-router";
2
-
import Login from "./Login";
2
+
import Login from "./OAuthLogin";
3
3
import { SearchBox } from "./Search";
4
+
import { useCachedProfileJotai } from "@/esav/hooks";
5
+
import { useAuth } from "@/providers/OAuthProvider";
6
+
import {
7
+
DropdownMenu,
8
+
DropdownMenuTrigger,
9
+
DropdownMenuContent,
10
+
DropdownMenuLabel,
11
+
DropdownMenuSeparator,
12
+
DropdownMenuItem,
13
+
} from "@radix-ui/react-dropdown-menu";
4
14
15
+
export default function Header() {
16
+
const { agent, status } = useAuth();
17
+
const did = agent && agent.did && status === "signedIn" ? agent.did : null;
18
+
const [profile, profileloading] = useCachedProfileJotai(did);
5
19
20
+
const avatarUrl =
21
+
profile?.profile.avatar?.ref.$link && profile?.pdsUrl
22
+
? `${profile.pdsUrl}/xrpc/com.atproto.sync.getBlob?did=${profile.did}&cid=${profile?.profile.avatar?.ref.$link}`
23
+
: undefined;
6
24
7
-
export default function Header(){
25
+
return (
26
+
<div className=" flex flex-row h-10 items-center px-2 sticky top-0 bg-gray-700 z-50">
27
+
<Link to="/">
28
+
<span className=" text-gray-50 font-bold">ForumTest</span>
29
+
</Link>
30
+
{/* <div className="spacer flex-1" /> */}
31
+
<SearchBox />
8
32
9
-
10
-
return <div className=" flex flex-row h-10 items-center px-2 sticky top-0 bg-gray-700 z-50">
11
-
<Link to="/"><span className=" text-gray-50 font-bold">ForumTest</span></Link>
12
-
{/* <div className="spacer flex-1" /> */}
13
-
<SearchBox />
14
-
<Login compact />
15
-
</div>
16
-
}
33
+
{profile && !profileloading ? (
34
+
<>
35
+
<DropdownMenu>
36
+
<DropdownMenuTrigger asChild>
37
+
<img
38
+
style={{ height: 28, width: 28, borderRadius: 9999 }}
39
+
src={avatarUrl}
40
+
alt={`Avatar for @${profile?.handle}`}
41
+
/>
42
+
</DropdownMenuTrigger>
43
+
<DropdownMenuContent className="w-48 mt-2 bg-gray-800 p-4 rounded-md" align="end">
44
+
<DropdownMenuLabel className="font-semibold text-gray-50">
45
+
@{profile.handle}
46
+
</DropdownMenuLabel>
47
+
<DropdownMenuSeparator />
48
+
<Login compact />
49
+
</DropdownMenuContent>
50
+
</DropdownMenu>
51
+
</>
52
+
) : (
53
+
<Login compact />
54
+
)}
55
+
</div>
56
+
);
57
+
}
src/components/Login.tsx
src/components/PassLogin.tsx
src/components/Login.tsx
src/components/PassLogin.tsx
+124
src/components/OAuthLogin.tsx
+124
src/components/OAuthLogin.tsx
···
1
+
import React, { useState, useRef, useEffect } from 'react';
2
+
import { useAuth } from '@/providers/OAuthProvider';
3
+
interface AuthButtonProps {
4
+
compact?: boolean;
5
+
}
6
+
7
+
export default function Login({ compact = false }: AuthButtonProps) {
8
+
// 1. Get state and functions from the new OAuth context
9
+
const { status, startLogin, logout } = useAuth();
10
+
11
+
// State for the handle input and the dropdown visibility
12
+
const [handle, setHandle] = useState('');
13
+
const [showLoginForm, setShowLoginForm] = useState(false);
14
+
const formRef = useRef<HTMLDivElement>(null);
15
+
16
+
useEffect(() => {
17
+
// This logic for closing the dropdown on outside click is still useful
18
+
function handleClickOutside(event: MouseEvent) {
19
+
if (formRef.current && !formRef.current.contains(event.target as Node)) {
20
+
setShowLoginForm(false);
21
+
}
22
+
}
23
+
if (showLoginForm) {
24
+
document.addEventListener('mousedown', handleClickOutside);
25
+
}
26
+
return () => {
27
+
document.removeEventListener('mousedown', handleClickOutside);
28
+
};
29
+
}, [showLoginForm]);
30
+
31
+
// Handle the form submission
32
+
const handleLogin = async (e: React.FormEvent) => {
33
+
e.preventDefault();
34
+
if (!handle.trim()) {
35
+
alert('Please enter your handle (e.g., name.example.com)');
36
+
return;
37
+
}
38
+
// This will redirect the user, so no need to manage loading states here
39
+
await startLogin(handle);
40
+
};
41
+
42
+
// Render loading state if the provider is initializing
43
+
if (status === 'loading') {
44
+
return (
45
+
<div className="flex items-center justify-center p-6 text-gray-500 dark:text-gray-400">
46
+
Loading...
47
+
</div>
48
+
);
49
+
}
50
+
51
+
// If logged in, show a logout button
52
+
if (status === 'signedIn') {
53
+
const buttonClass = compact
54
+
? "text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors"
55
+
: "bg-gray-600 hover:bg-gray-700 text-white rounded px-6 py-2 font-semibold text-base transition-colors";
56
+
57
+
const loggedInContent = (
58
+
<button onClick={logout} className={buttonClass}>
59
+
Log out
60
+
</button>
61
+
);
62
+
63
+
if (compact) {
64
+
return loggedInContent;
65
+
}
66
+
67
+
return (
68
+
<div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4">
69
+
<div className="flex flex-col items-center justify-center text-center">
70
+
<p className="text-lg font-semibold mb-6 text-gray-800 dark:text-gray-100">You are logged in!</p>
71
+
{loggedInContent}
72
+
</div>
73
+
</div>
74
+
);
75
+
}
76
+
77
+
// If logged out, show a login button/form
78
+
const loginButtonClass = compact
79
+
? "text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors"
80
+
: "bg-gray-600 hover:bg-gray-700 text-white rounded px-6 py-2 font-semibold text-base transition-colors mt-2";
81
+
82
+
const loginForm = (
83
+
<form onSubmit={handleLogin} className={`flex flex-col gap-${compact ? '3' : '4'}`}>
84
+
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
85
+
Login with your AT Protocol (Bluesky) handle
86
+
</p>
87
+
<input
88
+
type="text"
89
+
placeholder="name.example.com"
90
+
value={handle}
91
+
onChange={e => setHandle(e.target.value)}
92
+
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
93
+
autoComplete="webauthn" // Hint for password managers
94
+
/>
95
+
<button type="submit" className={loginButtonClass}>
96
+
Sign In
97
+
</button>
98
+
</form>
99
+
);
100
+
101
+
if (compact) {
102
+
return (
103
+
<div className="relative" ref={formRef}>
104
+
<button
105
+
onClick={() => setShowLoginForm(!showLoginForm)}
106
+
className={loginButtonClass}
107
+
>
108
+
Log in
109
+
</button>
110
+
{showLoginForm && (
111
+
<div className="absolute top-full right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50">
112
+
{loginForm}
113
+
</div>
114
+
)}
115
+
</div>
116
+
);
117
+
}
118
+
119
+
return (
120
+
<div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4">
121
+
{loginForm}
122
+
</div>
123
+
);
124
+
}
+1
-1
src/components/PostError.tsx
+1
-1
src/components/PostError.tsx
+38
src/helpers/oauthClient.ts
+38
src/helpers/oauthClient.ts
···
1
+
import { BrowserOAuthClient, type ClientMetadata } from '@atproto/oauth-client-browser';
2
+
3
+
// const domain = location.origin ? location.origin : ((import.meta?.env?.DEV) ? 'https://local3768forumtest.whey.party' : 'https://forumtest.whey.party')
4
+
const handleResolverPDS = 'https://pds-nd.whey.party'
5
+
6
+
// export const generateClientMetadata = (appOrigin: string) => {
7
+
// const callbackPath = '/callback';
8
+
9
+
// return {
10
+
// "client_id": `${appOrigin}/client-metadata.json`,
11
+
// "client_name": "ForumTest",
12
+
// "client_uri": appOrigin,
13
+
// "logo_uri": `${appOrigin}/logo192.png`,
14
+
// "tos_uri": `${appOrigin}/terms-of-service`,
15
+
// "policy_uri": `${appOrigin}/privacy-policy`,
16
+
// "redirect_uris": [`${appOrigin}${callbackPath}`] as [string, ...string[]],
17
+
// "scope": "atproto transition:generic",
18
+
// "grant_types": ["authorization_code", "refresh_token"] as ["authorization_code", "refresh_token"],
19
+
// "response_types": ["code"] as ["code"],
20
+
// "token_endpoint_auth_method": "none" as "none",
21
+
// "application_type": "web" as "web",
22
+
// "dpop_bound_access_tokens": true
23
+
// };
24
+
// }
25
+
26
+
// IF ERROR: you need to build it first, either npm run dev or npm run build
27
+
import clientMetadata from '../../public/client-metadata.json' assert { type: 'json' };
28
+
29
+
// async function loadClientMetadata(): Promise<ClientMetadata> {
30
+
// const res = await fetch('/client-metadata.json');
31
+
// if (!res.ok) throw new Error('Failed to load client metadata');
32
+
// return res.json();
33
+
// }
34
+
35
+
export const oauthClient = new BrowserOAuthClient({
36
+
clientMetadata: clientMetadata as ClientMetadata,
37
+
handleResolver: handleResolverPDS,
38
+
});
+3
-3
src/main.tsx
+3
-3
src/main.tsx
···
7
7
8
8
import "./styles.css";
9
9
import reportWebVitals from "./reportWebVitals.ts";
10
-
import { AuthProvider } from "./providers/PassAuthProvider.tsx";
10
+
import { OAuthProvider } from "./providers/OAuthProvider.tsx";
11
11
import { PersistentStoreProvider } from "./providers/PersistentStoreProvider.tsx";
12
12
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
13
13
import { ESAVLiveProvider } from "./esav/ESAVLiveProvider.tsx";
···
42
42
//<StrictMode>
43
43
<ESAVLiveProvider url={ESAV_WEBSOCKET_URL}>
44
44
<PersistentStoreProvider>
45
-
<AuthProvider>
45
+
<OAuthProvider>
46
46
<QueryClientProvider client={queryClient}>
47
47
{/* Pass the router instance with the context to the provider */}
48
48
<RouterProvider router={router} />
49
49
</QueryClientProvider>
50
-
</AuthProvider>
50
+
</OAuthProvider>
51
51
</PersistentStoreProvider>
52
52
</ESAVLiveProvider>
53
53
//</StrictMode>
+98
src/providers/OAuthProvider.tsx
+98
src/providers/OAuthProvider.tsx
···
1
+
import React, { createContext, useState, useEffect, useContext, useCallback } from 'react';
2
+
import { Agent } from '@atproto/api';
3
+
import { oauthClient } from '../helpers/oauthClient';
4
+
import { type OAuthSession, TokenInvalidError, TokenRefreshError, TokenRevokedError } from '@atproto/oauth-client-browser';
5
+
6
+
type Session = OAuthSession;
7
+
8
+
interface AuthContextValue {
9
+
agent: Agent | null;
10
+
session: Session | null;
11
+
status: 'loading' | 'signedIn' | 'signedOut';
12
+
startLogin: (handle: string) => Promise<void>;
13
+
logout: () => Promise<void>;
14
+
}
15
+
16
+
const AuthContext = createContext<AuthContextValue>({} as AuthContextValue);
17
+
18
+
export const OAuthProvider = ({ children }: { children: React.ReactNode }) => {
19
+
const [agent, setAgent] = useState<Agent | null>(null);
20
+
const [session, setSession] = useState<Session | null>(null);
21
+
const [status, setStatus] = useState<'loading' | 'signedIn' | 'signedOut'>('loading');
22
+
23
+
useEffect(() => {
24
+
const initialize = async () => {
25
+
try {
26
+
const result = await oauthClient.init();
27
+
28
+
if (result) {
29
+
const { session: oauthSession } = result;
30
+
const apiAgent = new Agent(oauthSession);
31
+
32
+
setAgent(apiAgent);
33
+
setSession(oauthSession);
34
+
setStatus('signedIn');
35
+
if ('state' in result && result.state) {
36
+
console.log(`Successfully authenticated ${oauthSession.sub} (state: ${result.state})`);
37
+
} else {
38
+
console.log(`Session for ${oauthSession.sub} was restored`);
39
+
}
40
+
} else {
41
+
setStatus('signedOut');
42
+
console.log('No active session found.');
43
+
}
44
+
} catch (e) {
45
+
console.error('Auth initialization failed:', e);
46
+
setStatus('signedOut');
47
+
}
48
+
};
49
+
50
+
const handleSessionDeleted = (
51
+
event: CustomEvent<{ sub: string; cause: TokenRefreshError | TokenRevokedError | TokenInvalidError }>
52
+
) => {
53
+
console.error(`Session for ${event.detail.sub} was deleted. Logging out.`, event.detail.cause);
54
+
setAgent(null);
55
+
setSession(null);
56
+
setStatus('signedOut');
57
+
};
58
+
59
+
oauthClient.addEventListener('deleted', handleSessionDeleted as EventListener);
60
+
initialize();
61
+
62
+
return () => {
63
+
oauthClient.removeEventListener('deleted', handleSessionDeleted as EventListener);
64
+
};
65
+
}, []);
66
+
67
+
const startLogin = useCallback(async (handleOrPdsUrl: string) => {
68
+
if (status !== 'signedOut') return;
69
+
try {
70
+
await oauthClient.signIn(handleOrPdsUrl);
71
+
} catch (err) {
72
+
console.error('Sign-in process aborted or failed:', err);
73
+
}
74
+
}, [status]);
75
+
76
+
const logout = useCallback(async () => {
77
+
if (!session) return;
78
+
setStatus('loading');
79
+
try {
80
+
await oauthClient.revoke(session.sub);
81
+
console.log('Successfully logged out.');
82
+
} catch (e) {
83
+
console.error("Logout failed:", e);
84
+
} finally {
85
+
setAgent(null);
86
+
setSession(null);
87
+
setStatus('signedOut');
88
+
}
89
+
}, [session]);
90
+
91
+
return (
92
+
<AuthContext.Provider value={{ agent, session, status, startLogin, logout }}>
93
+
{children}
94
+
</AuthContext.Provider>
95
+
);
96
+
};
97
+
98
+
export const useAuth = () => useContext(AuthContext);
+21
src/routeTree.gen.ts
+21
src/routeTree.gen.ts
···
11
11
import { Route as rootRouteImport } from './routes/__root'
12
12
import { Route as SearchRouteImport } from './routes/search'
13
13
import { Route as IndexRouteImport } from './routes/index'
14
+
import { Route as CallbackIndexRouteImport } from './routes/callback/index'
14
15
import { Route as FForumHandleRouteImport } from './routes/f/$forumHandle'
15
16
import { Route as FForumHandleIndexRouteImport } from './routes/f/$forumHandle/index'
16
17
import { Route as FForumHandleTUserHandleTopicRKeyRouteImport } from './routes/f/$forumHandle/t/$userHandle/$topicRKey'
···
23
24
const IndexRoute = IndexRouteImport.update({
24
25
id: '/',
25
26
path: '/',
27
+
getParentRoute: () => rootRouteImport,
28
+
} as any)
29
+
const CallbackIndexRoute = CallbackIndexRouteImport.update({
30
+
id: '/callback/',
31
+
path: '/callback/',
26
32
getParentRoute: () => rootRouteImport,
27
33
} as any)
28
34
const FForumHandleRoute = FForumHandleRouteImport.update({
···
46
52
'/': typeof IndexRoute
47
53
'/search': typeof SearchRoute
48
54
'/f/$forumHandle': typeof FForumHandleRouteWithChildren
55
+
'/callback': typeof CallbackIndexRoute
49
56
'/f/$forumHandle/': typeof FForumHandleIndexRoute
50
57
'/f/$forumHandle/t/$userHandle/$topicRKey': typeof FForumHandleTUserHandleTopicRKeyRoute
51
58
}
52
59
export interface FileRoutesByTo {
53
60
'/': typeof IndexRoute
54
61
'/search': typeof SearchRoute
62
+
'/callback': typeof CallbackIndexRoute
55
63
'/f/$forumHandle': typeof FForumHandleIndexRoute
56
64
'/f/$forumHandle/t/$userHandle/$topicRKey': typeof FForumHandleTUserHandleTopicRKeyRoute
57
65
}
···
60
68
'/': typeof IndexRoute
61
69
'/search': typeof SearchRoute
62
70
'/f/$forumHandle': typeof FForumHandleRouteWithChildren
71
+
'/callback/': typeof CallbackIndexRoute
63
72
'/f/$forumHandle/': typeof FForumHandleIndexRoute
64
73
'/f/$forumHandle/t/$userHandle/$topicRKey': typeof FForumHandleTUserHandleTopicRKeyRoute
65
74
}
···
69
78
| '/'
70
79
| '/search'
71
80
| '/f/$forumHandle'
81
+
| '/callback'
72
82
| '/f/$forumHandle/'
73
83
| '/f/$forumHandle/t/$userHandle/$topicRKey'
74
84
fileRoutesByTo: FileRoutesByTo
75
85
to:
76
86
| '/'
77
87
| '/search'
88
+
| '/callback'
78
89
| '/f/$forumHandle'
79
90
| '/f/$forumHandle/t/$userHandle/$topicRKey'
80
91
id:
···
82
93
| '/'
83
94
| '/search'
84
95
| '/f/$forumHandle'
96
+
| '/callback/'
85
97
| '/f/$forumHandle/'
86
98
| '/f/$forumHandle/t/$userHandle/$topicRKey'
87
99
fileRoutesById: FileRoutesById
···
90
102
IndexRoute: typeof IndexRoute
91
103
SearchRoute: typeof SearchRoute
92
104
FForumHandleRoute: typeof FForumHandleRouteWithChildren
105
+
CallbackIndexRoute: typeof CallbackIndexRoute
93
106
}
94
107
95
108
declare module '@tanstack/react-router' {
···
106
119
path: '/'
107
120
fullPath: '/'
108
121
preLoaderRoute: typeof IndexRouteImport
122
+
parentRoute: typeof rootRouteImport
123
+
}
124
+
'/callback/': {
125
+
id: '/callback/'
126
+
path: '/callback'
127
+
fullPath: '/callback'
128
+
preLoaderRoute: typeof CallbackIndexRouteImport
109
129
parentRoute: typeof rootRouteImport
110
130
}
111
131
'/f/$forumHandle': {
···
150
170
IndexRoute: IndexRoute,
151
171
SearchRoute: SearchRoute,
152
172
FForumHandleRoute: FForumHandleRouteWithChildren,
173
+
CallbackIndexRoute: CallbackIndexRoute,
153
174
}
154
175
export const routeTree = rootRouteImport
155
176
._addFileChildren(rootRouteChildren)
+11
src/routes/callback/index.tsx
+11
src/routes/callback/index.tsx
···
1
+
import { createFileRoute, useNavigate } from '@tanstack/react-router'
2
+
3
+
export const Route = createFileRoute('/callback/')({
4
+
component: RouteComponent,
5
+
})
6
+
7
+
function RouteComponent() {
8
+
const navigate = useNavigate()
9
+
navigate({to:"/"})
10
+
return <div>Hello "/callback/"!</div>
11
+
}
+4
-2
src/routes/f/$forumHandle/index.tsx
+4
-2
src/routes/f/$forumHandle/index.tsx
···
13
13
import * as Select from "@radix-ui/react-select";
14
14
import * as Dialog from "@radix-ui/react-dialog";
15
15
import { ChevronDownIcon, CheckIcon, Cross2Icon } from "@radix-ui/react-icons";
16
-
import { useAuth } from "@/providers/PassAuthProvider";
16
+
import { useAuth } from "@/providers/OAuthProvider";
17
17
import { AtUri, BskyAgent } from "@atproto/api";
18
18
import { useQuery, useQueryClient, QueryClient } from "@tanstack/react-query";
19
19
import {
···
215
215
);
216
216
217
217
const navigate = useNavigate();
218
-
const { agent, loading: authLoading } = useAuth();
218
+
const { agent, status } = useAuth();
219
+
220
+
const authLoading = status === 'loading'
219
221
220
222
const queryClient = useQueryClient();
221
223
+6
-4
src/routes/f/$forumHandle/t/$userHandle/$topicRKey.tsx
+6
-4
src/routes/f/$forumHandle/t/$userHandle/$topicRKey.tsx
···
1
1
import { createFileRoute, Link, useParams } from "@tanstack/react-router";
2
2
import { useMemo, useState } from "react";
3
-
import { useAuth } from "@/providers/PassAuthProvider";
3
+
import { useAuth } from "@/providers/OAuthProvider";
4
4
import { esavQuery } from "@/helpers/esquery";
5
5
import {
6
6
resolveIdentity,
7
7
type ResolvedIdentity,
8
8
} from "@/helpers/cachedidentityresolver";
9
-
import AtpAgent from "@atproto/api";
9
+
import AtpAgent, { Agent } from "@atproto/api";
10
10
import {
11
11
ArrowLeftIcon,
12
12
ChatBubbleIcon,
···
345
345
isCreatingReaction,
346
346
}: {
347
347
forumdid: string;
348
-
agent: AtpAgent | null;
348
+
agent: Agent | null;
349
349
post: PostDoc;
350
350
//author: AuthorInfo | null;
351
351
//reactions: ReactionDoc[];
···
491
491
const uri = useMemo(() => {
492
492
return `at://${op?.did}/party.whey.ft.topic.post/${topicRKey}`;
493
493
}, [op?.did]);
494
-
const { agent, loading: authLoading } = useAuth();
494
+
const { agent, status } = useAuth();
495
+
const authLoading = status === 'loading'
496
+
495
497
//const topic = useEsavDocument(uri);
496
498
//const parsed = parseAtUri(uri);
497
499
+5
-4
src/routes/search.tsx
+5
-4
src/routes/search.tsx
···
5
5
Link,
6
6
} from "@tanstack/react-router";
7
7
import { useEffect, useState, useCallback, useMemo } from "react";
8
-
import { useAuth } from "@/providers/PassAuthProvider";
8
+
import { useAuth } from "@/providers/OAuthProvider";
9
9
import { usePersistentStore } from "@/providers/PersistentStoreProvider";
10
10
import { esavQuery } from "@/helpers/esquery";
11
11
import {
12
12
cachedResolveIdentity,
13
13
type ResolvedIdentity,
14
14
} from "@/helpers/cachedidentityresolver";
15
-
import AtpAgent, { AtUri } from "@atproto/api";
15
+
import AtpAgent, { Agent, AtUri } from "@atproto/api";
16
16
import { ArrowRightIcon } from "@radix-ui/react-icons";
17
17
import {
18
18
PostCard,
···
60
60
});
61
61
62
62
interface SearchResultCardProps {
63
-
agent: AtpAgent | null;
63
+
agent: Agent | null;
64
64
post: PostDoc;
65
65
author: AuthorInfo | null;
66
66
reactions: ReactionDoc[];
···
174
174
export function SearchPage() {
175
175
const { q } = useSearch({ from: "/search" });
176
176
177
-
const { agent, loading: authLoading } = useAuth();
177
+
const { agent, status } = useAuth();
178
+
const authLoading = status === 'loading'
178
179
const { get, set } = usePersistentStore();
179
180
180
181
const [results, setResults] = useState<PostDoc[]>([]);
+29
-11
vite.config.ts
+29
-11
vite.config.ts
···
1
-
import { defineConfig } from 'vite'
2
-
import viteReact from '@vitejs/plugin-react'
3
-
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
4
-
import { resolve } from 'node:path'
1
+
import { defineConfig } from "vite";
2
+
import viteReact from "@vitejs/plugin-react";
3
+
import { tanstackRouter } from "@tanstack/router-plugin/vite";
4
+
import { resolve } from "node:path";
5
5
import tailwindcss from "@tailwindcss/vite";
6
+
import { generateMetadataPlugin } from "./oauthdev.mts";
6
7
8
+
const prodURL = "https://forumtest.whey.party"
9
+
const devURL = "https://local3768forumtest.whey.party"
10
+
11
+
function shp(url: string): string {
12
+
return url.replace(/^https?:\/\//, '');
13
+
}
7
14
// https://vitejs.dev/config/
8
15
export default defineConfig({
9
-
plugins: [tailwindcss(),TanStackRouterVite({ autoCodeSplitting: true }), viteReact()],
10
-
test: {
11
-
globals: true,
12
-
environment: 'jsdom',
13
-
},
16
+
plugins: [
17
+
generateMetadataPlugin({
18
+
prod: prodURL,
19
+
dev: devURL,
20
+
}),
21
+
tailwindcss(),
22
+
tanstackRouter({ autoCodeSplitting: true }),
23
+
viteReact(),
24
+
],
25
+
// test: {
26
+
// globals: true,
27
+
// environment: 'jsdom',
28
+
// },
14
29
resolve: {
15
30
alias: {
16
-
'@': resolve(__dirname, './src'),
31
+
"@": resolve(__dirname, "./src"),
17
32
},
18
33
},
19
-
})
34
+
server: {
35
+
allowedHosts: [shp(prodURL),shp(devURL)],
36
+
},
37
+
});