diff options
author | Vika <vika@fireburn.ru> | 2022-10-02 11:54:30 +0300 |
---|---|---|
committer | Vika <vika@fireburn.ru> | 2022-10-02 11:54:30 +0300 |
commit | 568f98589b6c30bb3f807517d28039b12dd54be3 (patch) | |
tree | 885f918885b246d3760cf3b1f2a252b88485f358 /kittybox-rs/companion-lite/src/main.ts | |
parent | b7d4e5c4686bc8aac41d832567190002542a1743 (diff) | |
download | kittybox-568f98589b6c30bb3f807517d28039b12dd54be3.tar.zst |
companion-lite: rewrite to use IndieAuth
This is a naive implementation that doesn't have some security checks. It's ok tho, should work fine... can refine it later
Diffstat (limited to 'kittybox-rs/companion-lite/src/main.ts')
-rw-r--r-- | kittybox-rs/companion-lite/src/main.ts | 213 |
1 files changed, 153 insertions, 60 deletions
diff --git a/kittybox-rs/companion-lite/src/main.ts b/kittybox-rs/companion-lite/src/main.ts index d593188..f45cb95 100644 --- a/kittybox-rs/companion-lite/src/main.ts +++ b/kittybox-rs/companion-lite/src/main.ts @@ -1,16 +1,30 @@ -import { query_channels, submit, MicropubChannel, MF2 } from "./micropub_api.js"; +import { Micropub, MicropubChannel, MF2 } from "./micropub_api.js"; -function get_token() { - return (form.elements.namedItem("access_token") as HTMLInputElement).value -} - -const form = document.getElementById("micropub") as HTMLFormElement; const channel_select_radio = document.getElementById("select_channels") as HTMLInputElement; - channel_select_radio.onclick = async () => { - const channels = await query_channels(form.action, get_token()) - if (channels !== undefined) { - populate_channel_list(channels) + function populate_channel_list(channels: MicropubChannel[]) { + (document.getElementById("channels") as HTMLElement).style.display = "block"; + const channel_list = document.getElementById("channels_target") as HTMLElement; + channel_list.innerHTML = ""; + channels.forEach((channel) => { + const template = (document.getElementById("channel_selector") as HTMLTemplateElement).content.cloneNode(true) as HTMLElement; + const input = template.querySelector("input") as HTMLInputElement; + const label = template.querySelector("label") as HTMLLabelElement; + input.id = `channel_selector_option_${channel.uid}` + input.value = channel.uid + label.htmlFor = input.id + label.innerHTML = `<a href="${channel.uid}">${channel.name}</a>` + + channel_list.appendChild(template) + }) + } + + if (micropub == null) { + throw new Error("need to authenticate first"); + } + const config = await micropub.config(); + if (config.channels !== undefined) { + populate_channel_list(config.channels) } } @@ -21,65 +35,144 @@ no_channel_radio.onclick = () => { channel_list.innerHTML = ""; } -function construct_body(form: HTMLFormElement): MF2 { - let content = (form.elements.namedItem("content") as HTMLInputElement).value; - let name: string | undefined = (form.elements.namedItem("name") as HTMLInputElement).value || undefined; - let category: string[] = (form.elements.namedItem("category") as HTMLInputElement).value - .split(",") - .map(val => val.trim()); - - let channel: string[] | undefined = undefined; - let channel_select = (form.elements.namedItem("channel_select") as HTMLInputElement).value; - if (channel_select) { - let channel_selector = form.elements.namedItem("channel"); - if (channel_selector instanceof RadioNodeList) { - channel = (Array.from(channel_selector) as HTMLInputElement[]) - .map(i => i.checked ? i.value : false) - .filter(i => i) as string[]; - } else if (channel_selector instanceof HTMLInputElement) { - channel = [channel_selector.value] +const main_form = document.getElementById("micropub") as HTMLFormElement; +main_form.onsubmit = async (event) => { + function construct_body(form: HTMLFormElement): MF2 { + let content = (form.elements.namedItem("content") as HTMLInputElement).value; + let name: string | undefined = (form.elements.namedItem("name") as HTMLInputElement).value || undefined; + let category: string[] = (form.elements.namedItem("category") as HTMLInputElement).value + .split(",") + .map(val => val.trim()); + + let channel: string[] | undefined = undefined; + let channel_select = (form.elements.namedItem("channel_select") as HTMLInputElement).value; + if (channel_select) { + let channel_selector = form.elements.namedItem("channel"); + if (channel_selector instanceof RadioNodeList) { + channel = (Array.from(channel_selector) as HTMLInputElement[]) + .map(i => i.checked ? i.value : false) + .filter(i => i) as string[]; + } else if (channel_selector instanceof HTMLInputElement) { + channel = [channel_selector.value] + } } - } - return { - type: ["h-entry"], - properties: { - content: [content], - name: name ? [name] : undefined, - category: category.length ? category : undefined, - channel: channel ? channel : undefined + return { + type: ["h-entry"], + properties: { + content: [content], + name: name ? [name] : undefined, + category: category.length ? category : undefined, + channel: channel ? channel : undefined + } } } -} -function populate_channel_list(channels: MicropubChannel[]) { - (document.getElementById("channels") as HTMLElement).style.display = "block"; - const channel_list = document.getElementById("channels_target") as HTMLElement; - channel_list.innerHTML = ""; - channels.forEach((channel) => { - const template = (document.getElementById("channel_selector") as HTMLTemplateElement).content.cloneNode(true) as HTMLElement; - const input = template.querySelector("input") as HTMLInputElement; - const label = template.querySelector("label") as HTMLLabelElement; - input.id = `channel_selector_option_${channel.uid}` - input.value = channel.uid - label.htmlFor = input.id - label.innerHTML = `<a href="${channel.uid}">${channel.name}</a>` - - channel_list.appendChild(template) - }) -} - -form.onsubmit = async (event) => { event.preventDefault() - const mf2 = construct_body(form); + const mf2 = construct_body(main_form); console.log(JSON.stringify(mf2)); + if (micropub == null) { + throw new Error("need to authenticate first"); + } try { - submit(form.action, get_token(), mf2) - } catch (e) { - // TODO show errors to user + const location = await micropub.submit(mf2); + main_form.clear() + window.open(location, "_blank") + } catch (e) { + console.error(e) + alert(`Error: ${e}`) return } - form.clear() + +} + +const indieauth_form = document.getElementById("indieauth") as HTMLFormElement; +indieauth_form.onsubmit = async (event) => { + event.preventDefault() + const form = event.target as HTMLFormElement; + const me = (form.elements.namedItem("me") as HTMLInputElement).value; + if (me != null) { + const { discover_endpoints, create_verifier, create_challenge } = await import("./indieauth.js"); + + const endpoints = await discover_endpoints(new URL(me)); + + if (endpoints != null) { + localStorage.setItem("micropub_endpoint", endpoints.micropub.toString()) + localStorage.setItem("token_endpoint", endpoints.token_endpoint.toString()) + if (endpoints.revocation_endpoint != null) { + localStorage.setItem("revocation_endpoint", endpoints.revocation_endpoint.toString()) + } + } else { + alert("Your website doesn't support Micropub.") + return + } + (document.getElementById("unauthorized") as HTMLElement).style.display = "none"; + (document.getElementById("authorizing") as HTMLElement).style.display = "block"; + const url = endpoints.authorization_endpoint; + let params = new URLSearchParams(); + for (const [key, val] of url.searchParams) { + params.append(key, val) + } + params.set("client_id", window.location.href) + params.set("redirect_uri", window.location.href) + params.set("response_type", "code") + params.set("scope", "profile create media") + params.set("state", "awoo") + const code_verifier = create_verifier() + localStorage.setItem("code_verifier", code_verifier) + params.set("code_challenge", await create_challenge(code_verifier)) + params.set("code_challenge_method", "S256") + + url.search = "?" + params.toString() + + console.log(url) + + window.location.href = url.toString() + } } -(document.getElementById("authorized") as HTMLElement).style.display = "block"; +if (window.location.search != "") { + (document.getElementById("authorizing") as HTMLElement).style.display = "block"; + const params = new URLSearchParams(window.location.search) + if (params.has("code") && params.has("state")) { + const token_endpoint = new URL(localStorage.getItem("token_endpoint")!) + const state = params.get("state") + // XXX check state + + const client_id = new URL(window.location.href); + client_id.search = ""; + const form = new URLSearchParams(); + form.set("grant_type", "authorization_code") + form.set("code", params.get("code")!) + form.set("client_id", client_id.toString()) + form.set("redirect_uri", client_id.toString()) + form.set("code_verifier", localStorage.getItem("code_verifier")!) + + const response = await fetch(token_endpoint, { + method: "POST", + headers: { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + }, + body: form.toString() + }); + + const grant = await response.json(); + + if ("access_token" in grant) { + localStorage.setItem("access_token", grant.access_token); + (document.getElementById("authorizing") as HTMLElement).style.display = "none"; + } + } +} + +let micropub: Micropub | null = null; +const token = localStorage.getItem("access_token") +const endpoint = localStorage.getItem("micropub_endpoint") +if (token == null || endpoint == null) { + (document.getElementById("unauthorized") as HTMLElement).style.display = "block"; +} else { + (document.getElementById("authorized") as HTMLElement).style.display = "block"; + + micropub = new Micropub({ endpoint: new URL(endpoint), token }); +} |