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

Add support for OAuth login

hexmani.ac d3765b7d 60dffc1d

verified
+34 -1
bun.lock
···
"": {
"name": "vite-template-solid",
"dependencies": {
+
"@atcute/lexicons": "^1.2.2",
+
"@atcute/oauth-browser-client": "^1.0.27",
"@solidjs/router": "^0.15.3",
"solid-js": "^1.9.5",
},
"devDependencies": {
+
"@types/bun": "^1.3.0",
"sass": "^1.81.0",
"solid-devtools": "^0.34.3",
"typescript": "^5.7.2",
···
},
},
"packages": {
+
"@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=="],
+
+
"@atcute/identity": ["@atcute/identity@1.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@badrap/valita": "^0.4.6" } }, "sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q=="],
+
+
"@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="],
+
+
"@atcute/multibase": ["@atcute/multibase@1.1.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.5" } }, "sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg=="],
+
+
"@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=="],
+
+
"@atcute/uint8array": ["@atcute/uint8array@1.0.5", "", {}, "sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q=="],
+
"@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=="],
"@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="],
···
"@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=="],
"@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=="],
+
+
"@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="],
···
"@solidjs/router": ["@solidjs/router@0.15.3", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw=="],
+
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
+
"@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=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
···
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
+
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
+
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
"@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
+
+
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
+
"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=="],
"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=="],
···
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"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=="],
+
+
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="],
···
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
+
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
···
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
-
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
···
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+
"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=="],
"validate-html-nesting": ["validate-html-nesting@1.2.3", "", {}, "sha512-kdkWdCl6eCeLlRShJKbjVOU2kFKxMF8Ghu50n+crEoyx+VKm3FxAxF9z4DCy6+bbTOqNW0+jcIYRnjoIRzigRw=="],
···
"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=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
}
}
+4 -1
package.json
···
},
"license": "MIT",
"devDependencies": {
+
"@types/bun": "^1.3.0",
+
"sass": "^1.81.0",
"solid-devtools": "^0.34.3",
-
"sass": "^1.81.0",
"typescript": "^5.7.2",
"vite": "^7.1.4",
"vite-plugin-solid": "^2.11.8"
},
"dependencies": {
+
"@atcute/lexicons": "^1.2.2",
+
"@atcute/oauth-browser-client": "^1.0.27",
"@solidjs/router": "^0.15.3",
"solid-js": "^1.9.5"
}
+29
src/base.tsx
···
+
import { RouteSectionProps } from "@solidjs/router";
+
import { Component, createSignal, onMount, Show } from "solid-js";
+
import { retrieveSession, loginState } from "./components/login";
+
import Navbar from "./components/navbar";
+
+
const Base = (props: RouteSectionProps<unknown>) => {
+
const [isLoading, setIsLoading] = createSignal(true);
+
+
onMount(async () => {
+
await retrieveSession();
+
if (loginState() && location.pathname === "/") {
+
window.location.href = "/dash";
+
}
+
setIsLoading(false);
+
});
+
+
return (
+
<Show when={!isLoading()} fallback={<></>}>
+
<>
+
<header>
+
<Navbar />
+
</header>
+
<main>{props.children}</main>
+
</>
+
</Show>
+
);
+
};
+
+
export default Base;
+153
src/components/login.tsx
···
+
import { Did, isHandle } from "@atcute/lexicons/syntax";
+
import {
+
configureOAuth,
+
createAuthorizationUrl,
+
deleteStoredSession,
+
finalizeAuthorization,
+
getSession,
+
OAuthUserAgent,
+
resolveFromIdentity,
+
resolveFromService,
+
Session,
+
} from "@atcute/oauth-browser-client";
+
import { Component, createSignal } from "solid-js";
+
import Container from "./container";
+
+
configureOAuth({
+
metadata: {
+
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
+
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL,
+
},
+
});
+
+
export const [loginState, setLoginState] = createSignal(false);
+
let agent: OAuthUserAgent;
+
+
const Login: Component = () => {
+
const [notice, setNotice] = createSignal("");
+
const [loginInput, setLoginInput] = createSignal("");
+
+
const login = async (handle: string) => {
+
try {
+
if (!handle) return;
+
let resolved;
+
document.querySelector(".submitInfo")!.removeAttribute("hidden");
+
document
+
.querySelector('button[type="submit"]')!
+
.setAttribute("disabled", "");
+
if (!isHandle(handle)) {
+
setNotice(`Resolving your service...`);
+
resolved = await resolveFromService(handle);
+
} else {
+
setNotice(`Resolving your identity...`);
+
resolved = await resolveFromIdentity(handle);
+
}
+
+
setNotice(`Contacting your data server...`);
+
const authUrl = await createAuthorizationUrl({
+
scope: import.meta.env.VITE_OAUTH_SCOPE,
+
...resolved,
+
});
+
+
setNotice(`Redirecting...`);
+
await new Promise((resolve) => setTimeout(resolve, 500));
+
+
location.assign(authUrl);
+
} catch (e: unknown) {
+
if (e instanceof Error) {
+
console.error(e);
+
setNotice(`${e.message}`);
+
} else {
+
console.error(e);
+
setNotice(`Unknown error, check console ¯\\_(ツ)_/¯`);
+
}
+
} finally {
+
document
+
.querySelector('button[type="submit"]')!
+
.removeAttribute("disabled");
+
}
+
};
+
+
return (
+
<>
+
<Container
+
title="Log in"
+
children={
+
<>
+
<div class="login">
+
<form name="login" id="login" onclick={(e) => e.preventDefault()}>
+
<label for="handle">Handle</label>
+
<br />
+
<input
+
type="text"
+
id="handle"
+
name="handle"
+
maxlength="255"
+
placeholder="soykaf.com"
+
onInput={(e) => setLoginInput(e.currentTarget.value)}
+
required
+
/>
+
<br />
+
<button type="submit" onclick={() => login(loginInput())}>
+
Login
+
</button>
+
</form>
+
<p class="submitInfo" hidden>
+
{notice()}
+
</p>
+
</div>
+
</>
+
}
+
/>
+
</>
+
);
+
};
+
+
const retrieveSession = async (): Promise<void> => {
+
const init = async (): Promise<Session | undefined> => {
+
const params = new URLSearchParams(location.hash.slice(1));
+
+
if (params.has("state") && (params.has("code") || params.has("error"))) {
+
history.replaceState(null, "", location.pathname + location.search);
+
+
const session = await finalizeAuthorization(params);
+
console.log("Finalizing authorization...", session);
+
const agent = new OAuthUserAgent(session);
+
console.log(await agent.getSession());
+
const did = session.info.sub;
+
+
localStorage.setItem("currentUser", did);
+
return session;
+
} else {
+
const currentUser = localStorage.getItem("currentUser");
+
+
if (currentUser) {
+
try {
+
console.log("Retrieving session...");
+
return await getSession(currentUser as Did);
+
} catch (err) {
+
deleteStoredSession(currentUser as Did);
+
localStorage.removeItem("currentUser");
+
throw err;
+
}
+
}
+
}
+
};
+
+
const session = await init().catch(() => {});
+
+
if (session) {
+
console.log("Retrieved session!", session);
+
agent = new OAuthUserAgent(session);
+
setLoginState(true);
+
}
+
};
+
+
const killSession = async (): Promise<void> => {
+
await agent.signOut();
+
setLoginState(false);
+
localStorage.removeItem("currentUser");
+
location.href = "/";
+
};
+
+
export { agent, killSession, Login, retrieveSession };
+7 -13
src/index.tsx
···
import { render } from "solid-js/web";
import "solid-devtools";
import { Route, Router } from "@solidjs/router";
-
-
import Login from "./routes/login";
-
-
const root = document.getElementById("root");
-
-
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
-
throw new Error(
-
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
-
);
-
}
+
import Splash from "./routes/splash";
+
import Base from "./base";
+
import Dashboard from "./routes/dashboard";
render(
() => (
-
<Router root={Login}>
-
<Route path="/" component={Login} />
+
<Router root={Base}>
+
<Route path="/" component={Splash} />
+
<Route path="/dash" component={Dashboard} />
</Router>
),
-
root!,
+
document.getElementById("root") as HTMLElement,
);
+16
src/routes/dashboard.tsx
···
+
import { killSession, loginState } from "../components/login";
+
+
const Dashboard = () => {
+
if (!loginState()) {
+
location.href = "/";
+
}
+
+
return (
+
<div>
+
<h1>Dashboard</h1>
+
<button onclick={killSession}>Log out</button>
+
</div>
+
);
+
};
+
+
export default Dashboard;
-71
src/routes/login.tsx
···
-
import { Component } from "solid-js";
-
import Navbar from "../components/navbar";
-
import "../styles/main.scss";
-
import typefaceLogo from "/logo.png?url";
-
import blueskyLogo from "/bluesky.svg?url";
-
import tangledLogo from "/tangled.svg?url";
-
import Container from "../components/container";
-
-
const Login: Component = () => {
-
return (
-
<>
-
<header>
-
<Navbar />
-
</header>
-
<main>
-
<div id="sidebar">
-
<Container
-
title="Log in"
-
children={
-
<div class="login">
-
<form name="login" id="login">
-
<label for="handle">Handle</label>
-
<br />
-
<input
-
type="text"
-
id="handle"
-
name="handle"
-
maxlength="255"
-
placeholder="soykaf.com"
-
required
-
/>
-
<br />
-
<button type="submit">Login</button>
-
</form>
-
</div>
-
}
-
/>
-
</div>
-
<div id="content">
-
<Container
-
title="About"
-
children={
-
<div class="container-content">
-
<img class={"typeface"} src={typefaceLogo} />
-
<h2>A Bluesky client with a familiar face</h2>
-
<hr />
-
<p>
-
<b>Bluroma</b> is a web client for Bluesky, built to provide a
-
customizable power-user experience. Its design is heavily
-
influenced by the <a href="https://pleroma.social">Pleroma</a>{" "}
-
and <a href="https://akkoma.social">Akkoma</a> projects, and
-
intends to provide a similar user interface for Bluesky users.
-
</p>
-
<div class="logo-crawl">
-
<a href="https://bsky.social/profile/did:plc:5szlrh3xkfxxsuu4mo6oe6h7">
-
<img src={blueskyLogo} />
-
</a>
-
<a href="https://tangled.org/@hexmani.ac/bluroma">
-
<img src={tangledLogo}></img>
-
</a>
-
</div>
-
</div>
-
}
-
/>
-
</div>
-
</main>
-
</>
-
);
-
};
-
-
export default Login;
+47
src/routes/splash.tsx
···
+
import { Component } from "solid-js";
+
import Navbar from "../components/navbar";
+
import "../styles/main.scss";
+
import typefaceLogo from "/logo.png?url";
+
import blueskyLogo from "/bluesky.svg?url";
+
import tangledLogo from "/tangled.svg?url";
+
import Container from "../components/container";
+
import { Login } from "../components/login";
+
+
const Splash: Component = () => {
+
return (
+
<>
+
<div id="sidebar">
+
<Login />
+
</div>
+
<div id="content">
+
<Container
+
title="About"
+
children={
+
<div class="container-content">
+
<img class={"typeface"} src={typefaceLogo} />
+
<h2>A Bluesky client with a familiar face</h2>
+
<hr />
+
<p>
+
<b>Bluroma</b> is a web client for Bluesky, built to provide a
+
customizable power-user experience. Its design is heavily
+
influenced by the <a href="https://pleroma.social">Pleroma</a>{" "}
+
and <a href="https://akkoma.social">Akkoma</a> projects, and
+
intends to provide a similar user interface for Bluesky users.
+
</p>
+
<div class="logo-crawl">
+
<a href="https://bsky.social/profile/did:plc:5szlrh3xkfxxsuu4mo6oe6h7">
+
<img src={blueskyLogo} />
+
</a>
+
<a href="https://tangled.org/@hexmani.ac/bluroma">
+
<img src={tangledLogo}></img>
+
</a>
+
</div>
+
</div>
+
}
+
/>
+
</div>
+
</>
+
);
+
};
+
+
export default Splash;
+14
src/styles/button.scss
···
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
}
+
+
button:disabled,
+
.button:disabled {
+
color: #666769;
+
}
+
+
button:focus,
+
.button:focus {
+
outline: none;
+
box-shadow:
+
0px 0px 1px 2px rgba(185, 185, 186, 0.4) inset,
+
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
+
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
+
}
+10
src/styles/container.scss
···
0px 1px 3px 0px rgba(0, 0, 0, 0.4),
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
}
+
+
.submitInfo {
+
background-color: rgba(211, 16, 20, 0.5);
+
border-radius: vars.$containerBorderRadius;
+
color: rgba(185, 185, 186, 1);
+
padding: 0.5rem 1rem;
+
width: 80%;
+
text-align: center;
+
align-self: center;
+
}
+12
static/oauth/client-metadata.json
···
+
{
+
"client_id": "https://pl.hexmani.ac/oauth/client-metadata.json",
+
"client_name": "Bluroma",
+
"client_uri": "https://pl.hexmani.ac",
+
"redirect_uris": ["https://pl.hexmani.ac/"],
+
"scope": "atproto transition:generic",
+
"grant_types": ["authorization_code", "refresh_token"],
+
"response_types": ["code"],
+
"token_endpoint_auth_method": "none",
+
"application_type": "web",
+
"dpop_bound_access_tokens": true
+
}
+37 -2
vite.config.ts
···
import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";
import devtools from "solid-devtools/vite";
+
import metadata from "./static/oauth/client-metadata.json";
+
+
const SERVER_HOST = "127.0.0.1";
+
const SERVER_PORT = 3000;
export default defineConfig({
-
plugins: [devtools(), solidPlugin()],
+
plugins: [
+
devtools(),
+
solidPlugin(),
+
{
+
// Shamelessly stolen from PDSls: https://tangled.org/@pdsls.dev/pdsls/blob/main/vite.config.ts
+
name: "oauth",
+
config(_conf, { command }) {
+
if (command === "build") {
+
process.env.VITE_OAUTH_CLIENT_ID = metadata.client_id;
+
process.env.VITE_OAUTH_REDIRECT_URL = metadata.redirect_uris[0];
+
} else {
+
const redirectUri = ((): string => {
+
const url = new URL(metadata.redirect_uris[0]);
+
return `http://${SERVER_HOST}:${SERVER_PORT}${url.pathname}`;
+
})();
+
+
const clientId =
+
`http://localhost` +
+
`?redirect_uri=${encodeURIComponent(redirectUri)}` +
+
`&scope=${encodeURIComponent(metadata.scope)}`;
+
+
process.env.VITE_DEV_SERVER_PORT = "" + SERVER_PORT;
+
process.env.VITE_OAUTH_CLIENT_ID = clientId;
+
process.env.VITE_OAUTH_REDIRECT_URL = redirectUri;
+
}
+
+
process.env.VITE_CLIENT_URI = metadata.client_uri;
+
process.env.VITE_OAUTH_SCOPE = metadata.scope;
+
},
+
},
+
],
server: {
-
port: 3000,
+
host: SERVER_HOST,
+
port: SERVER_PORT,
},
root: "./",
build: {