feature: the beginnings of chat working
This commit is contained in:
parent
85024c6e62
commit
649ff432bb
24 changed files with 1555 additions and 918 deletions
33
public/js/api.js
Normal file
33
public/js/api.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
const api = {
|
||||
fetch: async function (url, options = { method: "GET" }) {
|
||||
const session_id = (document.cookie.match(
|
||||
/^(?:.*;)?\s*session_id\s*=\s*([^;]+)(?:.*)?$/,
|
||||
) || [, null])[1];
|
||||
|
||||
// TODO: this wasn't really intended to be persisted in a cookie
|
||||
const session_secret = (document.cookie.match(
|
||||
/^(?:.*;)?\s*session_secret\s*=\s*([^;]+)(?:.*)?$/,
|
||||
) || [, null])[1];
|
||||
|
||||
const headers = {
|
||||
Accept: "application/json",
|
||||
"x-session_id": session_id,
|
||||
"x-totp": await otp_totp(session_secret),
|
||||
...(options.headers ?? {}),
|
||||
};
|
||||
|
||||
const fetch_options = {
|
||||
method: options.method,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (options.json) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
fetch_options.body = JSON.stringify(options.json);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api${url}`, fetch_options);
|
||||
|
||||
return response;
|
||||
},
|
||||
};
|
19
public/js/datetimeutils.js
Normal file
19
public/js/datetimeutils.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
function datetime_to_local(input) {
|
||||
const local_datetime = new Date(input);
|
||||
|
||||
return {
|
||||
long: local_datetime.toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
}),
|
||||
|
||||
short: local_datetime.toLocaleString("en-US", {
|
||||
timeStyle: "short",
|
||||
hour12: true,
|
||||
}),
|
||||
};
|
||||
}
|
22
public/js/locationchange.js
Normal file
22
public/js/locationchange.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
// https://stackoverflow.com/questions/6390341/how-to-detect-if-url-has-changed-after-hash-in-javascript
|
||||
(() => {
|
||||
let oldPushState = history.pushState;
|
||||
history.pushState = function pushState() {
|
||||
let ret = oldPushState.apply(this, arguments);
|
||||
window.dispatchEvent(new Event("pushstate"));
|
||||
window.dispatchEvent(new Event("locationchange"));
|
||||
return ret;
|
||||
};
|
||||
|
||||
let oldReplaceState = history.replaceState;
|
||||
history.replaceState = function replaceState() {
|
||||
let ret = oldReplaceState.apply(this, arguments);
|
||||
window.dispatchEvent(new Event("replacestate"));
|
||||
window.dispatchEvent(new Event("locationchange"));
|
||||
return ret;
|
||||
};
|
||||
|
||||
window.addEventListener("popstate", () => {
|
||||
window.dispatchEvent(new Event("locationchange"));
|
||||
});
|
||||
})();
|
65
public/js/smartforms.js
Normal file
65
public/js/smartforms.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
document.addEventListener("DOMContentLoaded", () => {
|
||||
/* make all forms semi-smart */
|
||||
const forms = document.querySelectorAll("form");
|
||||
for (const form of forms) {
|
||||
const script = form.querySelector("script");
|
||||
|
||||
form.onsubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const form_data = new FormData(form);
|
||||
const body = {};
|
||||
for (const [key, value] of form_data.entries()) {
|
||||
const elements = key.split(".");
|
||||
let current = body;
|
||||
for (const element of elements.slice(0, elements.length - 1)) {
|
||||
current[element] = current[element] ?? {};
|
||||
current = current[element];
|
||||
}
|
||||
|
||||
current[elements.slice(elements.length - 1).shift()] = value;
|
||||
}
|
||||
|
||||
const url = form.action;
|
||||
|
||||
try {
|
||||
// TODO: send session header
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error_body = await response.json();
|
||||
const error = error_body?.error;
|
||||
|
||||
if (form.on_error) {
|
||||
return form.on_error(error);
|
||||
}
|
||||
|
||||
alert(error.message ?? "Unknown error!");
|
||||
return;
|
||||
}
|
||||
|
||||
const response_body = await response.json();
|
||||
if (form.on_response) {
|
||||
return form.on_response(response_body);
|
||||
}
|
||||
} catch (error) {
|
||||
console.dir({
|
||||
error,
|
||||
});
|
||||
|
||||
if (form.onerror) {
|
||||
return form.onerror(error);
|
||||
}
|
||||
|
||||
alert(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
42
public/js/totp.js
Normal file
42
public/js/totp.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/* https://github.com/turistu/totp-in-javascript/blob/main/totp.js */
|
||||
|
||||
async function otp_totp(key, secs = 30, digits = 6) {
|
||||
return otp_hotp(otp_unbase32(key), otp_pack64bu(Date.now() / 1000 / secs), digits);
|
||||
}
|
||||
async function otp_hotp(key, counter, digits) {
|
||||
let y = self.crypto.subtle;
|
||||
if (!y) throw Error("no self.crypto.subtle object available");
|
||||
let k = await y.importKey("raw", key, { name: "HMAC", hash: "SHA-1" }, false, ["sign"]);
|
||||
return otp_hotp_truncate(await y.sign("HMAC", k, counter), digits);
|
||||
}
|
||||
function otp_hotp_truncate(buf, digits) {
|
||||
let a = new Uint8Array(buf),
|
||||
i = a[19] & 0xf;
|
||||
return otp_fmt(
|
||||
10,
|
||||
digits,
|
||||
(((a[i] & 0x7f) << 24) | (a[i + 1] << 16) | (a[i + 2] << 8) | a[i + 3]) % 10 ** digits,
|
||||
);
|
||||
}
|
||||
|
||||
function otp_fmt(base, width, num) {
|
||||
return num.toString(base).padStart(width, "0");
|
||||
}
|
||||
function otp_unbase32(s) {
|
||||
let t = (s.toLowerCase().match(/\S/g) || [])
|
||||
.map((c) => {
|
||||
let i = "abcdefghijklmnopqrstuvwxyz234567".indexOf(c);
|
||||
if (i < 0) throw Error(`bad char '${c}' in key`);
|
||||
return otp_fmt(2, 5, i);
|
||||
})
|
||||
.join("");
|
||||
if (t.length < 8) throw Error("key too short");
|
||||
return new Uint8Array(t.match(/.{8}/g).map((d) => parseInt(d, 2)));
|
||||
}
|
||||
function otp_pack64bu(v) {
|
||||
let b = new ArrayBuffer(8),
|
||||
d = new DataView(b);
|
||||
d.setUint32(0, v / 2 ** 32);
|
||||
d.setUint32(4, v);
|
||||
return b;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue