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
+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>