summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuebinm2021-04-04 15:30:26 +0200
committerstuebinm2021-04-04 15:30:26 +0200
commit198a580292ee6b1d15d4c7409bd84ca8e57c9ef2 (patch)
tree1b82a01c3bf7b9ae301ae145229e62fd5f8e0ed4
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.
-rw-r--r--site/index.html238
-rw-r--r--site/style.css21
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;
+}