document.addEventListener("DOMContentLoaded", async () => { "use strict";
const $ = (s,c) => (c||document).querySelector(s);
function $$(x,y,z,a){a=(z||document).querySelectorAll(x);if(typeof y=="function")[].forEach.call(a,y);return a}
function m(a,b,c){c=document;b=c.createElement(b||"p");b.innerHTML=a.trim();for(a=c.createDocumentFragment();c=b.firstChild;)a.appendChild(c);return a.firstChild}
function debounce(fn, delay) {
let timeout = null;
return (...args) => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
fn.apply(null, args);
}, delay||500);
}
}
//const secure = location.protocol === "https:";
//sock.init(`ws${secure?"s":""}://${location.host}/ws`);
//sock.on("hello", e => {
// console.log("hello", e);
// sock.send("world", {foo:"bar"});
//});
//if ("serviceWorker" in navigator) {
// navigator.serviceWorker.register("sw.js")
// .then(() => console.log("Service worker registered"));
//}
function drawNote(note) {
const el = m(`
`);
const ta = $("textarea", el)
ta.addEventListener("input",
debounce(() => { saveNote(note.id, ta.value); }, 500));
function resizeTextarea() { // TODO simplify
el.style.height = (el.scrollHeight) + "px";
ta.style.height = ""; ta.style.height = (ta.scrollHeight) + "px";
el.style.height = "";
}
ta.addEventListener("input", resizeTextarea);
ta.addEventListener("focus", resizeTextarea);
window.addEventListener("resize", resizeTextarea);
setTimeout(resizeTextarea);
$("#notes").appendChild(el);
}
async function getNote(id) {
const res = await fetch(`${id}`);
const note = await res.json();
console.log(note);
drawNote(note);
}
async function getList() {
const res = await fetch("/list");
const list = await res.json();
console.log(list);
return Promise.all(list.map(id => getNote(id)));
}
async function saveNote(id, content) {
const res = await fetch("/"+id, {
method: "POST",
body: JSON.stringify({id, content}),
});
const note = await res.json();
console.log(note);
}
async function newNote() {
const res = await fetch("/new", {
method: "POST",
});
const note = await res.json();
console.log(note);
drawNote(note);
}
$("#add-btn").addEventListener("click", newNote);
async function getUserData() {
const res = await fetch(`/user`);
const user = await res.json();
console.log(user);
}
async function getUserSessions() {
const res = await fetch(`/session-list`);
const sessions = await res.json();
console.log(sessions);
}
function showSignIn() {
const hash = location.hash.slice(2).replace(/^sign-in(\/to\/)?/,"");
history.replaceState("sign-in","","#/sign-in"+(hash?"/to/"+hash:""));
$("#sign-in-dialog").showModal();
}
$("#sign-in-dialog").addEventListener("cancel", e => e.preventDefault());
$("#sign-in-username").addEventListener("input", e => e.target.classList.remove("error"));
$("#sign-in-password").addEventListener("input", e => e.target.classList.remove("error"));
$("#sign-in-basic-go").addEventListener("click", async e => {
e.preventDefault();
$("#sign-in-error").textContent = "";
const inputs = [
$("#sign-in-username"),
$("#sign-in-password"),
$("#sign-in-keep-session"),
$("#sign-in-basic-go"),
];
const validatedInputs = inputs.slice(0,2);
if (validatedInputs.map(e => {
if (!e.checkValidity()) {
e.classList.add("error");
return true; }
}).reduce((a,e) => a||e, false)) return;
inputs.forEach(e => e.disabled = true);
$("#sign-in-form").classList.add("loading");
const res = await fetch("/login", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
username: $("#sign-in-username").value,
password: $("#sign-in-password").value,
keepSession: $("#sign-in-keep-session").checked,
})
});
inputs.forEach(e => e.disabled = false);
$("#sign-in-form").classList.remove("loading");
if (!res.ok) {
$("#sign-in-error").textContent = res.status+" "+res.statusText;
return;
}
const json = await res.json();
console.log(json);
if (!json.success) {
$("#sign-in-error").textContent = json.msg;
return;
}
if (json.mustChangePassword) {
alert("Must change password (TODO)");
// TODO
return;
}
const hash = location.hash.slice(9);
history.replaceState("","","#/"+hash);
// TODO load location
getUserData();
getList();
$("#sign-in-dialog").close();
});
$("#change-password").addEventListener("click", async () => {
const res = await fetch("/change-password", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
username: $("#username").value, // TODO
password: $("#password").value, // TODO
newPassword: $("#new-password").value,
keepSession: $("#keep-session").checked,
})
});
const json = await res.json();
console.log(json);
getUserData();
});
$("#change-username").addEventListener("click", async () => {
const res = await fetch("/change-username", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
newUsername: $("#new-username").value,
password: $("#password").value, // TODO
})
});
const json = await res.json();
console.log(json);
getUserData();
});
$("#log-out").addEventListener("click", async () => {
const res = await fetch("/logout", {
method: "POST",
});
//const json = await res.json();
//console.log(json);
console.log("Logged out");
$("#sign-in-dialog").showModal();
});
$("#log-out-everywhere").addEventListener("click", async () => {
const res = await fetch("/deauth-all", {
method: "POST",
});
console.log("Logged out everywhere");
showSignIn();
});
try { await getUserData(); await getList(); await getUserSessions(); } catch(e) { showSignIn(); }
(() => {
let prevScroll = 0;
window.addEventListener("scroll", () => {
document.querySelector("#toolbar").classList[
(window.scrollY && window.scrollY > prevScroll)?"add":"remove"]("hide");
prevScroll = window.scrollY;
});
})();
// TODO make sure to save unsynced delta even if token has expired, then sync when logged in
// TODO Cookie consent on signup; "only necessary for maintaining user session and cached data"
// TODO "Review sessions" popup client-side when relogin required on a token which shouldn't have expired yet
// TODO prevent data: and javascript: links?
// Also embedding of external content?
// encodeURI *AND* encode for attribute (quote marks etc)
// https://github.com/cure53/DOMPurify
//
// elem.textContent = dangerVariable; !!!!!!!!!
// elem.insertAdjacentText(dangerVariable);
// elem.className = dangerVariable;
// elem.setAttribute(safeName, dangerVariable);
// formfield.value = dangerVariable;
// document.createTextNode(dangerVariable);
// document.createElement(dangerVariable);
// elem.innerHTML = DOMPurify.sanitize(dangerVar);
// https://github.com/cure53/DOMPurify/blob/main/src/attrs.js
// https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
// TODO note kebab menu could be a