[auth] Add "Lock this device" control to re-lock a kept-unlocked session (#51)

The opt-in "keep unlocked" cache had no user-facing way to undo it mid-session
(it cleared only on logout / clear-site-data). Add a "Lock this device" button to
the profile modal footer (desktop + mobile), shown only when the active account
has a cached key to forget (app.isSessionKeptUnlocked -> client.hasCachedUnlockedKey).

Clicking it runs app.lockKeptUnlockedSession(pubkey), which re-locks in one action
everything the signer unlocks:
  - forgets the cached key (both tiers) so future reloads re-prompt for the PIN,
  - drops the in-memory signer (logoutSigner) so the CURRENT session re-locks now
    (the next signing action prompts again via ensureEncryptionCapableSigner),
  - storageService.lock(pubkey), and
  - nwcSettingsService.clearCache() (re-hydrates from the signer next time).

Visibility refreshes on renderSavedProfiles + selectPane. Tests: 2 in
tests/profile-modal-controller.test.mjs (shown only when kept-unlocked; click locks
the active pubkey). Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-01 20:16:00 -04:00
1 parent b0a2eeaf72
commit cebd3dc1e7
7 files changed
+255 -6

No files matched your search

+45
View File
@@ -360,6 +360,8 @@ export class ProfileModalController {
this.closeButton = null;
this.logoutButton = null;
this.mobileLogoutButton = null;
this.lockNowButton = null;
this.mobileLockNowButton = null;
this.channelLink = null;
this.addAccountButton = null;
this.navButtons = {
@@ -657,6 +659,9 @@ export class ProfileModalController {
this.logoutButton = document.getElementById("profileLogoutBtn") || null;
this.mobileLogoutButton =
document.getElementById("profileMobileLogoutBtn") || null;
this.lockNowButton = document.getElementById("profileLockNowBtn") || null;
this.mobileLockNowButton =
document.getElementById("profileMobileLockNowBtn") || null;
this.channelLink = document.getElementById("profileChannelLink") || null;
this.addAccountButton =
document.getElementById("profileAddAccountBtn") || null;
@@ -1291,6 +1296,25 @@ export class ProfileModalController {
}
}
// Show the "Lock this device" control only when there is a cached "keep
// unlocked" key to forget (an nsec account currently kept unlocked). TODO #51.
updateLockNowVisibility() {
const check = this.services?.isSessionKeptUnlocked;
let unlocked = false;
if (typeof check === "function") {
try {
unlocked = Boolean(check(this.getActivePubkey()));
} catch (error) {
unlocked = false;
}
}
for (const button of [this.lockNowButton, this.mobileLockNowButton]) {
if (button instanceof HTMLElement) {
button.classList.toggle("hidden", !unlocked);
}
}
}
setMessagesUnreadIndicator(visible) {
if (!(this.dmController.profileMessagesUnreadDot instanceof HTMLElement)) {
return;
@@ -1672,6 +1696,25 @@ export class ProfileModalController {
});
}
const handleLockNow = () => {
const lock = this.services?.lockKeptUnlockedSession;
if (typeof lock !== "function") {
return;
}
try {
lock(this.getActivePubkey());
} catch (error) {
devLogger.warn("[profileModal] Lock this device failed:", error);
}
this.updateLockNowVisibility();
};
if (this.lockNowButton instanceof HTMLElement) {
this.lockNowButton.addEventListener("click", handleLockNow);
}
if (this.mobileLockNowButton instanceof HTMLElement) {
this.mobileLockNowButton.addEventListener("click", handleLockNow);
}
if (this.channelLink instanceof HTMLElement) {
this.channelLink.addEventListener("click", (event) => {
event.preventDefault();
@@ -2365,6 +2408,7 @@ export class ProfileModalController {
* Triggers a batch fetch for any missing profile metadata.
*/
renderSavedProfiles() {
this.updateLockNowVisibility();
const normalizedActive = this.normalizeHexPubkey(this.getActivePubkey());
const entriesNeedingFetch = new Set();
const savedProfiles = this.getSavedProfiles();
@@ -3237,6 +3281,7 @@ export class ProfileModalController {
}
selectPane(name = "account", options = {}) {
this.updateLockNowVisibility();
const { keepMenuView = false } =
options && typeof options === "object" ? options : {};
const normalized = typeof name === "string" ? name.toLowerCase() : "account";