diff options
Diffstat (limited to 'app')
| -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 | 178 | ||||
| -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, 278 insertions, 129 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"));  //} +//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 el = m(`<div class="card-shadow"><div class="card"> +  //  <!--h3>${note.id}</h3--> +  //  <textarea>${note.content.trim()}</textarea> +  //</div></div>`); -  const ta = $("textarea", el) +  //const ta = $("textarea", el) -  ta.addEventListener("input", -    debounce(() => { saveNote(note.id, ta.value); }, 500)); +  //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); +  //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); +  } +}  | 
