pleroma-like client for Bluesky pl.hexmani.ac
bluesky pleroma social-media

Add support for OAuth login

hexmani.ac d3765b7d 60dffc1d

verified
+363 -88
+34 -1
bun.lock
··· 4 4 "": { 5 5 "name": "vite-template-solid", 6 6 "dependencies": { 7 + "@atcute/lexicons": "^1.2.2", 8 + "@atcute/oauth-browser-client": "^1.0.27", 7 9 "@solidjs/router": "^0.15.3", 8 10 "solid-js": "^1.9.5", 9 11 }, 10 12 "devDependencies": { 13 + "@types/bun": "^1.3.0", 11 14 "sass": "^1.81.0", 12 15 "solid-devtools": "^0.34.3", 13 16 "typescript": "^5.7.2", ··· 17 20 }, 18 21 }, 19 22 "packages": { 23 + "@atcute/client": ["@atcute/client@4.0.5", "", { "dependencies": { "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2" } }, "sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA=="], 24 + 25 + "@atcute/identity": ["@atcute/identity@1.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@badrap/valita": "^0.4.6" } }, "sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q=="], 26 + 27 + "@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="], 28 + 29 + "@atcute/multibase": ["@atcute/multibase@1.1.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.5" } }, "sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg=="], 30 + 31 + "@atcute/oauth-browser-client": ["@atcute/oauth-browser-client@1.0.27", "", { "dependencies": { "@atcute/client": "^4.0.4", "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2", "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.5", "nanoid": "^5.1.5" } }, "sha512-Ng1tCOTMLgFHHoIHXTtCZR1/ND62an1qxPX2kBoUzkxxd7iCP7IBYYqOiKyJYT5n1R4zS+s29hFS4t9mxXa5kQ=="], 32 + 33 + "@atcute/uint8array": ["@atcute/uint8array@1.0.5", "", {}, "sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q=="], 34 + 20 35 "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], 21 36 22 37 "@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="], ··· 54 69 "@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="], 55 70 56 71 "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], 72 + 73 + "@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="], 57 74 58 75 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="], 59 76 ··· 219 236 220 237 "@solidjs/router": ["@solidjs/router@0.15.3", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw=="], 221 238 239 + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 240 + 222 241 "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], 223 242 224 243 "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], ··· 227 246 228 247 "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], 229 248 249 + "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], 250 + 230 251 "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 231 252 253 + "@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="], 254 + 255 + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], 256 + 232 257 "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.1", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2", "validate-html-nesting": "^1.2.1" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-b4iHuirqK7RgaMzB2Lsl7MqrlDgQtVRSSazyrmx7wB3T759ggGjod5Rkok5MfHjQXhR7tRPmdwoeGPqBnW2KfA=="], 233 258 234 259 "babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="], ··· 238 263 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 239 264 240 265 "browserslist": ["browserslist@4.26.3", "", { "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w=="], 266 + 267 + "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], 241 268 242 269 "caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="], 243 270 ··· 259 286 260 287 "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 261 288 289 + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], 290 + 262 291 "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 263 292 264 293 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], ··· 293 322 294 323 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 295 324 296 - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 325 + "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], 297 326 298 327 "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], 299 328 ··· 333 362 334 363 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 335 364 365 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 366 + 336 367 "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], 337 368 338 369 "validate-html-nesting": ["validate-html-nesting@1.2.3", "", {}, "sha512-kdkWdCl6eCeLlRShJKbjVOU2kFKxMF8Ghu50n+crEoyx+VKm3FxAxF9z4DCy6+bbTOqNW0+jcIYRnjoIRzigRw=="], ··· 348 379 "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], 349 380 350 381 "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 382 + 383 + "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 351 384 } 352 385 }
+4 -1
package.json
··· 11 11 }, 12 12 "license": "MIT", 13 13 "devDependencies": { 14 + "@types/bun": "^1.3.0", 15 + "sass": "^1.81.0", 14 16 "solid-devtools": "^0.34.3", 15 - "sass": "^1.81.0", 16 17 "typescript": "^5.7.2", 17 18 "vite": "^7.1.4", 18 19 "vite-plugin-solid": "^2.11.8" 19 20 }, 20 21 "dependencies": { 22 + "@atcute/lexicons": "^1.2.2", 23 + "@atcute/oauth-browser-client": "^1.0.27", 21 24 "@solidjs/router": "^0.15.3", 22 25 "solid-js": "^1.9.5" 23 26 }
+29
src/base.tsx
··· 1 + import { RouteSectionProps } from "@solidjs/router"; 2 + import { Component, createSignal, onMount, Show } from "solid-js"; 3 + import { retrieveSession, loginState } from "./components/login"; 4 + import Navbar from "./components/navbar"; 5 + 6 + const Base = (props: RouteSectionProps<unknown>) => { 7 + const [isLoading, setIsLoading] = createSignal(true); 8 + 9 + onMount(async () => { 10 + await retrieveSession(); 11 + if (loginState() && location.pathname === "/") { 12 + window.location.href = "/dash"; 13 + } 14 + setIsLoading(false); 15 + }); 16 + 17 + return ( 18 + <Show when={!isLoading()} fallback={<></>}> 19 + <> 20 + <header> 21 + <Navbar /> 22 + </header> 23 + <main>{props.children}</main> 24 + </> 25 + </Show> 26 + ); 27 + }; 28 + 29 + export default Base;
+153
src/components/login.tsx
··· 1 + import { Did, isHandle } from "@atcute/lexicons/syntax"; 2 + import { 3 + configureOAuth, 4 + createAuthorizationUrl, 5 + deleteStoredSession, 6 + finalizeAuthorization, 7 + getSession, 8 + OAuthUserAgent, 9 + resolveFromIdentity, 10 + resolveFromService, 11 + Session, 12 + } from "@atcute/oauth-browser-client"; 13 + import { Component, createSignal } from "solid-js"; 14 + import Container from "./container"; 15 + 16 + configureOAuth({ 17 + metadata: { 18 + client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, 19 + redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL, 20 + }, 21 + }); 22 + 23 + export const [loginState, setLoginState] = createSignal(false); 24 + let agent: OAuthUserAgent; 25 + 26 + const Login: Component = () => { 27 + const [notice, setNotice] = createSignal(""); 28 + const [loginInput, setLoginInput] = createSignal(""); 29 + 30 + const login = async (handle: string) => { 31 + try { 32 + if (!handle) return; 33 + let resolved; 34 + document.querySelector(".submitInfo")!.removeAttribute("hidden"); 35 + document 36 + .querySelector('button[type="submit"]')! 37 + .setAttribute("disabled", ""); 38 + if (!isHandle(handle)) { 39 + setNotice(`Resolving your service...`); 40 + resolved = await resolveFromService(handle); 41 + } else { 42 + setNotice(`Resolving your identity...`); 43 + resolved = await resolveFromIdentity(handle); 44 + } 45 + 46 + setNotice(`Contacting your data server...`); 47 + const authUrl = await createAuthorizationUrl({ 48 + scope: import.meta.env.VITE_OAUTH_SCOPE, 49 + ...resolved, 50 + }); 51 + 52 + setNotice(`Redirecting...`); 53 + await new Promise((resolve) => setTimeout(resolve, 500)); 54 + 55 + location.assign(authUrl); 56 + } catch (e: unknown) { 57 + if (e instanceof Error) { 58 + console.error(e); 59 + setNotice(`${e.message}`); 60 + } else { 61 + console.error(e); 62 + setNotice(`Unknown error, check console ¯\\_(ツ)_/¯`); 63 + } 64 + } finally { 65 + document 66 + .querySelector('button[type="submit"]')! 67 + .removeAttribute("disabled"); 68 + } 69 + }; 70 + 71 + return ( 72 + <> 73 + <Container 74 + title="Log in" 75 + children={ 76 + <> 77 + <div class="login"> 78 + <form name="login" id="login" onclick={(e) => e.preventDefault()}> 79 + <label for="handle">Handle</label> 80 + <br /> 81 + <input 82 + type="text" 83 + id="handle" 84 + name="handle" 85 + maxlength="255" 86 + placeholder="soykaf.com" 87 + onInput={(e) => setLoginInput(e.currentTarget.value)} 88 + required 89 + /> 90 + <br /> 91 + <button type="submit" onclick={() => login(loginInput())}> 92 + Login 93 + </button> 94 + </form> 95 + <p class="submitInfo" hidden> 96 + {notice()} 97 + </p> 98 + </div> 99 + </> 100 + } 101 + /> 102 + </> 103 + ); 104 + }; 105 + 106 + const retrieveSession = async (): Promise<void> => { 107 + const init = async (): Promise<Session | undefined> => { 108 + const params = new URLSearchParams(location.hash.slice(1)); 109 + 110 + if (params.has("state") && (params.has("code") || params.has("error"))) { 111 + history.replaceState(null, "", location.pathname + location.search); 112 + 113 + const session = await finalizeAuthorization(params); 114 + console.log("Finalizing authorization...", session); 115 + const agent = new OAuthUserAgent(session); 116 + console.log(await agent.getSession()); 117 + const did = session.info.sub; 118 + 119 + localStorage.setItem("currentUser", did); 120 + return session; 121 + } else { 122 + const currentUser = localStorage.getItem("currentUser"); 123 + 124 + if (currentUser) { 125 + try { 126 + console.log("Retrieving session..."); 127 + return await getSession(currentUser as Did); 128 + } catch (err) { 129 + deleteStoredSession(currentUser as Did); 130 + localStorage.removeItem("currentUser"); 131 + throw err; 132 + } 133 + } 134 + } 135 + }; 136 + 137 + const session = await init().catch(() => {}); 138 + 139 + if (session) { 140 + console.log("Retrieved session!", session); 141 + agent = new OAuthUserAgent(session); 142 + setLoginState(true); 143 + } 144 + }; 145 + 146 + const killSession = async (): Promise<void> => { 147 + await agent.signOut(); 148 + setLoginState(false); 149 + localStorage.removeItem("currentUser"); 150 + location.href = "/"; 151 + }; 152 + 153 + export { agent, killSession, Login, retrieveSession };
+7 -13
src/index.tsx
··· 2 2 import { render } from "solid-js/web"; 3 3 import "solid-devtools"; 4 4 import { Route, Router } from "@solidjs/router"; 5 - 6 - import Login from "./routes/login"; 7 - 8 - const root = document.getElementById("root"); 9 - 10 - if (import.meta.env.DEV && !(root instanceof HTMLElement)) { 11 - throw new Error( 12 - "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", 13 - ); 14 - } 5 + import Splash from "./routes/splash"; 6 + import Base from "./base"; 7 + import Dashboard from "./routes/dashboard"; 15 8 16 9 render( 17 10 () => ( 18 - <Router root={Login}> 19 - <Route path="/" component={Login} /> 11 + <Router root={Base}> 12 + <Route path="/" component={Splash} /> 13 + <Route path="/dash" component={Dashboard} /> 20 14 </Router> 21 15 ), 22 - root!, 16 + document.getElementById("root") as HTMLElement, 23 17 );
+16
src/routes/dashboard.tsx
··· 1 + import { killSession, loginState } from "../components/login"; 2 + 3 + const Dashboard = () => { 4 + if (!loginState()) { 5 + location.href = "/"; 6 + } 7 + 8 + return ( 9 + <div> 10 + <h1>Dashboard</h1> 11 + <button onclick={killSession}>Log out</button> 12 + </div> 13 + ); 14 + }; 15 + 16 + export default Dashboard;
-71
src/routes/login.tsx
··· 1 - import { Component } from "solid-js"; 2 - import Navbar from "../components/navbar"; 3 - import "../styles/main.scss"; 4 - import typefaceLogo from "/logo.png?url"; 5 - import blueskyLogo from "/bluesky.svg?url"; 6 - import tangledLogo from "/tangled.svg?url"; 7 - import Container from "../components/container"; 8 - 9 - const Login: Component = () => { 10 - return ( 11 - <> 12 - <header> 13 - <Navbar /> 14 - </header> 15 - <main> 16 - <div id="sidebar"> 17 - <Container 18 - title="Log in" 19 - children={ 20 - <div class="login"> 21 - <form name="login" id="login"> 22 - <label for="handle">Handle</label> 23 - <br /> 24 - <input 25 - type="text" 26 - id="handle" 27 - name="handle" 28 - maxlength="255" 29 - placeholder="soykaf.com" 30 - required 31 - /> 32 - <br /> 33 - <button type="submit">Login</button> 34 - </form> 35 - </div> 36 - } 37 - /> 38 - </div> 39 - <div id="content"> 40 - <Container 41 - title="About" 42 - children={ 43 - <div class="container-content"> 44 - <img class={"typeface"} src={typefaceLogo} /> 45 - <h2>A Bluesky client with a familiar face</h2> 46 - <hr /> 47 - <p> 48 - <b>Bluroma</b> is a web client for Bluesky, built to provide a 49 - customizable power-user experience. Its design is heavily 50 - influenced by the <a href="https://pleroma.social">Pleroma</a>{" "} 51 - and <a href="https://akkoma.social">Akkoma</a> projects, and 52 - intends to provide a similar user interface for Bluesky users. 53 - </p> 54 - <div class="logo-crawl"> 55 - <a href="https://bsky.social/profile/did:plc:5szlrh3xkfxxsuu4mo6oe6h7"> 56 - <img src={blueskyLogo} /> 57 - </a> 58 - <a href="https://tangled.org/@hexmani.ac/bluroma"> 59 - <img src={tangledLogo}></img> 60 - </a> 61 - </div> 62 - </div> 63 - } 64 - /> 65 - </div> 66 - </main> 67 - </> 68 - ); 69 - }; 70 - 71 - export default Login;
+47
src/routes/splash.tsx
··· 1 + import { Component } from "solid-js"; 2 + import Navbar from "../components/navbar"; 3 + import "../styles/main.scss"; 4 + import typefaceLogo from "/logo.png?url"; 5 + import blueskyLogo from "/bluesky.svg?url"; 6 + import tangledLogo from "/tangled.svg?url"; 7 + import Container from "../components/container"; 8 + import { Login } from "../components/login"; 9 + 10 + const Splash: Component = () => { 11 + return ( 12 + <> 13 + <div id="sidebar"> 14 + <Login /> 15 + </div> 16 + <div id="content"> 17 + <Container 18 + title="About" 19 + children={ 20 + <div class="container-content"> 21 + <img class={"typeface"} src={typefaceLogo} /> 22 + <h2>A Bluesky client with a familiar face</h2> 23 + <hr /> 24 + <p> 25 + <b>Bluroma</b> is a web client for Bluesky, built to provide a 26 + customizable power-user experience. Its design is heavily 27 + influenced by the <a href="https://pleroma.social">Pleroma</a>{" "} 28 + and <a href="https://akkoma.social">Akkoma</a> projects, and 29 + intends to provide a similar user interface for Bluesky users. 30 + </p> 31 + <div class="logo-crawl"> 32 + <a href="https://bsky.social/profile/did:plc:5szlrh3xkfxxsuu4mo6oe6h7"> 33 + <img src={blueskyLogo} /> 34 + </a> 35 + <a href="https://tangled.org/@hexmani.ac/bluroma"> 36 + <img src={tangledLogo}></img> 37 + </a> 38 + </div> 39 + </div> 40 + } 41 + /> 42 + </div> 43 + </> 44 + ); 45 + }; 46 + 47 + export default Splash;
+14
src/styles/button.scss
··· 24 24 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 25 25 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; 26 26 } 27 + 28 + button:disabled, 29 + .button:disabled { 30 + color: #666769; 31 + } 32 + 33 + button:focus, 34 + .button:focus { 35 + outline: none; 36 + box-shadow: 37 + 0px 0px 1px 2px rgba(185, 185, 186, 0.4) inset, 38 + 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 39 + 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; 40 + }
+10
src/styles/container.scss
··· 27 27 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 28 28 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset; 29 29 } 30 + 31 + .submitInfo { 32 + background-color: rgba(211, 16, 20, 0.5); 33 + border-radius: vars.$containerBorderRadius; 34 + color: rgba(185, 185, 186, 1); 35 + padding: 0.5rem 1rem; 36 + width: 80%; 37 + text-align: center; 38 + align-self: center; 39 + }
+12
static/oauth/client-metadata.json
··· 1 + { 2 + "client_id": "https://pl.hexmani.ac/oauth/client-metadata.json", 3 + "client_name": "Bluroma", 4 + "client_uri": "https://pl.hexmani.ac", 5 + "redirect_uris": ["https://pl.hexmani.ac/"], 6 + "scope": "atproto transition:generic", 7 + "grant_types": ["authorization_code", "refresh_token"], 8 + "response_types": ["code"], 9 + "token_endpoint_auth_method": "none", 10 + "application_type": "web", 11 + "dpop_bound_access_tokens": true 12 + }
+37 -2
vite.config.ts
··· 1 1 import { defineConfig } from "vite"; 2 2 import solidPlugin from "vite-plugin-solid"; 3 3 import devtools from "solid-devtools/vite"; 4 + import metadata from "./static/oauth/client-metadata.json"; 5 + 6 + const SERVER_HOST = "127.0.0.1"; 7 + const SERVER_PORT = 3000; 4 8 5 9 export default defineConfig({ 6 - plugins: [devtools(), solidPlugin()], 10 + plugins: [ 11 + devtools(), 12 + solidPlugin(), 13 + { 14 + // Shamelessly stolen from PDSls: https://tangled.org/@pdsls.dev/pdsls/blob/main/vite.config.ts 15 + name: "oauth", 16 + config(_conf, { command }) { 17 + if (command === "build") { 18 + process.env.VITE_OAUTH_CLIENT_ID = metadata.client_id; 19 + process.env.VITE_OAUTH_REDIRECT_URL = metadata.redirect_uris[0]; 20 + } else { 21 + const redirectUri = ((): string => { 22 + const url = new URL(metadata.redirect_uris[0]); 23 + return `http://${SERVER_HOST}:${SERVER_PORT}${url.pathname}`; 24 + })(); 25 + 26 + const clientId = 27 + `http://localhost` + 28 + `?redirect_uri=${encodeURIComponent(redirectUri)}` + 29 + `&scope=${encodeURIComponent(metadata.scope)}`; 30 + 31 + process.env.VITE_DEV_SERVER_PORT = "" + SERVER_PORT; 32 + process.env.VITE_OAUTH_CLIENT_ID = clientId; 33 + process.env.VITE_OAUTH_REDIRECT_URL = redirectUri; 34 + } 35 + 36 + process.env.VITE_CLIENT_URI = metadata.client_uri; 37 + process.env.VITE_OAUTH_SCOPE = metadata.scope; 38 + }, 39 + }, 40 + ], 7 41 server: { 8 - port: 3000, 42 + host: SERVER_HOST, 43 + port: SERVER_PORT, 9 44 }, 10 45 root: "./", 11 46 build: {