Add optional Authentik (OIDC) sign-in with server-side preference sync
Signed-in users get their preferences (hidden feeds, weather location, weather toggle) stored server-side keyed by their OIDC subject and synced across devices. Anonymous visitors keep using browser localStorage, so the site stays public. First sign-in migrates existing localStorage prefs up. - config: [web.auth] section (issuer, client_id/secret, redirect, session_secret) - storage: user_preferences table + Get/PutUserPrefs - web/auth: OIDC code flow, HMAC-signed session cookie, CSRF state + nonce - web/prefs_api: GET/PUT /api/preferences (auth-gated, 64KB cap) - frontend: prefs.js sync layer seeds localStorage from server, pushes on write - header: sign-in / account control OIDC discovery is non-fatal at boot: if Authentik is down, Pete serves anonymously rather than refusing to start.
This commit is contained in:
File diff suppressed because one or more lines are too long
70
internal/web/static/js/prefs.js
Normal file
70
internal/web/static/js/prefs.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// Preference sync. Anonymous visitors keep using localStorage exactly as
|
||||
// before — this file is a no-op for them. When a user is signed in (Authentik
|
||||
// via OIDC), the server is the source of truth: their stored blob is injected
|
||||
// as window.PETE_PREFS and seeded into localStorage *synchronously* here, before
|
||||
// the feature scripts (settings.js, weather*.js) read it. Those scripts then
|
||||
// call PetePrefs.push() after every write to mirror the change back up.
|
||||
//
|
||||
// This script must run before the others — it's loaded first in layout.html,
|
||||
// and all the feature scripts are `defer`, so document order is guaranteed.
|
||||
(function () {
|
||||
// The localStorage keys we sync. The weather *cache* is deliberately excluded:
|
||||
// it's transient and per-device.
|
||||
var SYNCED = ["pete.disabledSources.v1", "pete.weather.loc.v1", "pete-weather-off"];
|
||||
|
||||
var user = window.PETE_USER || null;
|
||||
var serverPrefs = window.PETE_PREFS || null;
|
||||
|
||||
function seed(prefs) {
|
||||
SYNCED.forEach(function (k) {
|
||||
if (!Object.prototype.hasOwnProperty.call(prefs, k)) return;
|
||||
var v = prefs[k];
|
||||
try {
|
||||
if (v === null || v === undefined) localStorage.removeItem(k);
|
||||
else localStorage.setItem(k, String(v));
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
var out = {};
|
||||
SYNCED.forEach(function (k) {
|
||||
try { var v = localStorage.getItem(k); if (v !== null) out[k] = v; } catch (e) {}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
var timer = null;
|
||||
function push() {
|
||||
if (!user) return; // anonymous: localStorage only
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
timer = null;
|
||||
try {
|
||||
fetch("/api/preferences", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(snapshot()),
|
||||
credentials: "same-origin",
|
||||
keepalive: true
|
||||
}).catch(function () {});
|
||||
} catch (e) {}
|
||||
}, 600);
|
||||
}
|
||||
|
||||
window.PetePrefs = { push: push, loggedIn: !!user, syncedKeys: SYNCED };
|
||||
|
||||
if (user) {
|
||||
if (serverPrefs && typeof serverPrefs === "object") {
|
||||
seed(serverPrefs); // cross-device: server wins on load
|
||||
} else {
|
||||
push(); // first sign-in: migrate this browser's prefs to the account
|
||||
}
|
||||
// Swap "saved in this browser" copy for the synced story.
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelectorAll("[data-storage-note]").forEach(function (el) {
|
||||
el.textContent = "Synced to your account ✓";
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -13,6 +13,7 @@
|
||||
}
|
||||
function save(set) {
|
||||
try { localStorage.setItem(KEY, JSON.stringify(set)); } catch (e) {}
|
||||
if (window.PetePrefs) window.PetePrefs.push();
|
||||
}
|
||||
|
||||
var disabled = load();
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
function setLoc(loc) {
|
||||
if (loc) writeJSON(LOC_KEY, loc);
|
||||
else { try { localStorage.removeItem(LOC_KEY); } catch (e) {} }
|
||||
if (window.PetePrefs) window.PetePrefs.push();
|
||||
}
|
||||
|
||||
// ---- WMO weather code → canvas variant + label + emoji -------------------
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
if (v) localStorage.setItem(STORAGE_KEY, "1");
|
||||
else localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (e) {}
|
||||
if (window.PetePrefs) window.PetePrefs.push();
|
||||
}
|
||||
function syncBtn(enabled) {
|
||||
if (!toggleBtn) return;
|
||||
|
||||
Reference in New Issue
Block a user