tangled
alpha
login
or
join now
hexmani.ac
/
bluroma
12
fork
atom
pleroma-like client for Bluesky
pl.hexmani.ac
bluesky
pleroma
social-media
12
fork
atom
overview
issues
3
pulls
pipelines
Add support for OAuth login
hexmani.ac
5 months ago
d3765b7d
60dffc1d
verified
This commit was signed with the committer's
known signature
.
hexmani.ac
SSH Key Fingerprint:
SHA256:tV3v2UX4P3x12jjh+mHVzpRQ4ZhNBCHoFwqRiYzzTcM=
+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
···
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
···
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
···
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
···
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
···
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
···
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
···
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
···
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
···
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
···
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
···
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
···
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: {