ATProto forum built with ESAV

oauth (public client)

rimar1337 2ff6b79a 0419bd96

+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
··· 6 "": { 7 "name": "forumtest", 8 "dependencies": { 9 - "@atproto/api": "^0.16.0", 10 "@radix-ui/react-dialog": "^1.1.14", 11 "@radix-ui/react-icons": "^1.3.2", 12 "@radix-ui/react-popover": "^1.1.14", 13 "@radix-ui/react-select": "^2.2.5", ··· 26 "devDependencies": { 27 "@testing-library/dom": "^10.4.0", 28 "@testing-library/react": "^16.2.0", 29 "@types/react": "^19.0.8", 30 "@types/react-dom": "^19.0.3", 31 "@vitejs/plugin-react": "^4.3.4", ··· 70 "dev": true, 71 "license": "ISC" 72 }, 73 "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==", 77 "license": "MIT", 78 "dependencies": { 79 "@atproto/common-web": "^0.4.2", ··· 98 "zod": "^3.23.8" 99 } 100 }, 101 "node_modules/@atproto/lexicon": { 102 "version": "0.4.12", 103 "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.12.tgz", ··· 111 "zod": "^3.23.8" 112 } 113 }, 114 "node_modules/@atproto/syntax": { 115 "version": "0.4.0", 116 "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.0.tgz", ··· 1359 } 1360 } 1361 }, 1362 "node_modules/@radix-ui/react-focus-guards": { 1363 "version": "1.1.2", 1364 "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", ··· 1426 } 1427 } 1428 }, 1429 "node_modules/@radix-ui/react-popover": { 1430 "version": "1.1.14", 1431 "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", ··· 1566 } 1567 } 1568 }, 1569 "node_modules/@radix-ui/react-select": { 1570 "version": "2.2.5", 1571 "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", ··· 2745 "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 2746 "license": "MIT" 2747 }, 2748 "node_modules/@types/react": { 2749 "version": "19.1.9", 2750 "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", ··· 3648 "license": "MIT", 3649 "bin": { 3650 "jiti": "lib/jiti-cli.mjs" 3651 } 3652 }, 3653 "node_modules/jotai": { ··· 4801 "dependencies": { 4802 "multiformats": "^9.4.2" 4803 } 4804 }, 4805 "node_modules/unplugin": { 4806 "version": "2.3.5",
··· 6 "": { 7 "name": "forumtest", 8 "dependencies": { 9 + "@atproto/api": "^0.16.2", 10 + "@atproto/oauth-client-browser": "^0.3.27", 11 "@radix-ui/react-dialog": "^1.1.14", 12 + "@radix-ui/react-dropdown-menu": "^2.1.15", 13 "@radix-ui/react-icons": "^1.3.2", 14 "@radix-ui/react-popover": "^1.1.14", 15 "@radix-ui/react-select": "^2.2.5", ··· 28 "devDependencies": { 29 "@testing-library/dom": "^10.4.0", 30 "@testing-library/react": "^16.2.0", 31 + "@types/node": "^24.2.1", 32 "@types/react": "^19.0.8", 33 "@types/react-dom": "^19.0.3", 34 "@vitejs/plugin-react": "^4.3.4", ··· 73 "dev": true, 74 "license": "ISC" 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 + }, 149 "node_modules/@atproto/api": { 150 + "version": "0.16.2", 151 + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.16.2.tgz", 152 + "integrity": "sha512-sSTg31J8ws8DNaoiizp+/uJideRxRaJsq+Nyl8rnSxGw0w3oCvoeRU19iRWh2t0jZEmiRJAGkveGu23NKmPYEQ==", 153 "license": "MIT", 154 "dependencies": { 155 "@atproto/common-web": "^0.4.2", ··· 174 "zod": "^3.23.8" 175 } 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 + }, 217 "node_modules/@atproto/lexicon": { 218 "version": "0.4.12", 219 "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.12.tgz", ··· 227 "zod": "^3.23.8" 228 } 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 + }, 276 "node_modules/@atproto/syntax": { 277 "version": "0.4.0", 278 "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.0.tgz", ··· 1521 } 1522 } 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 + }, 1553 "node_modules/@radix-ui/react-focus-guards": { 1554 "version": "1.1.2", 1555 "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", ··· 1617 } 1618 } 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 + }, 1660 "node_modules/@radix-ui/react-popover": { 1661 "version": "1.1.14", 1662 "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", ··· 1797 } 1798 } 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 + }, 1831 "node_modules/@radix-ui/react-select": { 1832 "version": "2.2.5", 1833 "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", ··· 3007 "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 3008 "license": "MIT" 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 + }, 3020 "node_modules/@types/react": { 3021 "version": "19.1.9", 3022 "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", ··· 3920 "license": "MIT", 3921 "bin": { 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" 3932 } 3933 }, 3934 "node_modules/jotai": { ··· 5082 "dependencies": { 5083 "multiformats": "^9.4.2" 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" 5092 }, 5093 "node_modules/unplugin": { 5094 "version": "2.3.5",
+6 -3
package.json
··· 3 "private": true, 4 "type": "module", 5 "scripts": { 6 - "dev": "vite --port 3000", 7 - "start": "vite --port 3000", 8 "build": "vite build && tsc", 9 "serve": "vite preview", 10 "test": "vitest run" 11 }, 12 "dependencies": { 13 - "@atproto/api": "^0.16.0", 14 "@radix-ui/react-dialog": "^1.1.14", 15 "@radix-ui/react-icons": "^1.3.2", 16 "@radix-ui/react-popover": "^1.1.14", 17 "@radix-ui/react-select": "^2.2.5", ··· 30 "devDependencies": { 31 "@testing-library/dom": "^10.4.0", 32 "@testing-library/react": "^16.2.0", 33 "@types/react": "^19.0.8", 34 "@types/react-dom": "^19.0.3", 35 "@vitejs/plugin-react": "^4.3.4",
··· 3 "private": true, 4 "type": "module", 5 "scripts": { 6 + "dev": "vite --port 3768", 7 + "start": "vite --port 3768", 8 "build": "vite build && tsc", 9 "serve": "vite preview", 10 "test": "vitest run" 11 }, 12 "dependencies": { 13 + "@atproto/api": "^0.16.2", 14 + "@atproto/oauth-client-browser": "^0.3.27", 15 "@radix-ui/react-dialog": "^1.1.14", 16 + "@radix-ui/react-dropdown-menu": "^2.1.15", 17 "@radix-ui/react-icons": "^1.3.2", 18 "@radix-ui/react-popover": "^1.1.14", 19 "@radix-ui/react-select": "^2.2.5", ··· 32 "devDependencies": { 33 "@testing-library/dom": "^10.4.0", 34 "@testing-library/react": "^16.2.0", 35 + "@types/node": "^24.2.1", 36 "@types/react": "^19.0.8", 37 "@types/react-dom": "^19.0.3", 38 "@vitejs/plugin-react": "^4.3.4",
+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
··· 1 import { Link } from "@tanstack/react-router"; 2 - import Login from "./Login"; 3 import { SearchBox } from "./Search"; 4 5 6 7 - export default function Header(){ 8 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 - }
··· 1 import { Link } from "@tanstack/react-router"; 2 + import Login from "./OAuthLogin"; 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"; 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); 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; 24 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 /> 32 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
+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 - import { ErrorComponent, ErrorComponentProps } from '@tanstack/react-router' 2 3 export function PostErrorComponent({ error }: ErrorComponentProps) { 4 return <ErrorComponent error={error} />
··· 1 + import { ErrorComponent, type ErrorComponentProps } from '@tanstack/react-router' 2 3 export function PostErrorComponent({ error }: ErrorComponentProps) { 4 return <ErrorComponent error={error} />
+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
··· 7 8 import "./styles.css"; 9 import reportWebVitals from "./reportWebVitals.ts"; 10 - import { AuthProvider } from "./providers/PassAuthProvider.tsx"; 11 import { PersistentStoreProvider } from "./providers/PersistentStoreProvider.tsx"; 12 import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 13 import { ESAVLiveProvider } from "./esav/ESAVLiveProvider.tsx"; ··· 42 //<StrictMode> 43 <ESAVLiveProvider url={ESAV_WEBSOCKET_URL}> 44 <PersistentStoreProvider> 45 - <AuthProvider> 46 <QueryClientProvider client={queryClient}> 47 {/* Pass the router instance with the context to the provider */} 48 <RouterProvider router={router} /> 49 </QueryClientProvider> 50 - </AuthProvider> 51 </PersistentStoreProvider> 52 </ESAVLiveProvider> 53 //</StrictMode>
··· 7 8 import "./styles.css"; 9 import reportWebVitals from "./reportWebVitals.ts"; 10 + import { OAuthProvider } from "./providers/OAuthProvider.tsx"; 11 import { PersistentStoreProvider } from "./providers/PersistentStoreProvider.tsx"; 12 import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 13 import { ESAVLiveProvider } from "./esav/ESAVLiveProvider.tsx"; ··· 42 //<StrictMode> 43 <ESAVLiveProvider url={ESAV_WEBSOCKET_URL}> 44 <PersistentStoreProvider> 45 + <OAuthProvider> 46 <QueryClientProvider client={queryClient}> 47 {/* Pass the router instance with the context to the provider */} 48 <RouterProvider router={router} /> 49 </QueryClientProvider> 50 + </OAuthProvider> 51 </PersistentStoreProvider> 52 </ESAVLiveProvider> 53 //</StrictMode>
+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
··· 11 import { Route as rootRouteImport } from './routes/__root' 12 import { Route as SearchRouteImport } from './routes/search' 13 import { Route as IndexRouteImport } from './routes/index' 14 import { Route as FForumHandleRouteImport } from './routes/f/$forumHandle' 15 import { Route as FForumHandleIndexRouteImport } from './routes/f/$forumHandle/index' 16 import { Route as FForumHandleTUserHandleTopicRKeyRouteImport } from './routes/f/$forumHandle/t/$userHandle/$topicRKey' ··· 23 const IndexRoute = IndexRouteImport.update({ 24 id: '/', 25 path: '/', 26 getParentRoute: () => rootRouteImport, 27 } as any) 28 const FForumHandleRoute = FForumHandleRouteImport.update({ ··· 46 '/': typeof IndexRoute 47 '/search': typeof SearchRoute 48 '/f/$forumHandle': typeof FForumHandleRouteWithChildren 49 '/f/$forumHandle/': typeof FForumHandleIndexRoute 50 '/f/$forumHandle/t/$userHandle/$topicRKey': typeof FForumHandleTUserHandleTopicRKeyRoute 51 } 52 export interface FileRoutesByTo { 53 '/': typeof IndexRoute 54 '/search': typeof SearchRoute 55 '/f/$forumHandle': typeof FForumHandleIndexRoute 56 '/f/$forumHandle/t/$userHandle/$topicRKey': typeof FForumHandleTUserHandleTopicRKeyRoute 57 } ··· 60 '/': typeof IndexRoute 61 '/search': typeof SearchRoute 62 '/f/$forumHandle': typeof FForumHandleRouteWithChildren 63 '/f/$forumHandle/': typeof FForumHandleIndexRoute 64 '/f/$forumHandle/t/$userHandle/$topicRKey': typeof FForumHandleTUserHandleTopicRKeyRoute 65 } ··· 69 | '/' 70 | '/search' 71 | '/f/$forumHandle' 72 | '/f/$forumHandle/' 73 | '/f/$forumHandle/t/$userHandle/$topicRKey' 74 fileRoutesByTo: FileRoutesByTo 75 to: 76 | '/' 77 | '/search' 78 | '/f/$forumHandle' 79 | '/f/$forumHandle/t/$userHandle/$topicRKey' 80 id: ··· 82 | '/' 83 | '/search' 84 | '/f/$forumHandle' 85 | '/f/$forumHandle/' 86 | '/f/$forumHandle/t/$userHandle/$topicRKey' 87 fileRoutesById: FileRoutesById ··· 90 IndexRoute: typeof IndexRoute 91 SearchRoute: typeof SearchRoute 92 FForumHandleRoute: typeof FForumHandleRouteWithChildren 93 } 94 95 declare module '@tanstack/react-router' { ··· 106 path: '/' 107 fullPath: '/' 108 preLoaderRoute: typeof IndexRouteImport 109 parentRoute: typeof rootRouteImport 110 } 111 '/f/$forumHandle': { ··· 150 IndexRoute: IndexRoute, 151 SearchRoute: SearchRoute, 152 FForumHandleRoute: FForumHandleRouteWithChildren, 153 } 154 export const routeTree = rootRouteImport 155 ._addFileChildren(rootRouteChildren)
··· 11 import { Route as rootRouteImport } from './routes/__root' 12 import { Route as SearchRouteImport } from './routes/search' 13 import { Route as IndexRouteImport } from './routes/index' 14 + import { Route as CallbackIndexRouteImport } from './routes/callback/index' 15 import { Route as FForumHandleRouteImport } from './routes/f/$forumHandle' 16 import { Route as FForumHandleIndexRouteImport } from './routes/f/$forumHandle/index' 17 import { Route as FForumHandleTUserHandleTopicRKeyRouteImport } from './routes/f/$forumHandle/t/$userHandle/$topicRKey' ··· 24 const IndexRoute = IndexRouteImport.update({ 25 id: '/', 26 path: '/', 27 + getParentRoute: () => rootRouteImport, 28 + } as any) 29 + const CallbackIndexRoute = CallbackIndexRouteImport.update({ 30 + id: '/callback/', 31 + path: '/callback/', 32 getParentRoute: () => rootRouteImport, 33 } as any) 34 const FForumHandleRoute = FForumHandleRouteImport.update({ ··· 52 '/': typeof IndexRoute 53 '/search': typeof SearchRoute 54 '/f/$forumHandle': typeof FForumHandleRouteWithChildren 55 + '/callback': typeof CallbackIndexRoute 56 '/f/$forumHandle/': typeof FForumHandleIndexRoute 57 '/f/$forumHandle/t/$userHandle/$topicRKey': typeof FForumHandleTUserHandleTopicRKeyRoute 58 } 59 export interface FileRoutesByTo { 60 '/': typeof IndexRoute 61 '/search': typeof SearchRoute 62 + '/callback': typeof CallbackIndexRoute 63 '/f/$forumHandle': typeof FForumHandleIndexRoute 64 '/f/$forumHandle/t/$userHandle/$topicRKey': typeof FForumHandleTUserHandleTopicRKeyRoute 65 } ··· 68 '/': typeof IndexRoute 69 '/search': typeof SearchRoute 70 '/f/$forumHandle': typeof FForumHandleRouteWithChildren 71 + '/callback/': typeof CallbackIndexRoute 72 '/f/$forumHandle/': typeof FForumHandleIndexRoute 73 '/f/$forumHandle/t/$userHandle/$topicRKey': typeof FForumHandleTUserHandleTopicRKeyRoute 74 } ··· 78 | '/' 79 | '/search' 80 | '/f/$forumHandle' 81 + | '/callback' 82 | '/f/$forumHandle/' 83 | '/f/$forumHandle/t/$userHandle/$topicRKey' 84 fileRoutesByTo: FileRoutesByTo 85 to: 86 | '/' 87 | '/search' 88 + | '/callback' 89 | '/f/$forumHandle' 90 | '/f/$forumHandle/t/$userHandle/$topicRKey' 91 id: ··· 93 | '/' 94 | '/search' 95 | '/f/$forumHandle' 96 + | '/callback/' 97 | '/f/$forumHandle/' 98 | '/f/$forumHandle/t/$userHandle/$topicRKey' 99 fileRoutesById: FileRoutesById ··· 102 IndexRoute: typeof IndexRoute 103 SearchRoute: typeof SearchRoute 104 FForumHandleRoute: typeof FForumHandleRouteWithChildren 105 + CallbackIndexRoute: typeof CallbackIndexRoute 106 } 107 108 declare module '@tanstack/react-router' { ··· 119 path: '/' 120 fullPath: '/' 121 preLoaderRoute: typeof IndexRouteImport 122 + parentRoute: typeof rootRouteImport 123 + } 124 + '/callback/': { 125 + id: '/callback/' 126 + path: '/callback' 127 + fullPath: '/callback' 128 + preLoaderRoute: typeof CallbackIndexRouteImport 129 parentRoute: typeof rootRouteImport 130 } 131 '/f/$forumHandle': { ··· 170 IndexRoute: IndexRoute, 171 SearchRoute: SearchRoute, 172 FForumHandleRoute: FForumHandleRouteWithChildren, 173 + CallbackIndexRoute: CallbackIndexRoute, 174 } 175 export const routeTree = rootRouteImport 176 ._addFileChildren(rootRouteChildren)
+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
··· 13 import * as Select from "@radix-ui/react-select"; 14 import * as Dialog from "@radix-ui/react-dialog"; 15 import { ChevronDownIcon, CheckIcon, Cross2Icon } from "@radix-ui/react-icons"; 16 - import { useAuth } from "@/providers/PassAuthProvider"; 17 import { AtUri, BskyAgent } from "@atproto/api"; 18 import { useQuery, useQueryClient, QueryClient } from "@tanstack/react-query"; 19 import { ··· 215 ); 216 217 const navigate = useNavigate(); 218 - const { agent, loading: authLoading } = useAuth(); 219 220 const queryClient = useQueryClient(); 221
··· 13 import * as Select from "@radix-ui/react-select"; 14 import * as Dialog from "@radix-ui/react-dialog"; 15 import { ChevronDownIcon, CheckIcon, Cross2Icon } from "@radix-ui/react-icons"; 16 + import { useAuth } from "@/providers/OAuthProvider"; 17 import { AtUri, BskyAgent } from "@atproto/api"; 18 import { useQuery, useQueryClient, QueryClient } from "@tanstack/react-query"; 19 import { ··· 215 ); 216 217 const navigate = useNavigate(); 218 + const { agent, status } = useAuth(); 219 + 220 + const authLoading = status === 'loading' 221 222 const queryClient = useQueryClient(); 223
+6 -4
src/routes/f/$forumHandle/t/$userHandle/$topicRKey.tsx
··· 1 import { createFileRoute, Link, useParams } from "@tanstack/react-router"; 2 import { useMemo, useState } from "react"; 3 - import { useAuth } from "@/providers/PassAuthProvider"; 4 import { esavQuery } from "@/helpers/esquery"; 5 import { 6 resolveIdentity, 7 type ResolvedIdentity, 8 } from "@/helpers/cachedidentityresolver"; 9 - import AtpAgent from "@atproto/api"; 10 import { 11 ArrowLeftIcon, 12 ChatBubbleIcon, ··· 345 isCreatingReaction, 346 }: { 347 forumdid: string; 348 - agent: AtpAgent | null; 349 post: PostDoc; 350 //author: AuthorInfo | null; 351 //reactions: ReactionDoc[]; ··· 491 const uri = useMemo(() => { 492 return `at://${op?.did}/party.whey.ft.topic.post/${topicRKey}`; 493 }, [op?.did]); 494 - const { agent, loading: authLoading } = useAuth(); 495 //const topic = useEsavDocument(uri); 496 //const parsed = parseAtUri(uri); 497
··· 1 import { createFileRoute, Link, useParams } from "@tanstack/react-router"; 2 import { useMemo, useState } from "react"; 3 + import { useAuth } from "@/providers/OAuthProvider"; 4 import { esavQuery } from "@/helpers/esquery"; 5 import { 6 resolveIdentity, 7 type ResolvedIdentity, 8 } from "@/helpers/cachedidentityresolver"; 9 + import AtpAgent, { Agent } from "@atproto/api"; 10 import { 11 ArrowLeftIcon, 12 ChatBubbleIcon, ··· 345 isCreatingReaction, 346 }: { 347 forumdid: string; 348 + agent: Agent | null; 349 post: PostDoc; 350 //author: AuthorInfo | null; 351 //reactions: ReactionDoc[]; ··· 491 const uri = useMemo(() => { 492 return `at://${op?.did}/party.whey.ft.topic.post/${topicRKey}`; 493 }, [op?.did]); 494 + const { agent, status } = useAuth(); 495 + const authLoading = status === 'loading' 496 + 497 //const topic = useEsavDocument(uri); 498 //const parsed = parseAtUri(uri); 499
+5 -4
src/routes/search.tsx
··· 5 Link, 6 } from "@tanstack/react-router"; 7 import { useEffect, useState, useCallback, useMemo } from "react"; 8 - import { useAuth } from "@/providers/PassAuthProvider"; 9 import { usePersistentStore } from "@/providers/PersistentStoreProvider"; 10 import { esavQuery } from "@/helpers/esquery"; 11 import { 12 cachedResolveIdentity, 13 type ResolvedIdentity, 14 } from "@/helpers/cachedidentityresolver"; 15 - import AtpAgent, { AtUri } from "@atproto/api"; 16 import { ArrowRightIcon } from "@radix-ui/react-icons"; 17 import { 18 PostCard, ··· 60 }); 61 62 interface SearchResultCardProps { 63 - agent: AtpAgent | null; 64 post: PostDoc; 65 author: AuthorInfo | null; 66 reactions: ReactionDoc[]; ··· 174 export function SearchPage() { 175 const { q } = useSearch({ from: "/search" }); 176 177 - const { agent, loading: authLoading } = useAuth(); 178 const { get, set } = usePersistentStore(); 179 180 const [results, setResults] = useState<PostDoc[]>([]);
··· 5 Link, 6 } from "@tanstack/react-router"; 7 import { useEffect, useState, useCallback, useMemo } from "react"; 8 + import { useAuth } from "@/providers/OAuthProvider"; 9 import { usePersistentStore } from "@/providers/PersistentStoreProvider"; 10 import { esavQuery } from "@/helpers/esquery"; 11 import { 12 cachedResolveIdentity, 13 type ResolvedIdentity, 14 } from "@/helpers/cachedidentityresolver"; 15 + import AtpAgent, { Agent, AtUri } from "@atproto/api"; 16 import { ArrowRightIcon } from "@radix-ui/react-icons"; 17 import { 18 PostCard, ··· 60 }); 61 62 interface SearchResultCardProps { 63 + agent: Agent | null; 64 post: PostDoc; 65 author: AuthorInfo | null; 66 reactions: ReactionDoc[]; ··· 174 export function SearchPage() { 175 const { q } = useSearch({ from: "/search" }); 176 177 + const { agent, status } = useAuth(); 178 + const authLoading = status === 'loading' 179 const { get, set } = usePersistentStore(); 180 181 const [results, setResults] = useState<PostDoc[]>([]);
+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' 5 import tailwindcss from "@tailwindcss/vite"; 6 7 // https://vitejs.dev/config/ 8 export default defineConfig({ 9 - plugins: [tailwindcss(),TanStackRouterVite({ autoCodeSplitting: true }), viteReact()], 10 - test: { 11 - globals: true, 12 - environment: 'jsdom', 13 - }, 14 resolve: { 15 alias: { 16 - '@': resolve(__dirname, './src'), 17 }, 18 }, 19 - })
··· 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 import tailwindcss from "@tailwindcss/vite"; 6 + import { generateMetadataPlugin } from "./oauthdev.mts"; 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 + } 14 // https://vitejs.dev/config/ 15 export default defineConfig({ 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 + // }, 29 resolve: { 30 alias: { 31 + "@": resolve(__dirname, "./src"), 32 }, 33 }, 34 + server: { 35 + allowedHosts: [shp(prodURL),shp(devURL)], 36 + }, 37 + });