From 9f7b903901acb0cd6ec9cb2146406a92ebf79cab Mon Sep 17 00:00:00 2001 From: Vika Date: Fri, 7 Oct 2022 19:53:04 +0300 Subject: templates: move static assets to the templates crate It makes more sense to keep CSS near the templates, and the client-side JavaScript code too, since it depends on the DOM structure to work. Additionally, the overhead of `include_dir!()` is almost completely mitigated by the fact that this is a separate crate that isn't recompiled often. The linking stage, however, is still expected to take a little bit long. But I doubt it'd be longer than what it was before, since it's the same exact files that get linked into the app. --- kittybox-rs/templates/javascript/src/indieauth.ts | 150 ++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 kittybox-rs/templates/javascript/src/indieauth.ts (limited to 'kittybox-rs/templates/javascript/src/indieauth.ts') diff --git a/kittybox-rs/templates/javascript/src/indieauth.ts b/kittybox-rs/templates/javascript/src/indieauth.ts new file mode 100644 index 0000000..01732b7 --- /dev/null +++ b/kittybox-rs/templates/javascript/src/indieauth.ts @@ -0,0 +1,150 @@ +import { unreachable } from "./lib.js"; + +const WEBAUTHN_TIMEOUT = 60 * 1000; + +interface KittyboxWebauthnPreRegistrationData { + challenge: string, + rp: PublicKeyCredentialRpEntity, + user: { + cred_id: string, + name: string, + displayName: string + } +} + +async function webauthn_create_credential() { + const response = await fetch("/.kittybox/webauthn/pre_register"); + const { challenge, rp, user }: KittyboxWebauthnPreRegistrationData = await response.json(); + + return await navigator.credentials.create({ + publicKey: { + challenge: Uint8Array.from(challenge, (c) => c.charCodeAt(0)), + rp: rp, + user: { + id: Uint8Array.from(user.cred_id, (c) => c.charCodeAt(0)), + name: user.name, + displayName: user.displayName + }, + pubKeyCredParams: [{alg: -7, type: "public-key"}], + authenticatorSelection: {}, + timeout: WEBAUTHN_TIMEOUT, + attestation: "none" + } + }); +} + +interface KittyboxWebauthnCredential { + id: string, + type: "public-key" +} + +interface KittyboxWebauthnPreAuthenticationData { + challenge: string, + credentials: KittyboxWebauthnCredential[] +} + +async function webauthn_authenticate() { + const response = await fetch("/.kittybox/webauthn/pre_auth"); + const { challenge, credentials } = await response.json() as unknown as KittyboxWebauthnPreAuthenticationData; + + try { + return await navigator.credentials.get({ + publicKey: { + challenge: Uint8Array.from(challenge, (c) => c.charCodeAt(0)), + allowCredentials: credentials.map(cred => ({ + id: Uint8Array.from(cred.id, c => c.charCodeAt(0)), + type: cred.type + })), + timeout: WEBAUTHN_TIMEOUT + } + }) + } catch (e) { + console.error("WebAuthn authentication failed:", e); + alert("Using your authenticator failed. (Check the DevTools for details)"); + throw e; + } +} + +export async function submit_handler(e: SubmitEvent) { + e.preventDefault(); + if (e.target != null && e.target instanceof HTMLFormElement) { + const form = e.target as HTMLFormElement; + + let scopes: Array; + let scope_elem = form.elements.namedItem("scope"); + if (scope_elem == null) { + scopes = [] + } else if (scope_elem instanceof Element) { + scopes = ([scope_elem] as Array) + .filter((e: HTMLInputElement) => e.checked) + .map((e: HTMLInputElement) => e.value); + } else if (scope_elem instanceof RadioNodeList) { + scopes = (Array.from(scope_elem) as Array) + .filter((e: HTMLInputElement) => e.checked) + .map((e: HTMLInputElement) => e.value); + } else { + unreachable("HTMLFormControlsCollection returned something that's not null, Element or RadioNodeList") + } + + const authorization_request = { + response_type: (form.elements.namedItem("response_type") as HTMLInputElement).value, + client_id: (form.elements.namedItem("client_id") as HTMLInputElement).value, + redirect_uri: (form.elements.namedItem("redirect_uri") as HTMLInputElement).value, + state: (form.elements.namedItem("state") as HTMLInputElement).value, + code_challenge: (form.elements.namedItem("code_challenge") as HTMLInputElement).value, + code_challenge_method: (form.elements.namedItem("code_challenge_method") as HTMLInputElement).value, + // I would love to leave that as a list, but such is the form of + // IndieAuth. application/x-www-form-urlencoded doesn't have + // lists, so scopes are space-separated instead. It is annoying. + scope: scopes.length > 0 ? scopes.join(" ") : undefined, + }; + + let credential = null; + switch ((form.elements.namedItem("auth_method") as HTMLInputElement).value) { + case "password": + credential = (form.elements.namedItem("user_password") as HTMLInputElement).value; + if (credential.length == 0) { + alert("Please enter a password.") + return + } + break; + case "webauthn": + // credential = await webauthn_authenticate(); + alert("WebAuthn isn't implemented yet!") + return + break + default: + alert("Please choose an authentication method.") + return + } + + console.log("Authorization request:", authorization_request); + console.log("Authentication method:", credential); + + const body = JSON.stringify({ + request: authorization_request, + authorization_method: credential + }); + console.log(body); + + const response = await fetch(form.action, { + method: form.method, + body: body, + headers: { + "Content-Type": "application/json" + } + }); + + if (response.ok) { + let location = response.headers.get("Location"); + if (location != null) { + window.location.href = location + } else { + throw "Error: didn't return a location" + } + } + } else { + return + } + +} -- cgit 1.4.1