Replace sessionStorage with localStorage in the auth bootstrap
Intent: Switch the storage backend from sessionStorage (scoped to a single tab) to localStorage (shared across all same-origin tabs) so that authentication survives opening new tabs and normal page navigation.
Affected files: crates/pika-news/templates/base.html
Evidence
@@ -95,18 +95,41 @@
- let authToken = sessionStorage.getItem('pika_news_token');
- let savedNpub = sessionStorage.getItem('pika_news_npub');
- let savedIsAdmin = sessionStorage.getItem('pika_news_is_admin') === '1';
- let savedCanForgeWrite = sessionStorage.getItem('pika_news_can_forge_write') === '1';
+ const authStorage = window.localStorage;
+
+ function getAuthValue(key) {
+ return authStorage.getItem(key);
+ }
+
+ function setAuthValue(key, value) {
+ authStorage.setItem(key, value);
+ }
+
+ function removeAuthValue(key) {
+ authStorage.removeItem(key);
+ }
+
+ function readAuthSnapshot() {
+ return {
+ token: getAuthValue('pika_news_token'),
+ npub: getAuthValue('pika_news_npub'),
+ isAdmin: getAuthValue('pika_news_is_admin') === '1',
+ canForgeWrite: getAuthValue('pika_news_can_forge_write') === '1',
+ };
+ }
The core change lives at the top of the auth bootstrap block in base.html. A single const authStorage = window.localStorage binding replaces every previous bare sessionStorage reference. Four thin wrapper functions are introduced:
| Function | Purpose |
getAuthValue(key) | Read a key from localStorage |
setAuthValue(key, value) | Write a key to localStorage |
removeAuthValue(key) | Delete a key from localStorage |
readAuthSnapshot() | Return an object with all four auth fields at once |
All downstream call-sites in base.html — the initial state read, window.pikaAuth accessor methods, clearAuth(), fetchInboxCount(), refreshSessionInfo(), and the login flow — are rewritten to use these wrappers instead of calling sessionStorage directly. This means a future change to the storage backend (e.g., cookie-backed) only requires editing one line.
Add cross-tab auth synchronisation via the Storage event
Intent: React to auth changes made in other tabs so that every open pika-news tab stays in sync without requiring a manual reload.
Affected files: crates/pika-news/templates/base.html
Evidence
@@ -239,6 +262,17 @@
+ window.addEventListener('storage', function (event) {
+ if (!event.key || !event.key.startsWith('pika_news_')) return;
+ const snapshot = readAuthSnapshot();
+ if (snapshot.token && snapshot.npub) {
+ showAuthed(snapshot.npub, snapshot.isAdmin, snapshot.canForgeWrite);
+ refreshSessionInfo();
+ } else {
+ clearAuth();
+ }
+ });
A storage event listener is registered on window. The browser fires this event in every other tab when localStorage is modified, making it the standard mechanism for cross-tab communication.
The handler:
- Ignores events for keys that don't start with
pika_news_ to avoid reacting to unrelated localStorage writes.
- Reads a full auth snapshot via
readAuthSnapshot().
- If a valid token and npub exist, calls
showAuthed() and refreshSessionInfo() to update the UI and revalidate the session with the server.
- Otherwise calls
clearAuth() to log the user out in the current tab.
This means signing in on Tab A immediately updates Tab B's navbar, and signing out in any tab propagates the logout everywhere.
Remove sessionStorage fallbacks from admin and inbox templates
Intent: Eliminate redundant fallback branches that directly called sessionStorage, ensuring all auth clearing goes through the centralised pikaAuth.clear() path.
Affected files: crates/pika-news/templates/admin.html, crates/pika-news/templates/inbox.html
Evidence
@@ -165,11 +165,6 @@
function clearAuth() {
if (window.pikaAuth && window.pikaAuth.clear) {
window.pikaAuth.clear();
- } else {
- sessionStorage.removeItem('pika_news_token');
- sessionStorage.removeItem('pika_news_npub');
- sessionStorage.removeItem('pika_news_is_admin');
- sessionStorage.removeItem('pika_news_can_forge_write');
}
@@ -77,10 +77,6 @@
function clearAuth() {
if (window.pikaAuth && window.pikaAuth.clear) {
window.pikaAuth.clear();
- } else {
- sessionStorage.removeItem('pika_news_token');
- sessionStorage.removeItem('pika_news_npub');
- sessionStorage.removeItem('pika_news_is_admin');
}
Both admin.html and inbox.html contained local clearAuth() functions with an else branch that called sessionStorage.removeItem directly as a fallback in case window.pikaAuth was not yet initialised.
These branches are deleted because:
base.html always defines window.pikaAuth before any page-specific script runs (it's in the shared layout).
- The fallback would now be incorrect — it would write to sessionStorage while the real auth lives in localStorage, creating a ghost state.
After this change, admin.html and inbox.html contain zero direct storage API calls for auth keys.
Add a Rust test asserting the localStorage migration invariants
Intent: Prevent regressions by statically checking that templates use localStorage, listen for the storage event, and contain no residual sessionStorage references for auth keys.
Affected files: crates/pika-news/src/web.rs
Evidence
@@ -4882,6 +4882,21 @@
+ #[test]
+ fn browser_auth_bootstrap_uses_local_storage_across_tabs() {
+ let base_template = include_str!("../templates/base.html");
+ let inbox_template = include_str!("../templates/inbox.html");
+ let admin_template = include_str!("../templates/admin.html");
+
+ assert!(base_template.contains("const authStorage = window.localStorage;"));
+ assert!(base_template.contains("window.addEventListener('storage'"));
+ assert!(!base_template.contains("sessionStorage.getItem('pika_news_token')"));
+ assert!(!base_template.contains("sessionStorage.setItem('pika_news_token'"));
+ assert!(!base_template.contains("sessionStorage.removeItem('pika_news_token'"));
+ assert!(!inbox_template.contains("sessionStorage.removeItem('pika_news_token'"));
+ assert!(!admin_template.contains("sessionStorage.removeItem('pika_news_token'"));
+ }
A compile-time test (browser_auth_bootstrap_uses_local_storage_across_tabs) is added to web.rs. It uses include_str! to embed the three template files at compile time and then makes seven assertions:
Positive checks (base.html must contain):
const authStorage = window.localStorage; — confirms the localStorage binding exists.
window.addEventListener('storage' — confirms the cross-tab listener is registered.
Negative checks (no template may contain):
sessionStorage.getItem('pika_news_token') in base.html
sessionStorage.setItem('pika_news_token') in base.html
sessionStorage.removeItem('pika_news_token') in base.html, inbox.html, and admin.html
Because include_str! is evaluated at compile time, the test runs with zero I/O overhead and will fail the build immediately if anyone reintroduces a sessionStorage call for the auth token.