alpha
Login
or
Join now
hexmani.ac
/
bluroma
Star
2
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
(attempted) pleroma clone for bluesky
pl.hexmani.ac
bluesky
pleroma
social-media
Star
2
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
Add support for OAuth login
author
penny
date
9 months ago
(Oct 21, 2025, 9:46 AM -0400)
commit
d3765b7d
d3765b7def6f752b03efae343b50fda1f21b3ffb
parent
60dffc1d
60dffc1d5e528c44275d2be75b7d7f2b8f22ef1f
+363
-88
12 changed files
Expand all
Collapse all
Unified
Split
bun.lock
package.json
src
base.tsx
components
login.tsx
index.tsx
routes
dashboard.tsx
login.tsx
splash.tsx
styles
button.scss
container.scss
static
oauth
client-metadata.json
vite.config.ts
+34
-1
bun.lock
View file
Reviewed
···
4
4
"": {
5
5
"name": "vite-template-solid",
6
6
"dependencies": {
7
7
+
"@atcute/lexicons": "^1.2.2",
8
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
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
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
24
+
25
25
+
"@atcute/identity": ["@atcute/identity@1.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@badrap/valita": "^0.4.6" } }, "sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q=="],
26
26
+
27
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
28
+
29
29
+
"@atcute/multibase": ["@atcute/multibase@1.1.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.5" } }, "sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg=="],
30
30
+
31
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
32
+
33
33
+
"@atcute/uint8array": ["@atcute/uint8array@1.0.5", "", {}, "sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q=="],
34
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
72
+
73
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
239
+
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
240
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
249
+
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
250
250
+
230
251
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
231
252
253
253
+
"@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
254
254
+
255
255
+
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
256
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
266
+
267
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
289
+
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
290
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
296
-
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
325
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
365
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
366
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
382
+
383
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
View file
Reviewed
···
11
11
},
12
12
"license": "MIT",
13
13
"devDependencies": {
14
14
+
"@types/bun": "^1.3.0",
15
15
+
"sass": "^1.81.0",
14
16
"solid-devtools": "^0.34.3",
15
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
22
+
"@atcute/lexicons": "^1.2.2",
23
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
View file
Reviewed
···
1
1
+
import { RouteSectionProps } from "@solidjs/router";
2
2
+
import { Component, createSignal, onMount, Show } from "solid-js";
3
3
+
import { retrieveSession, loginState } from "./components/login";
4
4
+
import Navbar from "./components/navbar";
5
5
+
6
6
+
const Base = (props: RouteSectionProps<unknown>) => {
7
7
+
const [isLoading, setIsLoading] = createSignal(true);
8
8
+
9
9
+
onMount(async () => {
10
10
+
await retrieveSession();
11
11
+
if (loginState() && location.pathname === "/") {
12
12
+
window.location.href = "/dash";
13
13
+
}
14
14
+
setIsLoading(false);
15
15
+
});
16
16
+
17
17
+
return (
18
18
+
<Show when={!isLoading()} fallback={<></>}>
19
19
+
<>
20
20
+
<header>
21
21
+
<Navbar />
22
22
+
</header>
23
23
+
<main>{props.children}</main>
24
24
+
</>
25
25
+
</Show>
26
26
+
);
27
27
+
};
28
28
+
29
29
+
export default Base;
+153
src/components/login.tsx
View file
Reviewed
···
1
1
+
import { Did, isHandle } from "@atcute/lexicons/syntax";
2
2
+
import {
3
3
+
configureOAuth,
4
4
+
createAuthorizationUrl,
5
5
+
deleteStoredSession,
6
6
+
finalizeAuthorization,
7
7
+
getSession,
8
8
+
OAuthUserAgent,
9
9
+
resolveFromIdentity,
10
10
+
resolveFromService,
11
11
+
Session,
12
12
+
} from "@atcute/oauth-browser-client";
13
13
+
import { Component, createSignal } from "solid-js";
14
14
+
import Container from "./container";
15
15
+
16
16
+
configureOAuth({
17
17
+
metadata: {
18
18
+
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
19
19
+
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL,
20
20
+
},
21
21
+
});
22
22
+
23
23
+
export const [loginState, setLoginState] = createSignal(false);
24
24
+
let agent: OAuthUserAgent;
25
25
+
26
26
+
const Login: Component = () => {
27
27
+
const [notice, setNotice] = createSignal("");
28
28
+
const [loginInput, setLoginInput] = createSignal("");
29
29
+
30
30
+
const login = async (handle: string) => {
31
31
+
try {
32
32
+
if (!handle) return;
33
33
+
let resolved;
34
34
+
document.querySelector(".submitInfo")!.removeAttribute("hidden");
35
35
+
document
36
36
+
.querySelector('button[type="submit"]')!
37
37
+
.setAttribute("disabled", "");
38
38
+
if (!isHandle(handle)) {
39
39
+
setNotice(`Resolving your service...`);
40
40
+
resolved = await resolveFromService(handle);
41
41
+
} else {
42
42
+
setNotice(`Resolving your identity...`);
43
43
+
resolved = await resolveFromIdentity(handle);
44
44
+
}
45
45
+
46
46
+
setNotice(`Contacting your data server...`);
47
47
+
const authUrl = await createAuthorizationUrl({
48
48
+
scope: import.meta.env.VITE_OAUTH_SCOPE,
49
49
+
...resolved,
50
50
+
});
51
51
+
52
52
+
setNotice(`Redirecting...`);
53
53
+
await new Promise((resolve) => setTimeout(resolve, 500));
54
54
+
55
55
+
location.assign(authUrl);
56
56
+
} catch (e: unknown) {
57
57
+
if (e instanceof Error) {
58
58
+
console.error(e);
59
59
+
setNotice(`${e.message}`);
60
60
+
} else {
61
61
+
console.error(e);
62
62
+
setNotice(`Unknown error, check console ¯\\_(ツ)_/¯`);
63
63
+
}
64
64
+
} finally {
65
65
+
document
66
66
+
.querySelector('button[type="submit"]')!
67
67
+
.removeAttribute("disabled");
68
68
+
}
69
69
+
};
70
70
+
71
71
+
return (
72
72
+
<>
73
73
+
<Container
74
74
+
title="Log in"
75
75
+
children={
76
76
+
<>
77
77
+
<div class="login">
78
78
+
<form name="login" id="login" onclick={(e) => e.preventDefault()}>
79
79
+
<label for="handle">Handle</label>
80
80
+
<br />
81
81
+
<input
82
82
+
type="text"
83
83
+
id="handle"
84
84
+
name="handle"
85
85
+
maxlength="255"
86
86
+
placeholder="soykaf.com"
87
87
+
onInput={(e) => setLoginInput(e.currentTarget.value)}
88
88
+
required
89
89
+
/>
90
90
+
<br />
91
91
+
<button type="submit" onclick={() => login(loginInput())}>
92
92
+
Login
93
93
+
</button>
94
94
+
</form>
95
95
+
<p class="submitInfo" hidden>
96
96
+
{notice()}
97
97
+
</p>
98
98
+
</div>
99
99
+
</>
100
100
+
}
101
101
+
/>
102
102
+
</>
103
103
+
);
104
104
+
};
105
105
+
106
106
+
const retrieveSession = async (): Promise<void> => {
107
107
+
const init = async (): Promise<Session | undefined> => {
108
108
+
const params = new URLSearchParams(location.hash.slice(1));
109
109
+
110
110
+
if (params.has("state") && (params.has("code") || params.has("error"))) {
111
111
+
history.replaceState(null, "", location.pathname + location.search);
112
112
+
113
113
+
const session = await finalizeAuthorization(params);
114
114
+
console.log("Finalizing authorization...", session);
115
115
+
const agent = new OAuthUserAgent(session);
116
116
+
console.log(await agent.getSession());
117
117
+
const did = session.info.sub;
118
118
+
119
119
+
localStorage.setItem("currentUser", did);
120
120
+
return session;
121
121
+
} else {
122
122
+
const currentUser = localStorage.getItem("currentUser");
123
123
+
124
124
+
if (currentUser) {
125
125
+
try {
126
126
+
console.log("Retrieving session...");
127
127
+
return await getSession(currentUser as Did);
128
128
+
} catch (err) {
129
129
+
deleteStoredSession(currentUser as Did);
130
130
+
localStorage.removeItem("currentUser");
131
131
+
throw err;
132
132
+
}
133
133
+
}
134
134
+
}
135
135
+
};
136
136
+
137
137
+
const session = await init().catch(() => {});
138
138
+
139
139
+
if (session) {
140
140
+
console.log("Retrieved session!", session);
141
141
+
agent = new OAuthUserAgent(session);
142
142
+
setLoginState(true);
143
143
+
}
144
144
+
};
145
145
+
146
146
+
const killSession = async (): Promise<void> => {
147
147
+
await agent.signOut();
148
148
+
setLoginState(false);
149
149
+
localStorage.removeItem("currentUser");
150
150
+
location.href = "/";
151
151
+
};
152
152
+
153
153
+
export { agent, killSession, Login, retrieveSession };
+7
-13
src/index.tsx
View file
Reviewed
···
2
2
import { render } from "solid-js/web";
3
3
import "solid-devtools";
4
4
import { Route, Router } from "@solidjs/router";
5
5
-
6
6
-
import Login from "./routes/login";
7
7
-
8
8
-
const root = document.getElementById("root");
9
9
-
10
10
-
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
11
11
-
throw new Error(
12
12
-
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
13
13
-
);
14
14
-
}
5
5
+
import Splash from "./routes/splash";
6
6
+
import Base from "./base";
7
7
+
import Dashboard from "./routes/dashboard";
15
8
16
9
render(
17
10
() => (
18
18
-
<Router root={Login}>
19
19
-
<Route path="/" component={Login} />
11
11
+
<Router root={Base}>
12
12
+
<Route path="/" component={Splash} />
13
13
+
<Route path="/dash" component={Dashboard} />
20
14
</Router>
21
15
),
22
22
-
root!,
16
16
+
document.getElementById("root") as HTMLElement,
23
17
);
+16
src/routes/dashboard.tsx
View file
Reviewed
···
1
1
+
import { killSession, loginState } from "../components/login";
2
2
+
3
3
+
const Dashboard = () => {
4
4
+
if (!loginState()) {
5
5
+
location.href = "/";
6
6
+
}
7
7
+
8
8
+
return (
9
9
+
<div>
10
10
+
<h1>Dashboard</h1>
11
11
+
<button onclick={killSession}>Log out</button>
12
12
+
</div>
13
13
+
);
14
14
+
};
15
15
+
16
16
+
export default Dashboard;
-71
src/routes/login.tsx
Reviewed
···
1
1
-
import { Component } from "solid-js";
2
2
-
import Navbar from "../components/navbar";
3
3
-
import "../styles/main.scss";
4
4
-
import typefaceLogo from "/logo.png?url";
5
5
-
import blueskyLogo from "/bluesky.svg?url";
6
6
-
import tangledLogo from "/tangled.svg?url";
7
7
-
import Container from "../components/container";
8
8
-
9
9
-
const Login: Component = () => {
10
10
-
return (
11
11
-
<>
12
12
-
<header>
13
13
-
<Navbar />
14
14
-
</header>
15
15
-
<main>
16
16
-
<div id="sidebar">
17
17
-
<Container
18
18
-
title="Log in"
19
19
-
children={
20
20
-
<div class="login">
21
21
-
<form name="login" id="login">
22
22
-
<label for="handle">Handle</label>
23
23
-
<br />
24
24
-
<input
25
25
-
type="text"
26
26
-
id="handle"
27
27
-
name="handle"
28
28
-
maxlength="255"
29
29
-
placeholder="soykaf.com"
30
30
-
required
31
31
-
/>
32
32
-
<br />
33
33
-
<button type="submit">Login</button>
34
34
-
</form>
35
35
-
</div>
36
36
-
}
37
37
-
/>
38
38
-
</div>
39
39
-
<div id="content">
40
40
-
<Container
41
41
-
title="About"
42
42
-
children={
43
43
-
<div class="container-content">
44
44
-
<img class={"typeface"} src={typefaceLogo} />
45
45
-
<h2>A Bluesky client with a familiar face</h2>
46
46
-
<hr />
47
47
-
<p>
48
48
-
<b>Bluroma</b> is a web client for Bluesky, built to provide a
49
49
-
customizable power-user experience. Its design is heavily
50
50
-
influenced by the <a href="https://pleroma.social">Pleroma</a>{" "}
51
51
-
and <a href="https://akkoma.social">Akkoma</a> projects, and
52
52
-
intends to provide a similar user interface for Bluesky users.
53
53
-
</p>
54
54
-
<div class="logo-crawl">
55
55
-
<a href="https://bsky.social/profile/did:plc:5szlrh3xkfxxsuu4mo6oe6h7">
56
56
-
<img src={blueskyLogo} />
57
57
-
</a>
58
58
-
<a href="https://tangled.org/@hexmani.ac/bluroma">
59
59
-
<img src={tangledLogo}></img>
60
60
-
</a>
61
61
-
</div>
62
62
-
</div>
63
63
-
}
64
64
-
/>
65
65
-
</div>
66
66
-
</main>
67
67
-
</>
68
68
-
);
69
69
-
};
70
70
-
71
71
-
export default Login;
+47
src/routes/splash.tsx
View file
Reviewed
···
1
1
+
import { Component } from "solid-js";
2
2
+
import Navbar from "../components/navbar";
3
3
+
import "../styles/main.scss";
4
4
+
import typefaceLogo from "/logo.png?url";
5
5
+
import blueskyLogo from "/bluesky.svg?url";
6
6
+
import tangledLogo from "/tangled.svg?url";
7
7
+
import Container from "../components/container";
8
8
+
import { Login } from "../components/login";
9
9
+
10
10
+
const Splash: Component = () => {
11
11
+
return (
12
12
+
<>
13
13
+
<div id="sidebar">
14
14
+
<Login />
15
15
+
</div>
16
16
+
<div id="content">
17
17
+
<Container
18
18
+
title="About"
19
19
+
children={
20
20
+
<div class="container-content">
21
21
+
<img class={"typeface"} src={typefaceLogo} />
22
22
+
<h2>A Bluesky client with a familiar face</h2>
23
23
+
<hr />
24
24
+
<p>
25
25
+
<b>Bluroma</b> is a web client for Bluesky, built to provide a
26
26
+
customizable power-user experience. Its design is heavily
27
27
+
influenced by the <a href="https://pleroma.social">Pleroma</a>{" "}
28
28
+
and <a href="https://akkoma.social">Akkoma</a> projects, and
29
29
+
intends to provide a similar user interface for Bluesky users.
30
30
+
</p>
31
31
+
<div class="logo-crawl">
32
32
+
<a href="https://bsky.social/profile/did:plc:5szlrh3xkfxxsuu4mo6oe6h7">
33
33
+
<img src={blueskyLogo} />
34
34
+
</a>
35
35
+
<a href="https://tangled.org/@hexmani.ac/bluroma">
36
36
+
<img src={tangledLogo}></img>
37
37
+
</a>
38
38
+
</div>
39
39
+
</div>
40
40
+
}
41
41
+
/>
42
42
+
</div>
43
43
+
</>
44
44
+
);
45
45
+
};
46
46
+
47
47
+
export default Splash;
+14
src/styles/button.scss
View file
Reviewed
···
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
27
+
28
28
+
button:disabled,
29
29
+
.button:disabled {
30
30
+
color: #666769;
31
31
+
}
32
32
+
33
33
+
button:focus,
34
34
+
.button:focus {
35
35
+
outline: none;
36
36
+
box-shadow:
37
37
+
0px 0px 1px 2px rgba(185, 185, 186, 0.4) inset,
38
38
+
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
39
39
+
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
40
40
+
}
+10
src/styles/container.scss
View file
Reviewed
···
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
30
+
31
31
+
.submitInfo {
32
32
+
background-color: rgba(211, 16, 20, 0.5);
33
33
+
border-radius: vars.$containerBorderRadius;
34
34
+
color: rgba(185, 185, 186, 1);
35
35
+
padding: 0.5rem 1rem;
36
36
+
width: 80%;
37
37
+
text-align: center;
38
38
+
align-self: center;
39
39
+
}
+12
static/oauth/client-metadata.json
View file
Reviewed
···
1
1
+
{
2
2
+
"client_id": "https://pl.hexmani.ac/oauth/client-metadata.json",
3
3
+
"client_name": "Bluroma",
4
4
+
"client_uri": "https://pl.hexmani.ac",
5
5
+
"redirect_uris": ["https://pl.hexmani.ac/"],
6
6
+
"scope": "atproto transition:generic",
7
7
+
"grant_types": ["authorization_code", "refresh_token"],
8
8
+
"response_types": ["code"],
9
9
+
"token_endpoint_auth_method": "none",
10
10
+
"application_type": "web",
11
11
+
"dpop_bound_access_tokens": true
12
12
+
}
+37
-2
vite.config.ts
View file
Reviewed
···
1
1
import { defineConfig } from "vite";
2
2
import solidPlugin from "vite-plugin-solid";
3
3
import devtools from "solid-devtools/vite";
4
4
+
import metadata from "./static/oauth/client-metadata.json";
5
5
+
6
6
+
const SERVER_HOST = "127.0.0.1";
7
7
+
const SERVER_PORT = 3000;
4
8
5
9
export default defineConfig({
6
6
-
plugins: [devtools(), solidPlugin()],
10
10
+
plugins: [
11
11
+
devtools(),
12
12
+
solidPlugin(),
13
13
+
{
14
14
+
// Shamelessly stolen from PDSls: https://tangled.org/@pdsls.dev/pdsls/blob/main/vite.config.ts
15
15
+
name: "oauth",
16
16
+
config(_conf, { command }) {
17
17
+
if (command === "build") {
18
18
+
process.env.VITE_OAUTH_CLIENT_ID = metadata.client_id;
19
19
+
process.env.VITE_OAUTH_REDIRECT_URL = metadata.redirect_uris[0];
20
20
+
} else {
21
21
+
const redirectUri = ((): string => {
22
22
+
const url = new URL(metadata.redirect_uris[0]);
23
23
+
return `http://${SERVER_HOST}:${SERVER_PORT}${url.pathname}`;
24
24
+
})();
25
25
+
26
26
+
const clientId =
27
27
+
`http://localhost` +
28
28
+
`?redirect_uri=${encodeURIComponent(redirectUri)}` +
29
29
+
`&scope=${encodeURIComponent(metadata.scope)}`;
30
30
+
31
31
+
process.env.VITE_DEV_SERVER_PORT = "" + SERVER_PORT;
32
32
+
process.env.VITE_OAUTH_CLIENT_ID = clientId;
33
33
+
process.env.VITE_OAUTH_REDIRECT_URL = redirectUri;
34
34
+
}
35
35
+
36
36
+
process.env.VITE_CLIENT_URI = metadata.client_uri;
37
37
+
process.env.VITE_OAUTH_SCOPE = metadata.scope;
38
38
+
},
39
39
+
},
40
40
+
],
7
41
server: {
8
8
-
port: 3000,
42
42
+
host: SERVER_HOST,
43
43
+
port: SERVER_PORT,
9
44
},
10
45
root: "./",
11
46
build: {