This commit is contained in:
@@ -1,48 +0,0 @@
|
|||||||
(function () {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const STORAGE_KEY = "orgBackendApiKey";
|
|
||||||
const HEADER = "X-Org-Api-Key";
|
|
||||||
|
|
||||||
function getStoredKey() {
|
|
||||||
return window.localStorage.getItem(STORAGE_KEY) || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestKey() {
|
|
||||||
const key = window.prompt("API key");
|
|
||||||
if (key && key.trim()) {
|
|
||||||
window.localStorage.setItem(STORAGE_KEY, key.trim());
|
|
||||||
return key.trim();
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function authHeaders(existingHeaders = {}, options = {}) {
|
|
||||||
const headers = new Headers(existingHeaders);
|
|
||||||
let key = getStoredKey();
|
|
||||||
if (!key && options.prompt !== false) {
|
|
||||||
key = requestKey();
|
|
||||||
}
|
|
||||||
if (key) headers.set(HEADER, key);
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authFetch(input, options = {}) {
|
|
||||||
const headers = authHeaders(options.headers, options);
|
|
||||||
const response = await fetch(input, { ...options, headers });
|
|
||||||
if (response.status !== 401) return response;
|
|
||||||
|
|
||||||
window.localStorage.removeItem(STORAGE_KEY);
|
|
||||||
if (options.retry === false) return response;
|
|
||||||
|
|
||||||
const retryHeaders = authHeaders(options.headers, options);
|
|
||||||
if (!retryHeaders.has(HEADER)) return response;
|
|
||||||
return fetch(input, { ...options, headers: retryHeaders, retry: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
window.orgAuth = {
|
|
||||||
fetch: authFetch,
|
|
||||||
headers: authHeaders,
|
|
||||||
clear: () => window.localStorage.removeItem(STORAGE_KEY),
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
102
assets/scripts/auth.js
Normal file
102
assets/scripts/auth.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const TOKEN_KEY = "orgWebJwt";
|
||||||
|
const LOGIN_PATH = "/home/login.html";
|
||||||
|
|
||||||
|
function token() {
|
||||||
|
return window.localStorage.getItem(TOKEN_KEY) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLoginPage() {
|
||||||
|
return window.location.pathname === LOGIN_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loginUrl() {
|
||||||
|
const next = window.location.pathname + window.location.search + window.location.hash;
|
||||||
|
return `${LOGIN_PATH}?next=${encodeURIComponent(next)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectToLogin() {
|
||||||
|
if (!isLoginPage()) {
|
||||||
|
window.location.assign(loginUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function authHeaders(existingHeaders = {}) {
|
||||||
|
const headers = new Headers(existingHeaders);
|
||||||
|
const jwt = token();
|
||||||
|
if (jwt) headers.set("Authorization", `Bearer ${jwt}`);
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authFetch(input, options = {}) {
|
||||||
|
const response = await fetch(input, {
|
||||||
|
...options,
|
||||||
|
headers: authHeaders(options.headers),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
window.localStorage.removeItem(TOKEN_KEY);
|
||||||
|
redirectToLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(username, password) {
|
||||||
|
const response = await fetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || "Login failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem(TOKEN_KEY, data.token);
|
||||||
|
return data.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initLoginForm() {
|
||||||
|
const form = document.getElementById("login-form");
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
const message = document.getElementById("login-message");
|
||||||
|
form.addEventListener("submit", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const submit = form.querySelector("button[type='submit']");
|
||||||
|
if (submit) submit.disabled = true;
|
||||||
|
if (message) message.textContent = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login(form.username.value.trim(), form.password.value);
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
window.location.assign(params.get("next") || "/");
|
||||||
|
} catch (error) {
|
||||||
|
if (message) message.textContent = error.message;
|
||||||
|
} finally {
|
||||||
|
if (submit) submit.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.orgAuth = {
|
||||||
|
fetch: authFetch,
|
||||||
|
headers: authHeaders,
|
||||||
|
token,
|
||||||
|
logout: () => {
|
||||||
|
window.localStorage.removeItem(TOKEN_KEY);
|
||||||
|
redirectToLogin();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
initLoginForm();
|
||||||
|
if (!isLoginPage() && !token()) {
|
||||||
|
redirectToLogin();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -121,7 +121,7 @@ function renderComments(comments) {
|
|||||||
* ----------------------------- */
|
* ----------------------------- */
|
||||||
|
|
||||||
async function fetchComments() {
|
async function fetchComments() {
|
||||||
const res = await fetch(`/api/comments/${pageSlug}`);
|
const res = await window.orgAuth.fetch(`/api/comments/${pageSlug}`);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.error("Failed to fetch comments");
|
console.error("Failed to fetch comments");
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -228,6 +228,8 @@ function renderBoard(items) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadBoard() {
|
async function loadBoard() {
|
||||||
|
if (!document.getElementById("kanban-board")) return;
|
||||||
|
|
||||||
const res = await window.orgAuth.fetch(`/api/competencies/items?group=${currentLevel}`);
|
const res = await window.orgAuth.fetch(`/api/competencies/items?group=${currentLevel}`);
|
||||||
const items = await res.json();
|
const items = await res.json();
|
||||||
|
|
||||||
|
|||||||
@@ -328,6 +328,8 @@
|
|||||||
|
|
||||||
// ── Boot ─────────────────────────────────────────────────
|
// ── Boot ─────────────────────────────────────────────────
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
if (!document.getElementById("wird-app")) return;
|
||||||
|
|
||||||
setTodayLabel();
|
setTodayLabel();
|
||||||
await Promise.all([loadAll(), loadMotalah()]);
|
await Promise.all([loadAll(), loadMotalah()]);
|
||||||
renderToday();
|
renderToday();
|
||||||
|
|||||||
@@ -427,3 +427,29 @@ h2[id], h3[id], h4[id] { scroll-margin-top: 6.5rem; }
|
|||||||
background-color: var(--active-toc);
|
background-color: var(--active-toc);
|
||||||
/* border: 1px solid var(--border, #ccc); */
|
/* border: 1px solid var(--border, #ccc); */
|
||||||
}
|
}
|
||||||
|
.auth-page {
|
||||||
|
min-height: 70vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
width: min(100%, 360px);
|
||||||
|
display: grid;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form h1 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form input {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-message {
|
||||||
|
min-height: 1.4em;
|
||||||
|
color: #a33;
|
||||||
|
}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ FMT / ARGS are passed to `format'."
|
|||||||
(format "<script src=\"/assets/scripts/%s\" defer></script>" f))
|
(format "<script src=\"/assets/scripts/%s\" defer></script>" f))
|
||||||
'("script.js"
|
'("script.js"
|
||||||
"lunr.js"
|
"lunr.js"
|
||||||
"api-auth.js"
|
"auth.js"
|
||||||
"competency-status-board.js"
|
"competency-status-board.js"
|
||||||
"notes.js"
|
"notes.js"
|
||||||
"comments.js"
|
"comments.js"
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ The script is as follows, at the start I have some metadata relating to the file
|
|||||||
<link rel=\"stylesheet\" href=\"/assets/styles/media.css\" />
|
<link rel=\"stylesheet\" href=\"/assets/styles/media.css\" />
|
||||||
|
|
||||||
<script src=\"/assets/scripts/script.js\" defer></script>
|
<script src=\"/assets/scripts/script.js\" defer></script>
|
||||||
<script src=\"/assets/scripts/api-auth.js\" defer></script>
|
<script src=\"/assets/scripts/auth.js\" defer></script>
|
||||||
<script src=\"/assets/scripts/competency-status-board.js\" defer></script>
|
<script src=\"/assets/scripts/competency-status-board.js\" defer></script>
|
||||||
<script src=\"/assets/scripts/comments.js\" defer></script>
|
<script src=\"/assets/scripts/comments.js\" defer></script>
|
||||||
<script src=\"/assets/scripts/bigger-picture.min.js\" defer></script>
|
<script src=\"/assets/scripts/bigger-picture.min.js\" defer></script>
|
||||||
|
|||||||
21
home/login.org
Normal file
21
home/login.org
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#+TITLE: Login
|
||||||
|
#+OPTIONS: toc:nil num:nil
|
||||||
|
#+NO_SIDENOTES: t
|
||||||
|
#+COMMENTS: nil
|
||||||
|
#+SLUG: login
|
||||||
|
|
||||||
|
#+BEGIN_EXPORT html
|
||||||
|
<main class="auth-page">
|
||||||
|
<form id="login-form" class="auth-form">
|
||||||
|
<h1>Login</h1>
|
||||||
|
<label for="login-username">Username</label>
|
||||||
|
<input id="login-username" name="username" type="text" autocomplete="username" required>
|
||||||
|
|
||||||
|
<label for="login-password">Password</label>
|
||||||
|
<input id="login-password" name="password" type="password" autocomplete="current-password" required>
|
||||||
|
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
<p id="login-message" class="auth-message" role="alert"></p>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
#+END_EXPORT
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
||||||
|
|
||||||
* Posts:
|
* Posts:
|
||||||
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">09-05-2026 00:25</span>@@
|
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">09-05-2026 00:35</span>@@
|
||||||
- [[file:posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:<span class="post-date">14-04-2026 16:36</span>@@
|
- [[file:posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:<span class="post-date">14-04-2026 16:36</span>@@
|
||||||
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
#+OPTIONS: toc:nil num:nil
|
#+OPTIONS: toc:nil num:nil
|
||||||
|
|
||||||
* Recently Updated (top 26 files)
|
* Recently Updated (top 26 files)
|
||||||
- [[file:home/guide/setup.org][Setup]] @@html:<span class="post-date">2026-05-09 00:20</span>@@
|
- [[file:home/login.org][Login]] @@html:<span class="post-date">2026-05-09 00:33</span>@@
|
||||||
|
- [[file:home/guide/setup.org][Setup]] @@html:<span class="post-date">2026-05-09 00:33</span>@@
|
||||||
- [[file:blogs/2026/05-may/ai-datacamp-08-05.org][AI Datacamp]] @@html:<span class="post-date">2026-05-08 12:49</span>@@
|
- [[file:blogs/2026/05-may/ai-datacamp-08-05.org][AI Datacamp]] @@html:<span class="post-date">2026-05-08 12:49</span>@@
|
||||||
- [[file:blogs/2026/05-may/using-codex-07-05-26.org][Using Codex]] @@html:<span class="post-date">2026-05-07 16:12</span>@@
|
- [[file:blogs/2026/05-may/using-codex-07-05-26.org][Using Codex]] @@html:<span class="post-date">2026-05-07 16:12</span>@@
|
||||||
- [[file:blogs/2026/05-may/03-05-week-review.org][[03-05-2026] - Weekly Review]] @@html:<span class="post-date">2026-05-03 12:00</span>@@
|
- [[file:blogs/2026/05-may/03-05-week-review.org][[03-05-2026] - Weekly Review]] @@html:<span class="post-date">2026-05-03 12:00</span>@@
|
||||||
@@ -27,4 +28,3 @@
|
|||||||
- [[file:blogs/2026/04-april/joining-new-team-meeting-02-04-2026.org][New team meeting]] @@html:<span class="post-date">2026-04-02 16:03</span>@@
|
- [[file:blogs/2026/04-april/joining-new-team-meeting-02-04-2026.org][New team meeting]] @@html:<span class="post-date">2026-04-02 16:03</span>@@
|
||||||
- [[file:blogs/2026/03-march/intellectually-challenging-myself-30-03-26.org][Intellectually challenging oneself]] @@html:<span class="post-date">2026-03-31 11:14</span>@@
|
- [[file:blogs/2026/03-march/intellectually-challenging-myself-30-03-26.org][Intellectually challenging oneself]] @@html:<span class="post-date">2026-03-31 11:14</span>@@
|
||||||
- [[file:blogs/2026/03-march/cooking-dinner-29-03-26.org][Cooking Dinner (num)]] @@html:<span class="post-date">2026-03-30 11:49</span>@@
|
- [[file:blogs/2026/03-march/cooking-dinner-29-03-26.org][Cooking Dinner (num)]] @@html:<span class="post-date">2026-03-30 11:49</span>@@
|
||||||
- [[file:blogs/2026/03-march/29-03-week-review.org][[29-03-2026] - Weekly Review]] @@html:<span class="post-date">2026-03-29 12:00</span>@@
|
|
||||||
|
|||||||
@@ -11,12 +11,12 @@
|
|||||||
- [[file:tags/notes.org][Tag: notes]]
|
- [[file:tags/notes.org][Tag: notes]]
|
||||||
- [[file:tags/review.org][Tag: review]]
|
- [[file:tags/review.org][Tag: review]]
|
||||||
- [[file:tags/website.org][Tag: website]]
|
- [[file:tags/website.org][Tag: website]]
|
||||||
- [[file:tags/life.org][Tag: life]]
|
|
||||||
- [[file:tags/update.org][Tag: update]]
|
- [[file:tags/update.org][Tag: update]]
|
||||||
- [[file:tags/insights.org][Tag: insights]]
|
- [[file:tags/insights.org][Tag: insights]]
|
||||||
- [[file:tags/emacs.org][Tag: emacs]]
|
- [[file:tags/life.org][Tag: life]]
|
||||||
- [[file:tags/education.org][Tag: education]]
|
- [[file:tags/education.org][Tag: education]]
|
||||||
- [[file:tags/reading.org][Tag: reading]]
|
- [[file:tags/reading.org][Tag: reading]]
|
||||||
|
- [[file:tags/emacs.org][Tag: emacs]]
|
||||||
- [[file:tags/maths.org][Tag: maths]]
|
- [[file:tags/maths.org][Tag: maths]]
|
||||||
- posts
|
- posts
|
||||||
- [[file:posts/posts-intro.org][Posts Introduction]]
|
- [[file:posts/posts-intro.org][Posts Introduction]]
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
- [[file:home/services.org][Service]]
|
- [[file:home/services.org][Service]]
|
||||||
- [[file:home/status.org][Competency Status Board]]
|
- [[file:home/status.org][Competency Status Board]]
|
||||||
- [[file:home/wird-tracker.org][Wird Tracker]]
|
- [[file:home/wird-tracker.org][Wird Tracker]]
|
||||||
|
- [[file:home/login.org][Login]]
|
||||||
- [[file:home/categories.org][Categories]]
|
- [[file:home/categories.org][Categories]]
|
||||||
- guide
|
- guide
|
||||||
- [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
|
- [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
|
||||||
|
|||||||
Reference in New Issue
Block a user