Initial import: KeepNote service (dcntrlized-auth consumer)
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Log In / Register</title>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<script src="./cookies.js"></script>
|
||||
<script src="./jwt.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>KeepNote</h1>
|
||||
<div id="error"></div>
|
||||
<form id="login">
|
||||
<h2>Log in</h2>
|
||||
<input name="username" type="text" placeholder="username" />
|
||||
<input name="password" type="password" placeholder="password" />
|
||||
<button>Log In</button>
|
||||
</form>
|
||||
<form id="register">
|
||||
<h2>Register</h2>
|
||||
<input name="username" type="text" placeholder="username" />
|
||||
<input name="email" type="text" placeholder="email" />
|
||||
<input name="password" type="password" placeholder="password" />
|
||||
<button>Register</button>
|
||||
</form>
|
||||
<script>
|
||||
const API = "http://127.0.0.1:8081";
|
||||
const errorBox = document.getElementById("error");
|
||||
|
||||
function showError(msg) {
|
||||
errorBox.textContent = msg;
|
||||
}
|
||||
|
||||
async function loginAction(event) {
|
||||
event.preventDefault();
|
||||
showError("");
|
||||
const data = Object.fromEntries(new FormData(event.target).entries());
|
||||
const res = await fetch(`${API}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!res.ok) {
|
||||
showError(`login failed: ${res.status} ${await res.text()}`);
|
||||
return;
|
||||
}
|
||||
const resp = await res.json();
|
||||
cookieBaker("accessToken", resp.accessToken, new Date(new Date().getTime() + 10 * 60000), "/", null, null, null, null);
|
||||
window.location.replace("./notes.html");
|
||||
}
|
||||
|
||||
async function registerAction(event) {
|
||||
event.preventDefault();
|
||||
showError("");
|
||||
const data = Object.fromEntries(new FormData(event.target).entries());
|
||||
const res = await fetch(`${API}/auth/register`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!res.ok) {
|
||||
showError(`register failed: ${res.status} ${await res.text()}`);
|
||||
return;
|
||||
}
|
||||
const resp = await res.json();
|
||||
cookieBaker("accessToken", resp.accessToken, new Date(new Date().getTime() + 10 * 60000), "/", null, null, null, null);
|
||||
window.location.replace("./notes.html");
|
||||
}
|
||||
|
||||
document.getElementById("login").addEventListener("submit", loginAction);
|
||||
document.getElementById("register").addEventListener("submit", registerAction);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
function cookieBaker(key, value, expiry, path, secure, httpOnly, domain, sameSite) {
|
||||
// Fuck secure parsing of the values real devs create cookies with unparsed values
|
||||
document.cookie = `${key}=${value};${expiry ? `Expires=${expiry};` : ''} ${path ? `Path=${path};` : ''} Domain=${domain ? `${domain};` : ''} ${secure ? 'Secure;' : ''} ${httpOnly ? 'HttpOnly;' : ''} ${sameSite ? `SameSite=${sameSite};` : ''}`
|
||||
}
|
||||
|
||||
function cookieEater(key) {
|
||||
// https://www.w3schools.com/js/js_cookies.asp
|
||||
let name = key + "=";
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Index</title>
|
||||
<script src="./cookies.js"></script>
|
||||
<script src="./jwt.js"></script>
|
||||
<script>
|
||||
const token = cookieEater("accessToken");
|
||||
let authed = false;
|
||||
try {
|
||||
if (token) {
|
||||
const res = jwtDecode(token);
|
||||
authed = new Date().valueOf() / 1000 < res.payload.exp;
|
||||
}
|
||||
} catch (e) { /* stale or malformed token */ }
|
||||
window.location.replace(authed ? "./notes.html" : "./auth.html");
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
function jwtDecode(jwt) {
|
||||
// Source: https://inventivehq.com/blog/how-to-decode-a-jwt
|
||||
|
||||
const parts = jwt.split('.');
|
||||
const header = parts[0];
|
||||
const payload = parts[1];
|
||||
const signature = parts[2];
|
||||
// Base64URL to Base64
|
||||
function base64UrlToBase64(str) {
|
||||
return str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
}
|
||||
|
||||
// Decode
|
||||
const decodedHeader = JSON.parse(atob(base64UrlToBase64(header)));
|
||||
const decodedPayload = JSON.parse(atob(base64UrlToBase64(payload)));
|
||||
|
||||
return {
|
||||
raw: jwt,
|
||||
header: decodedHeader,
|
||||
payload: decodedPayload,
|
||||
signature: signature
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
margin: 2rem auto;
|
||||
max-width: 40rem;
|
||||
padding: 0 1rem;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
max-width: 20rem;
|
||||
}
|
||||
|
||||
form h2 {
|
||||
margin: 0.5rem 0 0;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.4rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#error {
|
||||
color: #b00020;
|
||||
min-height: 1.2em;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#notes li {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: #eee;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.1rem 0.5rem;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.edit-note {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.edit-note input {
|
||||
flex: 1;
|
||||
min-width: 8rem;
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Notes</title>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<script src="./cookies.js"></script>
|
||||
<script src="./jwt.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>KeepNote</h1>
|
||||
<div id="toolbar">
|
||||
<button id="logout">Log out</button>
|
||||
</div>
|
||||
<form id="create-note">
|
||||
<input name="title" type="text" placeholder="title" required />
|
||||
<input name="content" type="text" placeholder="content" />
|
||||
<button>Add note</button>
|
||||
</form>
|
||||
<div id="error"></div>
|
||||
<ul id="notes"></ul>
|
||||
<script>
|
||||
const API = "http://127.0.0.1:8081";
|
||||
const errorBox = document.getElementById("error");
|
||||
let editingId = null;
|
||||
|
||||
function token() {
|
||||
return cookieEater("accessToken");
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
errorBox.textContent = msg;
|
||||
}
|
||||
|
||||
async function api(path, options) {
|
||||
options = options || {};
|
||||
options.headers = Object.assign({ "X-Access-Token": token() }, options.headers || {});
|
||||
const res = await fetch(`${API}${path}`, options);
|
||||
if (res.status === 401) {
|
||||
window.location.replace("./auth.html");
|
||||
throw new Error("unauthorized");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function renderNote(note) {
|
||||
const li = document.createElement("li");
|
||||
if (editingId === note.id) {
|
||||
const form = document.createElement("form");
|
||||
form.className = "edit-note";
|
||||
const title = document.createElement("input");
|
||||
title.name = "title";
|
||||
title.value = note.title || "";
|
||||
const content = document.createElement("input");
|
||||
content.name = "content";
|
||||
content.value = note.content || "";
|
||||
const tags = document.createElement("input");
|
||||
tags.name = "tags";
|
||||
tags.value = (note.tags || []).join(", ");
|
||||
tags.placeholder = "tags (comma separated)";
|
||||
const save = document.createElement("button");
|
||||
save.textContent = "Save";
|
||||
const cancel = document.createElement("button");
|
||||
cancel.type = "button";
|
||||
cancel.textContent = "Cancel";
|
||||
cancel.addEventListener("click", () => { editingId = null; loadNotes(); });
|
||||
form.append(title, content, tags, save, cancel);
|
||||
form.addEventListener("submit", (event) => updateNote(event, note.id, title, content, tags));
|
||||
li.appendChild(form);
|
||||
return li;
|
||||
}
|
||||
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = note.title || "(untitled)";
|
||||
li.appendChild(title);
|
||||
if (note.content) {
|
||||
li.appendChild(document.createTextNode(" - " + note.content));
|
||||
}
|
||||
for (const tag of note.tags || []) {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "tag";
|
||||
badge.textContent = tag;
|
||||
li.appendChild(badge);
|
||||
}
|
||||
const edit = document.createElement("button");
|
||||
edit.textContent = "Edit";
|
||||
edit.addEventListener("click", () => { editingId = note.id; loadNotes(); });
|
||||
li.appendChild(edit);
|
||||
const del = document.createElement("button");
|
||||
del.textContent = "Delete";
|
||||
del.addEventListener("click", () => deleteNote(note.id));
|
||||
li.appendChild(del);
|
||||
return li;
|
||||
}
|
||||
|
||||
async function loadNotes() {
|
||||
const res = await api("/action/getNotes");
|
||||
if (!res.ok) {
|
||||
showError(`failed to load notes: ${res.status} ${await res.text()}`);
|
||||
return;
|
||||
}
|
||||
const notes = await res.json();
|
||||
const list = document.getElementById("notes");
|
||||
list.innerHTML = "";
|
||||
for (const note of notes) {
|
||||
list.appendChild(renderNote(note));
|
||||
}
|
||||
}
|
||||
|
||||
async function createNote(event) {
|
||||
event.preventDefault();
|
||||
showError("");
|
||||
const data = Object.fromEntries(new FormData(event.target).entries());
|
||||
const res = await api("/action/writeNote", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ payload: data })
|
||||
});
|
||||
if (!res.ok) {
|
||||
showError(`failed to add note: ${res.status} ${await res.text()}`);
|
||||
return;
|
||||
}
|
||||
event.target.reset();
|
||||
await loadNotes();
|
||||
}
|
||||
|
||||
async function updateNote(event, id, titleInput, contentInput, tagsInput) {
|
||||
event.preventDefault();
|
||||
showError("");
|
||||
const tags = tagsInput.value.split(",").map((t) => t.trim()).filter((t) => t !== "");
|
||||
const res = await api("/action/updateNote", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ payload: { id: id, title: titleInput.value, content: contentInput.value, tags: tags } })
|
||||
});
|
||||
if (!res.ok) {
|
||||
showError(`failed to update note: ${res.status} ${await res.text()}`);
|
||||
return;
|
||||
}
|
||||
editingId = null;
|
||||
await loadNotes();
|
||||
}
|
||||
|
||||
async function deleteNote(id) {
|
||||
showError("");
|
||||
const res = await api("/action/deleteNote", {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ payload: { id: id } })
|
||||
});
|
||||
if (!res.ok) {
|
||||
showError(`failed to delete note: ${res.status} ${await res.text()}`);
|
||||
return;
|
||||
}
|
||||
await loadNotes();
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await api("/auth/logout");
|
||||
} catch (e) { /* ignore */ }
|
||||
document.cookie = "accessToken=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/";
|
||||
window.location.replace("./auth.html");
|
||||
}
|
||||
|
||||
document.getElementById("create-note").addEventListener("submit", createNote);
|
||||
document.getElementById("logout").addEventListener("click", logout);
|
||||
|
||||
loadNotes();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user