document.addEventListener("DOMContentLoaded", async () => { "use strict"; const secure = location.protocol === "https:"; 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); } } //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 element? });