diff options
-rw-r--r-- | app/auth.js | 8 | ||||
-rw-r--r-- | app/public/api.js | 91 | ||||
-rw-r--r-- | app/public/index.html | 12 | ||||
-rw-r--r-- | app/public/main.js | 186 | ||||
-rw-r--r-- | app/public/note-ponys.html | 102 | ||||
-rw-r--r-- | app/public/ponys.js | 1 | ||||
-rw-r--r-- | app/public/sock.js | 5 | ||||
-rw-r--r-- | app/public/utils.js | 10 |
8 files changed, 282 insertions, 133 deletions
diff --git a/app/auth.js b/app/auth.js index 8a55857..4d5c1cb 100644 --- a/app/auth.js +++ b/app/auth.js @@ -165,7 +165,7 @@ const checkReferer = req => { if (req.headers["referer"] && !req.headers["referer"].includes(DOMAIN)) console.log(Date.now()+" [WARN] Unexpected HTTP Referer: "+req.headers["referer"]); }; -async function login(req, res, match, data) { +async function signIn(req, res, match, data) { const currentToken = parseCookies(req)?.token; if (currentToken || !data.username || !data.password) return err400(res); const error = {success:false, msg:"Bad username or password"}; @@ -206,7 +206,7 @@ async function login(req, res, match, data) { } else return err500(res); } -function logout(req, res) { +function signOut(req, res) { const token = parseCookies(req)?.token; const tokenData = getToken(token); if (tokenData) { @@ -419,8 +419,8 @@ export function authed(fn) { return rateLimit((req, res, ...rest) => { }); } export const attach = (app) => { // TODO make endpoints RESTier? - app.jpost("/login", rateLimit(login)); // {username, password[, keepSession]} -> {success[, msg][, mustChangePassword]} - app.post("/logout", rateLimit(logout)); + app.jpost("/sign-in", rateLimit(signIn)); // {username, password[, keepSession]} -> {success[, msg][, mustChangePassword]} + app.post("/sign-out", rateLimit(signOut)); app.jpost("/change-password", rateLimit(changePassword)); // {password, newPassword[, username[, keepSession]]} -> {success[, msg]} app.jpost("/change-username", authed(changeUsernameReq)); // {newUsername, password} -> {success[, msg]} app.get("/session-list", authed(sessionList)); // -> {active:[{id:<sessionID>, ...}, ...], recent:[...]} diff --git a/app/public/api.js b/app/public/api.js new file mode 100644 index 0000000..6321909 --- /dev/null +++ b/app/public/api.js @@ -0,0 +1,91 @@ +export async function getNote(id) { + const res = await fetch(`${id}`); + const note = await res.json(); + //console.log(note); + return note; +} + +export async function getList() { + const res = await fetch("/list"); + const list = await res.json(); + //console.log(list); + + return Promise.all(list.map(id => getNote(id))); +} + +export 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); +} + +export async function newNote() { + const res = await fetch("/new", { + method: "POST", + }); + + const note = await res.json(); + //console.log(note); + return note; +} + +export async function getUserData() { + const res = await fetch(`/user`); + const user = await res.json(); + //console.log(user); + return user; +} + +export async function getUserSessions() { + const res = await fetch(`/session-list`); + const sessions = await res.json(); + //console.log(sessions); + return sessions; +} + +export async function signIn({username, password, keepSession}) { + const res = await fetch("/sign-in", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({username, password, keepSession}) + }); + + if (res.ok) return res.json(); + return {code: res.status, error: res.statusText}; // TODO +} + +export async function changePassword({username, password, newPassword, keepSession}) { + const res = await fetch("/change-password", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({username, password, newPassword, keepSession}) + }); + + return res.json(); +} + +export async function changeUsername({newUsername, password}) { + const res = await fetch("/change-username", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({newUsername, password}) + }); + + return res.json(); +} + +export async function signOut() { + const res = await fetch("/sign-out", { + method: "POST", + }); +} + +export async function signOutEverywhere() { + const res = await fetch("/deauth-all", { + method: "POST", + }); +} diff --git a/app/public/index.html b/app/public/index.html index 593e6b3..d900888 100644 --- a/app/public/index.html +++ b/app/public/index.html @@ -89,8 +89,8 @@ <input type="text" id="new-username" name="newUsername" placeholder="New username" autocomplete="none" autocorrect="off" autocapitalize="off" spellcheck="false"> <button id="change-username">Change username</button> <br> - <button id="log-out">Log out</button> - <button id="log-out-everywhere">Log out everywhere</button> + <button id="sign-out">Sign out</button> + <button id="sign-out-everywhere">Sign out everywhere</button> <br> <!--input type="text" inputmode="search" id="search" name="search" placeholder="Search…" list="search-hints"--> <input type="search" id="search" name="search" placeholder="Search…" list="search-hints" incremental> @@ -110,7 +110,11 @@ <button class="toolbar-fab" id="add-btn"></button> </div> - <!--script src="sock.js"></script--> - <script src="main.js"></script> + <script type="module"> + import Ponys from "./ponys.js"; + Ponys.defineAll(); + </script> + <template name="note-card" src="./note-ponys.html"></template> + <script type="module" src="main.js"></script> </body> </html> diff --git a/app/public/main.js b/app/public/main.js index 2a30c45..a9119b4 100644 --- a/app/public/main.js +++ b/app/public/main.js @@ -1,4 +1,7 @@ -document.addEventListener("DOMContentLoaded", async () => { "use strict"; +import { getNote, getList, saveNote, newNote, getUserData, getUserSessions, + signIn, changePassword, changeUsername, signOut, signOutEverywhere } from "./api.js"; + +document.addEventListener("DOMContentLoaded", async () => { 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} @@ -27,79 +30,37 @@ function debounce(fn, delay) { // .then(() => console.log("Service worker registered")); //} -function drawNote(note) { - const el = m(`<div class="card-shadow"><div class="card"> - <!--h3>${note.id}</h3--> - <textarea>${note.content.trim()}</textarea> - </div></div>`); - - 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); +//console.log("outside", $("note-card").textContent); +//$("note-card").addEventListener("edit", () => console.log("edit event fired")); +function drawNote(note) { + //const el = m(`<div class="card-shadow"><div class="card"> + // <!--h3>${note.id}</h3--> + // <textarea>${note.content.trim()}</textarea> + //</div></div>`); + + //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); + + const el = m(`<note-card data-id="${note.id}">${note.content.trim()}</note-card>`); $("#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); -} +$("#add-btn").addEventListener("click", async () => { + drawNote(await newNote()); +}); function showSignIn() { const hash = location.hash.slice(2).replace(/^sign-in(\/to\/)?/,""); @@ -131,32 +92,27 @@ $("#sign-in-basic-go").addEventListener("click", async e => { 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, - }) + const res = await signIn({ + 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; + if (res.error) { + $("#sign-in-error").textContent = res.code+" "+res.error; return; } - const json = await res.json(); - console.log(json); + console.log(res); - if (!json.success) { - $("#sign-in-error").textContent = json.msg; + if (!res.success) { + $("#sign-in-error").textContent = res.msg; return; } - if (json.mustChangePassword) { + if (res.mustChangePassword) { alert("Must change password (TODO)"); // TODO return; @@ -167,66 +123,54 @@ $("#sign-in-basic-go").addEventListener("click", async e => { // TODO load location getUserData(); - getList(); + const noteList = await getList(); + for (const note of noteList) drawNote(note); $("#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 res = await changePassword({ + username: $("#username").value, // TODO + password: $("#password").value, // TODO + newPassword: $("#new-password").value, + keepSession: $("#keep-session").checked, }); - const json = await res.json(); - console.log(json); + console.log(res); 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 res = await changeUsername({ + newUsername: $("#new-username").value, + password: $("#password").value, // TODO }); - const json = await res.json(); - console.log(json); + console.log(res); 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-out").addEventListener("click", async () => { + await signOut(); + console.log("Signed out"); $("#sign-in-dialog").showModal(); }); -$("#log-out-everywhere").addEventListener("click", async () => { - const res = await fetch("/deauth-all", { - method: "POST", - }); - - console.log("Logged out everywhere"); +$("#sign-out-everywhere").addEventListener("click", async () => { + signOutEverywhere(); + console.log("Signed out everywhere"); showSignIn(); }); -try { await getUserData(); await getList(); await getUserSessions(); } catch(e) { showSignIn(); } +try { + await getUserData(); + const noteList = await getList(); + for (const note of noteList) drawNote(note); + await getUserSessions(); +} catch(e) { showSignIn(); } (() => { let prevScroll = 0; diff --git a/app/public/note-ponys.html b/app/public/note-ponys.html new file mode 100644 index 0000000..bcbe7ed --- /dev/null +++ b/app/public/note-ponys.html @@ -0,0 +1,102 @@ +<div class="card-shadow"> + <div class="card"> + <!--h3>${note.id}</h3--> + <textarea rows=1></textarea> + </div> +</div> + +<script setup> +import { debounce } from "./utils.js"; +import { saveNote } from "./api.js"; +export default class extends HTMLElement { + #resizeFn; + + connectedCallback() { + const ta = this.$("textarea"); + ta.value = this.textContent; + + ta.addEventListener("input", debounce(() => { // TODO ensure good debounce behavior + saveNote(this.dataset.id, ta.value); + }, 500)); + + //ta.addEventListener("input", () => this.dispatchEvent(new Event("edit"))); + + this.#resizeFn = () => this.resizeTextarea(); + ta.addEventListener("input", this.#resizeFn); + ta.addEventListener("focus", this.#resizeFn); + window.addEventListener("resize", this.#resizeFn); + setTimeout(this.#resizeFn); + } + + disconnectedCallback() { + window.removeEventListener("resize", this.#resizeFn); + } + + resizeTextarea() { // TODO simplify? + const cs = this.$(".card-shadow"); + const ta = this.$("textarea"); + cs.style.height = (cs.scrollHeight) + "px"; + ta.style.height = ""; + ta.style.height = (ta.scrollHeight) + "px"; + cs.style.height = ""; + } +} +</script> + +<style> +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} +textarea { + font-family: inherit; +} +/* TODO :target? */ +.card-shadow { + position: relative; + margin: 16px auto; + max-width: 5.5in; + border-radius: 24px; + box-shadow: 0 2px 16px rgba(0,0,0,.05), 0 2px 4px rgba(0,0,0,.1); + transition: box-shadow .2s; +} +.card-shadow:hover, +.card-shadow:focus-within { + box-shadow: 0 2px 16px rgba(0,0,0,.15), 0 2px 4px rgba(0,0,0,.15); +} +.card-shadow::after { + content: ""; + position: absolute; + inset: 0; + box-shadow: 0 0 0 1px rgba(255,255,255,.2) inset; + border-radius: 24px; + pointer-events: none; + transition: box-shadow .2s; +} +.card-shadow:hover::after, +.card-shadow:focus-within::after { + box-shadow: 0 0 0 2px rgba(255,255,255,.25) inset; +} +.card { + /*padding-bottom: 16px;*/ + border-radius: 24px; + background-color: var(--bg-color); + color: var(--text-color); + overflow: hidden; +} + +.card > textarea { /* TODO */ + display: block; + width: 100%; + padding: 16px 24px; + border: none; + resize: none; + outline: none; + font-size: 16px; + line-height: 24px; + letter-spacing: .3px; + background: transparent; + color: inherit; +} +</style> diff --git a/app/public/ponys.js b/app/public/ponys.js new file mode 100644 index 0000000..adc54ab --- /dev/null +++ b/app/public/ponys.js @@ -0,0 +1 @@ +/* ponys v0.3.6 */export default class{static define(t,r,n,o=""){if(!r.content){let e=document.createElement("template");e.innerHTML=r,r=e}let l=(r=r.content).querySelector("script[setup]")||r.querySelector("script");return import("data:text/javascript;base64,"+btoa(l?.text?.replace(/(?<=(import|from)\s*?("|'))\.{0,2}\/.*?[^\\](?=\2)/g,(e=>new URL(e,new URL(o,location.origin)))))).then((o=>{l?.remove();class c extends(o.default||HTMLElement){constructor(){super();let t=this;try{t=t.attachShadow({mode:"open"})}catch{}this.$=e=>t.querySelector(e),this.$$=e=>t.querySelectorAll(e);let n=r.cloneNode(!0);e(this,n),t.append(n)}}return customElements.define(t,c,n),c}))}static defineAll(e=document){return Promise.allSettled([...e.querySelectorAll("template[name]")].map((e=>{let t={};for(let{name:r,value:n}of e.attributes)t[r]=n;return t.src?this.import(t.name,t.src,t):this.define(t.name,e,t)})))}static import(e,t,r){return fetch(t).then((e=>e.ok?e.text():Promise.reject(Error(t)))).then((n=>this.define(e,n,r,t)))}}function e(t,r){for(let n of r.children)n.host=t,n.$=t.$,n.$$=t.$$,e(t,n)} diff --git a/app/public/sock.js b/app/public/sock.js index b4905e5..72e00ec 100644 --- a/app/public/sock.js +++ b/app/public/sock.js @@ -1,4 +1,3 @@ -const sock = (() => { "use strict"; const refresh = () => setTimeout(() => location.reload(), 1e3); let ws, pingTimeout; @@ -12,7 +11,7 @@ const handlers = { }], }; -const sock = { +export const sock = { init: url => { ws = new WebSocket(url); ws.addEventListener("close", refresh); @@ -39,5 +38,3 @@ const sock = { else prequeue.push([type, data]); }, }; - -return sock })(); diff --git a/app/public/utils.js b/app/public/utils.js new file mode 100644 index 0000000..2e08012 --- /dev/null +++ b/app/public/utils.js @@ -0,0 +1,10 @@ +export function debounce(fn, delay) { + let timeout = null; + return (...args) => { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => { + timeout = null; + fn.apply(null, args); + }, delay||500); + } +} |