From 447baac50f926ccc146e19fe9ac5050fa95f1c11 Mon Sep 17 00:00:00 2001 From: cubap Date: Tue, 10 Mar 2026 13:37:49 -0500 Subject: [PATCH 1/3] Gate 4.2: token/localStorage hardening (token-expiration UX, vault cache clear on logout, annotationsState scoping) --- api/TPEN.js | 13 +++++++++++++ components/new-action.js | 3 ++- interfaces/manage-columns/index.js | 10 +++++----- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/api/TPEN.js b/api/TPEN.js index 7e4bcd36..ce152742 100644 --- a/api/TPEN.js +++ b/api/TPEN.js @@ -61,6 +61,12 @@ class Tpen { eventDispatcher.on("tpen-user-loaded", ev => this.currentUser = ev.detail) eventDispatcher.on("tpen-project-loaded", ev => this.activeProject = ev.detail) + // Centralized token expiration UX: notify user and redirect to login + eventDispatcher.on('token-expiration', () => { + eventDispatcher.dispatch('tpen-toast', { status: 'error', message: 'Your session has expired. Redirecting to login...' }) + setTimeout(() => this.login(), 4000) + }) + if (this.screen.projectInQuery) { try { import('./Project.js').then(module => { @@ -176,6 +182,13 @@ class Tpen { logout(redirect = origin + location.pathname) { localStorage.removeItem("userToken") + // Clear user-specific IIIF resource cache so another user on this device + // does not inherit authenticated annotation data from the previous session (#402) + for (const key of Object.keys(localStorage)) { + if (/^vault:(annotation|annotationpage|annotationcollection):/.test(key)) { + localStorage.removeItem(key) + } + } location.href = `${this.TPEN3URL}/logout?returnTo=${encodeURIComponent(redirect)}` return } diff --git a/components/new-action.js b/components/new-action.js index a00c9d0a..21a4d0bb 100644 --- a/components/new-action.js +++ b/components/new-action.js @@ -94,7 +94,8 @@ class NewAction extends HTMLElement { } TPEN2ImportHandler = async () => { - const userToken = localStorage.getItem("userToken") + const userToken = TPEN.getAuthorization() + if (!userToken) return TPEN.login() let tokenDomain let isProdTPEN diff --git a/interfaces/manage-columns/index.js b/interfaces/manage-columns/index.js index 5aaccb0b..fdb2f848 100644 --- a/interfaces/manage-columns/index.js +++ b/interfaces/manage-columns/index.js @@ -305,7 +305,7 @@ class TpenManageColumns extends HTMLElement { connectedCallback() { TPEN.attachAuthentication(this) - localStorage.removeItem('annotationsState') + localStorage.removeItem(`annotationsState:${TPEN.screen.projectInQuery ?? 'unknown'}`) if (!TPEN.screen.pageInQuery) { this.showError("No page id provided") return @@ -403,7 +403,7 @@ class TpenManageColumns extends HTMLElement { })) }) this.totalIds = annotations.filter(anno => !assignedAnnotationIds.some(a => a.lineId === anno.lineId)).map(a => a.lineId) - localStorage.setItem('annotationsState', JSON.stringify({ + localStorage.setItem(`annotationsState:${TPEN.screen.projectInQuery ?? 'unknown'}`, JSON.stringify({ remainingIDs: this.totalIds, selectedIDs: [] })) @@ -526,7 +526,7 @@ class TpenManageColumns extends HTMLElement { this.totalIds = this.cachedAnnotations .filter(anno => !assignedAnnotationIds.some(a => a.lineId === anno.lineId)) .map(a => a.lineId) - localStorage.setItem('annotationsState', JSON.stringify({ + localStorage.setItem(`annotationsState:${TPEN.screen.projectInQuery ?? 'unknown'}`, JSON.stringify({ remainingIDs: this.totalIds, selectedIDs: [] })) @@ -916,7 +916,7 @@ class TpenManageColumns extends HTMLElement { } restoreAnnotationState() { - const saved = localStorage.getItem('annotationsState') + const saved = localStorage.getItem(`annotationsState:${TPEN.screen.projectInQuery ?? 'unknown'}`) if (!saved) return const { selectedIDs = [] } = JSON.parse(saved) const boxes = Array.from(this.shadowRoot.querySelectorAll('.overlayBox')) @@ -1012,7 +1012,7 @@ class TpenManageColumns extends HTMLElement { const selectedIDs = this.selectedBoxes.map(b => b.dataset.lineId) const remainingIDs = this.totalIds.filter(id => !this.selectedBoxes.some(b => b.dataset.lineId === id)) try { - localStorage.setItem('annotationsState', JSON.stringify({ remainingIDs, selectedIDs })) + localStorage.setItem(`annotationsState:${TPEN.screen.projectInQuery ?? 'unknown'}`, JSON.stringify({ remainingIDs, selectedIDs })) } catch (e) { // localStorage may be unavailable (e.g., private mode, quota exceeded) // Silent fail as this is just a convenience feature From f4cb8f9b5bc9285b951901fac37c9ae68e760704 Mon Sep 17 00:00:00 2001 From: cubap Date: Tue, 10 Mar 2026 13:41:17 -0500 Subject: [PATCH 2/3] Remove vault cache clear on logout RERUM annotations are open data, clearing is unnecessary churn --- api/TPEN.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/api/TPEN.js b/api/TPEN.js index ce152742..a7dd4ba4 100644 --- a/api/TPEN.js +++ b/api/TPEN.js @@ -182,13 +182,6 @@ class Tpen { logout(redirect = origin + location.pathname) { localStorage.removeItem("userToken") - // Clear user-specific IIIF resource cache so another user on this device - // does not inherit authenticated annotation data from the previous session (#402) - for (const key of Object.keys(localStorage)) { - if (/^vault:(annotation|annotationpage|annotationcollection):/.test(key)) { - localStorage.removeItem(key) - } - } location.href = `${this.TPEN3URL}/logout?returnTo=${encodeURIComponent(redirect)}` return } From f0d7e295db8780a91b8fdd793e513335c61f2501 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Mon, 16 Mar 2026 12:29:21 -0500 Subject: [PATCH 3/3] Changes during review --- api/TPEN.js | 4 +++- interfaces/manage-columns/index.js | 14 +++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/api/TPEN.js b/api/TPEN.js index a7dd4ba4..56373a16 100644 --- a/api/TPEN.js +++ b/api/TPEN.js @@ -62,9 +62,11 @@ class Tpen { eventDispatcher.on("tpen-project-loaded", ev => this.activeProject = ev.detail) // Centralized token expiration UX: notify user and redirect to login + let expirationTimeout = null eventDispatcher.on('token-expiration', () => { + if (expirationTimeout) return eventDispatcher.dispatch('tpen-toast', { status: 'error', message: 'Your session has expired. Redirecting to login...' }) - setTimeout(() => this.login(), 4000) + expirationTimeout = setTimeout(() => this.login(), 4000) }) if (this.screen.projectInQuery) { diff --git a/interfaces/manage-columns/index.js b/interfaces/manage-columns/index.js index 41c84e03..9b17766d 100644 --- a/interfaces/manage-columns/index.js +++ b/interfaces/manage-columns/index.js @@ -305,7 +305,11 @@ class TpenManageColumns extends HTMLElement { connectedCallback() { TPEN.attachAuthentication(this) - localStorage.removeItem(`annotationsState:${TPEN.screen.projectInQuery ?? 'unknown'}`) + if (!TPEN.screen.projectInQuery) { + this.showError("No project id provided") + return + } + localStorage.removeItem(`annotationsState:${TPEN.screen.projectInQuery}`) if (!TPEN.screen.pageInQuery) { this.showError("No page id provided") return @@ -403,7 +407,7 @@ class TpenManageColumns extends HTMLElement { })) }) this.totalIds = annotations.filter(anno => !assignedAnnotationIds.some(a => a.lineId === anno.lineId)).map(a => a.lineId) - localStorage.setItem(`annotationsState:${TPEN.screen.projectInQuery ?? 'unknown'}`, JSON.stringify({ + localStorage.setItem(`annotationsState:${TPEN.screen.projectInQuery}`, JSON.stringify({ remainingIDs: this.totalIds, selectedIDs: [] })) @@ -526,7 +530,7 @@ class TpenManageColumns extends HTMLElement { this.totalIds = this.cachedAnnotations .filter(anno => !assignedAnnotationIds.some(a => a.lineId === anno.lineId)) .map(a => a.lineId) - localStorage.setItem(`annotationsState:${TPEN.screen.projectInQuery ?? 'unknown'}`, JSON.stringify({ + localStorage.setItem(`annotationsState:${TPEN.screen.projectInQuery}`, JSON.stringify({ remainingIDs: this.totalIds, selectedIDs: [] })) @@ -927,7 +931,7 @@ class TpenManageColumns extends HTMLElement { } restoreAnnotationState() { - const saved = localStorage.getItem(`annotationsState:${TPEN.screen.projectInQuery ?? 'unknown'}`) + const saved = localStorage.getItem(`annotationsState:${TPEN.screen.projectInQuery}`) if (!saved) return const { selectedIDs = [] } = JSON.parse(saved) const boxes = Array.from(this.shadowRoot.querySelectorAll('.overlayBox')) @@ -1023,7 +1027,7 @@ class TpenManageColumns extends HTMLElement { const selectedIDs = this.selectedBoxes.map(b => b.dataset.lineId) const remainingIDs = this.totalIds.filter(id => !this.selectedBoxes.some(b => b.dataset.lineId === id)) try { - localStorage.setItem(`annotationsState:${TPEN.screen.projectInQuery ?? 'unknown'}`, JSON.stringify({ remainingIDs, selectedIDs })) + localStorage.setItem(`annotationsState:${TPEN.screen.projectInQuery}`, JSON.stringify({ remainingIDs, selectedIDs })) } catch (e) { // localStorage may be unavailable (e.g., private mode, quota exceeded) // Silent fail as this is just a convenience feature