diff options
author | stuebinm | 2021-04-04 15:30:26 +0200 |
---|---|---|
committer | stuebinm | 2021-04-04 15:30:26 +0200 |
commit | 198a580292ee6b1d15d4c7409bd84ca8e57c9ef2 (patch) | |
tree | 1b82a01c3bf7b9ae301ae145229e62fd5f8e0ed4 /site |
simple forms with simple encryption
this depends on age stuffed into web assembly, which is not yet
part of this repository.
The idea is to have a web app (which is a static html page + js / wasm)
and a set of (optionally encrypted) json files which describe surveys,
which the main site can download on demand.
Diffstat (limited to 'site')
-rw-r--r-- | site/index.html | 238 | ||||
-rw-r--r-- | site/style.css | 21 |
2 files changed, 259 insertions, 0 deletions
diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..7fef9e9 --- /dev/null +++ b/site/index.html @@ -0,0 +1,238 @@ +<html> + <head> + <title>test</title> + <link rel="stylesheet" type="text/css" href="style.css"> + </head> + <body> + <div id="root"> + </body> + <script type="module"> + + import init, { age_encrypt, age_decrypt } from "./rage_wasm.js"; + + /// the basic idea here is to have functions which construct parts + /// of the DOM that renders the survey to a user. These functions + /// are all free of side effects (as far as possible, i.e. apart + /// from creating DOM elements), and each returns the root element + /// it just created + a callback function which will be used later + /// to check which values the user entered in the corresponding + /// parts of the survey. + /// In other words, this misuses lots of lambdas to get a kind of + /// polymorphism which would otherwise be hard to get in javascript. + + let root = document.getElementById("root"); + + // appends multiple children to an element. returns the root + // element to be composable + function appendChildren (root, children) { + for (var child of children) { + root.appendChild (child); + } + return root; + } + + function mkElement (kind, text = "") { + let p = document.createElement(kind); + p.innerText = text; + return p; + } + + + /// creates an option with a label next to it. + /// mainly for list-like things, but also the password field + function mkOption (kind, option="", qid="") { + let i = document.createElement("input"); + i.type = kind; + i.name = qid; + i.value = option; + i.id = option; + let l = mkElement("label", option); + l.htmlFor = option; + return [i,l]; + } + + // for all kinds of multiple-choise like answer spaces + function mkListSpace(kind, options, qid) { + let ul = mkElement("ul") + let buttons = options.map((option) => + mkOption(kind, option, qid) + ); + appendChildren (ul, buttons.map((button) => { + let li = mkElement("li"); + appendChildren(li, button); + return li; + })); + // return the answer chosen by the user. This function uses + // null to indicate that a choice was invalid, i.e. the form + // is not ready for submission yet. + let getAnswer = () => { + let selected = buttons + .filter((b) => b[0].checked) + .map((b) => b[0].value); + if (kind === "radio") + return selected.length == 0 ? null : selected[0]; + if (kind === "checkbox") + return selected; + console.err("PartialityError: encountered unknown list type!") + } + return [ul, getAnswer]; + } + + // for freeform text answers (TODO) + function mkFreeform (placeholder) { + let text = mkElement("textarea"); + text.placeholder = placeholder; + return [ + text, + () => text.value + ]; + } + + // essentially a case - of statement over the answerspace type. + // since there is no validation of the config json, it may turn + // out to be a partial function in practice. + function mkAnswerSpace(space, qid) { + if (space === "YesOrNo") { + return mkListSpace("radio", ["yes", "no"], qid); + } if ("Single" in space) { + return mkListSpace("radio", space.Single, qid); + } if ("Multiple" in space) { + return mkListSpace("checkbox", space.Multiple, qid); + } if ("Freeform" in space) { + return mkFreeform(space.Freeform); + } + console.err("PartialityError: encountered unknown AnswerSpace type!"); + } + + // makes a survey from a given json config object + function mkSurvey (survey) { + document.title = survey.title; + // make the header with general information + let header = document.createElement("div"); + appendChildren(header, [ + mkElement("h1", survey.title), + mkElement("p", survey.description) + ]); + root.appendChild(header); + + // map question configs to their callbacks. note that + // this iterator uses a function with side-effects, which + // append the created objects to the DOM survey root. + let callbacks = survey.questions.map ((question) => { + let section = document.createElement("section"); + let [answerspace, whichselection] = + mkAnswerSpace(question.space, question.name); + appendChildren(section, [ + mkElement("h2", question.name), + mkElement("p", question.question), + //mkAnswerSpace(question.space, question.name) + answerspace + ]); + root.appendChild(section); + return whichselection; + }); + + let footer = mkElement("section"); + let submit = mkElement("button", "Submit"); + appendChildren(footer, [ + mkElement("hr"), + submit + ]); + submit.onclick = () => { + // the callback over the complete survey just maps all + // other callbacks to their values, i.e. calls them all + let answers = callbacks.map((c) => c()); + console.log("answers given by user:", answers); + // encrypt things + let byteArray = age_encrypt(JSON.stringify(answers), survey.pubkey); + + let blobData = new Blob( + [byteArray], + { type: 'application/octet-stream' } + ); + + // TODO! + fetch(`http://localhost:8000/upload`, {method:"POST", body:blobData}) + .then(response => console.log(response.text())) + + } + root.appendChild(footer); + + return () => { + return callbacks.map ((c) => c()) + }; + } + + + /// displays a passphrase prompt until the use enters a passphrase + /// which age can use for successful decryption + function askPassphrase (ciphertext, secondTry=false) { + document.title = "Enter Passphrase"; + let div = mkElement("div"); + let button = mkElement("button", "decrypt"); + let [passphrase,label] = mkOption( + "password", + "please enter a passphrase to access the survey:" + ); + passphrase.value = ""; + appendChildren(div, [ + mkElement("h1", "Passphrase"), + appendChildren(mkElement("p"),[label]), + passphrase, + button + ].concat( + secondTry ? [mkElement("p","passphrase was incorrect!")] : [] + )); + root.appendChild(div); + button.onclick = () => { + console.log("trying passphrase ..."); + let decrypted = age_decrypt ( + ciphertext, + passphrase.value + ); + /// if we can't decrypt, the passphrase was wrong + if (decrypted === undefined) { + div.remove(); + askPassphrase (ciphertext, true); + } else { + let survey = JSON.parse(decrypted); + div.remove(); + mkSurvey(survey); + } + }; + } + + async function main () { + // initialise the web assembly parts of this + await init(); + const Http = new XMLHttpRequest(); + const url="http://127.0.0.1:8000/pubcrypt.age"; + Http.open("GET", url); + Http.responseType = "arraybuffer"; + Http.send(); + + Http.onreadystatechange = (e) => { + if (Http.readyState == 4 && Http.status == 200) { + let bytearray = new Uint8Array (Http.response); + let string = String.fromCharCode.apply(null, bytearray); + let survey = null; + try { + survey = JSON.parse(string); + } catch (e) { + console.log ("survey appears to be encrypted"); + askPassphrase(bytearray); + } + /// if the survey was unencrypted, start it here. If it + /// was encrypted, we need to wait for user action via + /// a js callback (handled in askPassphrase). + if (survey !== null) { + mkSurvey(survey); + } + } + } + } + + + main () + </script> +</html> diff --git a/site/style.css b/site/style.css new file mode 100644 index 0000000..026c1cc --- /dev/null +++ b/site/style.css @@ -0,0 +1,21 @@ + + +h1 { + font-size: 30 pt; +} + +html { + background-color: white; +} + +body { + background-color: white; + max-width: 30em; + margin: auto; + padding: 2em; + box-shadow: 0 0 3em lightgray; +} + +li { + list-style-type: none; +} |