summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/auth.js8
-rw-r--r--app/public/api.js91
-rw-r--r--app/public/index.html12
-rw-r--r--app/public/main.js186
-rw-r--r--app/public/note-ponys.html102
-rw-r--r--app/public/ponys.js1
-rw-r--r--app/public/sock.js5
-rw-r--r--app/public/utils.js10
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);
+ }
+}