about summary refs log tree commit diff
path: root/kittybox-rs/javascript/src
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2022-09-28 23:13:02 +0300
committerVika <vika@fireburn.ru>2022-09-28 23:13:02 +0300
commitc35a55babd9aa6e1ad8797f3d54deec2f78ff78a (patch)
tree742023e1d1e75601b7f7cc4020b53938e82416d8 /kittybox-rs/javascript/src
parentf420c7fd09b0f9ef82784a9c49889f620e73e886 (diff)
Switch to TypeScript
This neccesitates installing TypeScript to build Kittybox, but
thankfully Nix actually takes care of that. Build Kittybox with Nix
and you won't have problems.

Also now I can safely do stuff.
Diffstat (limited to 'kittybox-rs/javascript/src')
-rw-r--r--kittybox-rs/javascript/src/indieauth.ts145
-rw-r--r--kittybox-rs/javascript/src/onboarding.ts120
2 files changed, 265 insertions, 0 deletions
diff --git a/kittybox-rs/javascript/src/indieauth.ts b/kittybox-rs/javascript/src/indieauth.ts
new file mode 100644
index 0000000..8222070
--- /dev/null
+++ b/kittybox-rs/javascript/src/indieauth.ts
@@ -0,0 +1,145 @@
+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;
+  }
+}
+
+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<string>;
+    if (form.elements.namedItem("scope") === undefined) {
+      scopes = []
+    } else if (form.elements.namedItem("scope") instanceof Node) {
+      scopes = ([form.elements.namedItem("scope")] as Array<HTMLInputElement>)
+        .filter((e: HTMLInputElement) => e.checked)
+        .map((e: HTMLInputElement) => e.value);
+    } else {
+      scopes = (Array.from(form.elements.namedItem("scope") as RadioNodeList) as Array<HTMLInputElement>)
+        .filter((e: HTMLInputElement) => e.checked)
+        .map((e: HTMLInputElement) => e.value);
+    }
+
+    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
+  }
+
+}
diff --git a/kittybox-rs/javascript/src/onboarding.ts b/kittybox-rs/javascript/src/onboarding.ts
new file mode 100644
index 0000000..0b455eb
--- /dev/null
+++ b/kittybox-rs/javascript/src/onboarding.ts
@@ -0,0 +1,120 @@
+const firstOnboardingCard = "intro";
+
+function switchOnboardingCard(card: string) {
+  (Array.from(document.querySelectorAll("form.onboarding > fieldset")) as HTMLElement[])
+    .map((node: HTMLElement) => {
+      if (node.id == card) {
+        node.style.display = "block";
+      } else {
+        node.style.display = "none";
+      }
+    });
+
+  (Array.from(document.querySelectorAll("form.onboarding > ul#progressbar > li")) as HTMLElement[])
+    .map(node => {
+      if (node.id == card) {
+        node.classList.add("active")
+      } else {
+        node.classList.remove("active")
+      }
+    });
+};
+
+interface Window {
+  kittybox_onboarding: {
+    switchOnboardingCard: (card: string) => void
+  }
+}
+
+window.kittybox_onboarding = {
+  switchOnboardingCard
+};
+
+(document.querySelector("form.onboarding > ul#progressbar") as HTMLElement).style.display = "";
+switchOnboardingCard(firstOnboardingCard);
+
+function switchCardOnClick(event: MouseEvent) {
+  if (event.target instanceof HTMLElement) {
+    if (event.target.dataset.card !== undefined) {
+      switchOnboardingCard(event.target.dataset.card)
+    }
+  }
+}
+
+function multiInputAddMore(event: (MouseEvent | { target: HTMLElement })) {
+  if (event.target instanceof HTMLElement) {
+    let parent = event.target.parentElement;
+    if (parent !== null) {
+      let template = (parent.querySelector("template") as HTMLTemplateElement).content.cloneNode(true);
+      parent.prepend(template);
+    }
+  }
+}
+
+(Array.from(
+  document.querySelectorAll(
+    "form.onboarding > fieldset button.switch_card"
+  )
+) as HTMLButtonElement[])
+  .map(button => {
+    button.addEventListener("click", switchCardOnClick)
+  });
+
+(Array.from(
+  document.querySelectorAll(
+    "form.onboarding > fieldset div.multi_input > button.add_more"
+  )
+) as HTMLButtonElement[])
+  .map(button => {
+    button.addEventListener("click", multiInputAddMore)
+    multiInputAddMore({ target: button });
+  });
+
+const form = document.querySelector("form.onboarding") as HTMLFormElement;
+console.log(form);
+form.onsubmit = async (event: SubmitEvent) => {
+    console.log(event);
+    event.preventDefault();
+    const form = event.target as HTMLFormElement;
+    const json = {
+        user: {
+          type: ["h-card"],
+          properties: {
+            name: [(form.querySelector("#hcard_name") as HTMLInputElement).value],
+            pronoun: (Array.from(
+              form.querySelectorAll("#hcard_pronouns")
+            ) as HTMLInputElement[])
+              .map(input => input.value).filter(i => i != ""),
+            url: (Array.from(form.querySelectorAll("#hcard_url")) as HTMLInputElement[])
+              .map(input => input.value).filter(i => i != ""),
+            note: [(form.querySelector("#hcard_note") as HTMLInputElement).value]
+          }
+        },
+        first_post: {
+          type: ["h-entry"],
+          properties: {
+            content: [(form.querySelector("#first_post_content") as HTMLTextAreaElement).value]
+          }
+        },
+      blog_name: (form.querySelector("#blog_name") as HTMLInputElement).value,
+      feeds: (Array.from(
+        form.querySelectorAll(".multi_input#custom_feeds > fieldset.feed")
+      ) as HTMLElement[])
+        .map(form => {
+          return {
+            name: (form.querySelector("#feed_name") as HTMLInputElement).value,
+            slug: (form.querySelector("#feed_slug") as HTMLInputElement).value
+          }
+        }).filter(feed => feed.name == "" || feed.slug == "")
+    };
+
+    await fetch("/.kittybox/onboarding", {
+        method: "POST",
+        body: JSON.stringify(json),
+        headers: { "Content-Type": "application/json" }
+    }).then(response => {
+        if (response.status == 201) {
+            window.location.href = window.location.href;
+        }
+    })
+}