Initial import: KeepNote service (dcntrlized-auth consumer)

This commit is contained in:
2026-08-14 23:35:38 +01:00
commit 77dfcaf64a
49 changed files with 2346 additions and 0 deletions
+75
View File
@@ -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>
+21
View File
@@ -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 "";
}
+23
View File
@@ -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
View File
@@ -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
}
}
+74
View File
@@ -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
View File
@@ -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>