<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>User Accounts |</title><link>https://ascendnextlevel.org.uk/07_secure/03_user-accounts/</link><atom:link href="https://ascendnextlevel.org.uk/07_secure/03_user-accounts/index.xml" rel="self" type="application/rss+xml"/><description>User Accounts</description><generator>Wowchemy (https://wowchemy.com)</generator><language>en</language><copyright>S.J.Dolley</copyright><item><title>Manage Permitted Users</title><link>https://ascendnextlevel.org.uk/07_secure/03_user-accounts/01_manageuseraccounts/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://ascendnextlevel.org.uk/07_secure/03_user-accounts/01_manageuseraccounts/</guid><description>&lt;div id="form-config" style="display:none"&gt;
{
"save_bin_key": "USER_ACCESS_KEY",
"save_sectionKey": "permitted_users",
"listLabel": "Existing Permitted Users",
"checkList_bin_key": "USER_ACCESS_KEY",
"checkList_section_key": "Role_Details",
"checkList_fields": ["Role"]
}
&lt;/div&gt;
&lt;fieldset&gt;
&lt;legend&gt;Permitted User&lt;/legend&gt;
&lt;label&gt;Name&lt;input required class="name" type="text" placeholder="Samantha Smith"/&gt;&lt;/label&gt;
&lt;label&gt;Email&lt;input required type="email" placeholder="Samantha@mail.com" /&gt;&lt;/label&gt;
&lt;label&gt;User name&lt;input class="name" type="text" disabled/&gt;&lt;/label&gt;
&lt;label hidden&gt;login_token&lt;input type="text" hidden/&gt;&lt;/label&gt;
&lt;fieldset&gt;
&lt;legend&gt;Role&lt;/legend&gt;
&lt;div class="check-list-container"&gt;&lt;/div&gt;
&lt;/fieldset&gt;
&lt;/fieldset&gt;</description></item><item><title>Manage Roles</title><link>https://ascendnextlevel.org.uk/07_secure/03_user-accounts/03_manageroles/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://ascendnextlevel.org.uk/07_secure/03_user-accounts/03_manageroles/</guid><description>&lt;div id="form-config" style="display:none"&gt;
{
"save_bin_key": "USER_ACCESS_KEY",
"save_sectionKey": "Role_Details",
"listLabel": "Existing Roles",
"checkList_bin_key": null,
"checkList_section_key": null,
"checkList_fields": null
}
&lt;/div&gt;
&lt;fieldset&gt;
&lt;legend&gt;Role Details&lt;/legend&gt;
&lt;label&gt;Role&lt;input required class="name" type="text" /&gt;&lt;/label&gt;
&lt;label&gt;Description&lt;input required type="text" /&gt;&lt;/label&gt;
&lt;/fieldset&gt;</description></item><item><title>Manage Stored Records</title><link>https://ascendnextlevel.org.uk/07_secure/03_user-accounts/10_managestoredrecords/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://ascendnextlevel.org.uk/07_secure/03_user-accounts/10_managestoredrecords/</guid><description>&lt;h2&gt;Stored Secure Records&lt;/h2&gt;
&lt;p&gt;This form lists all secure records currently stored. Select one or more to delete.&lt;/p&gt;
&lt;p&gt;&lt;button type="button" id="refreshRecordsBtn"&gt;Refresh List&lt;/button&gt;&lt;/p&gt;
&lt;ul id="recordList"&gt;&lt;/ul&gt;
&lt;button type="button" id="deleteSelectedBtn" style="display:none;"&gt;Delete Selected&lt;/button&gt;
&lt;script&gt;
document.addEventListener("DOMContentLoaded", () =&gt; {
const recordsList = document.getElementById("recordList");
const refreshBtn = document.getElementById("refreshRecordsBtn");
const deleteBtn = document.getElementById("deleteSelectedBtn");
// Normalise server response into an array of { token, value }
function normalizeRecordsPayload(data) {
if (!data) return [];
if (Array.isArray(data.records)) {
// Already an array of {token, value} (ideal)
return data.records;
}
// Some backends return an object: { token1: value1, token2: value2, ... }
if (data.records &amp;&amp; typeof data.records === "object") {
const obj = data.records;
// numeric-keyed object (like { "0": {...}, "1": {...} })
const keys = Object.keys(obj);
const allNumeric = keys.length &gt; 0 &amp;&amp; keys.every(k =&gt; !isNaN(k));
if (allNumeric) {
return Object.values(obj).map(item =&gt; {
// if values are objects that already contain token, try to preserve token if present
if (item &amp;&amp; item.token) return item;
return { token: null, value: item };
});
}
// token -&gt; value mapping
return Object.entries(obj).map(([token, value]) =&gt; ({ token, value }));
}
// Fallback: unknown shape
return [];
}
async function fetchRecords() {
recordsList.innerHTML = "&lt;li&gt;Loading...&lt;/li&gt;";
try {
const res = await fetch("/.netlify/functions/secureStore_ClientAccess", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ list: true })
});
const data = await res.json();
console.debug("[DEBUG] fetchRecords response:", data);
const records = normalizeRecordsPayload(data);
if (!records.length) {
recordsList.innerHTML = "&lt;li&gt;&lt;em&gt;No records found&lt;/em&gt;&lt;/li&gt;";
deleteBtn.style.display = "none";
return;
}
// show newest first if there is a date-ish key, otherwise just reverse for simple recent-first
// Here we simply render newest-first by reversing the array copy
const rows = [...records].reverse();
recordsList.innerHTML = "";
for (const item of rows) {
const token = item.token ?? (item.value &amp;&amp; item.value.token) ?? "(unknown-token)";
const value = item.value ?? item; // in case item is itself a value
const li = document.createElement("li");
li.style.listStyle = "none";
// pretty-print small JSON, fall back to toString for primitives
let pretty;
try { pretty = typeof value === "object" ? JSON.stringify(value) : String(value); }
catch (e) { pretty = String(value); }
li.innerHTML = `
&lt;label&gt;
&lt;input type="checkbox" class="record-checkbox" value="${encodeURIComponent(token)}"&gt;
&lt;strong&gt;${token}&lt;/strong&gt; — ${pretty}
&lt;/label&gt;
`;
recordsList.appendChild(li);
}
deleteBtn.style.display = "inline-block";
updateInlineFit();
} catch (err) {
console.error("[ERROR] fetchRecords failed:", err);
recordsList.innerHTML = `&lt;li&gt;Error loading records: ${err.message}&lt;/li&gt;`;
deleteBtn.style.display = "none";
}
}
async function deleteSelected() {
const checked = Array.from(document.querySelectorAll(".record-checkbox:checked"));
if (!checked.length) return;
if (!confirm(`Delete ${checked.length} selected record(s)? This cannot be undone.`)) return;
// NOTE: your server-side currently supports SET (value provided) and GET (token).
// The previous client attempted to delete by sending value: null — that will NOT work
// with the current setSecureItem implementation (spreading null fails).
//
// Recommended minimal server change:
// - Add support for a delete flag `delete: true` that removes the key from the stored object.
//
// Example server request body for delete (recommended):
// { token: "&lt;token&gt;", delete: true }
//
// If you prefer, I can provide the tiny server-side patch to handle `delete: true`.
//
// Meanwhile, if your server already accepts value:null for deletion you can keep using it.
//
for (const box of checked) {
const token = decodeURIComponent(box.value);
// Preferred approach (server must implement delete handling)
await fetch("/.netlify/functions/secureStore_ClientAccess", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, delete: true })
});
}
// refresh list
await fetchRecords();
}
refreshBtn.addEventListener("click", fetchRecords);
deleteBtn.addEventListener("click", deleteSelected);
// initial load
fetchRecords();
});
&lt;/script&gt;</description></item></channel></rss>