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() {
|
||||
const res = await fetch(`/api/comments/${pageSlug}`);
|
||||
const res = await window.orgAuth.fetch(`/api/comments/${pageSlug}`);
|
||||
if (!res.ok) {
|
||||
console.error("Failed to fetch comments");
|
||||
return [];
|
||||
|
||||
@@ -228,6 +228,8 @@ function renderBoard(items) {
|
||||
}
|
||||
|
||||
async function loadBoard() {
|
||||
if (!document.getElementById("kanban-board")) return;
|
||||
|
||||
const res = await window.orgAuth.fetch(`/api/competencies/items?group=${currentLevel}`);
|
||||
const items = await res.json();
|
||||
|
||||
|
||||
@@ -328,6 +328,8 @@
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
if (!document.getElementById("wird-app")) return;
|
||||
|
||||
setTodayLabel();
|
||||
await Promise.all([loadAll(), loadMotalah()]);
|
||||
renderToday();
|
||||
|
||||
@@ -427,3 +427,29 @@ h2[id], h3[id], h4[id] { scroll-margin-top: 6.5rem; }
|
||||
background-color: var(--active-toc);
|
||||
/* 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user