From 22da11514b155ef5b8f5445e387b68c90cd2dce8 Mon Sep 17 00:00:00 2001 From: Pascal Dietrich Date: Tue, 2 Jun 2026 20:46:30 +0200 Subject: [PATCH 1/2] feat: add passwordHashFile to admin.json --- lib/admin/migrate.ts | 2 +- lib/admin/password.ts | 18 ++++++++++++++---- lib/admin/types.ts | 3 ++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/admin/migrate.ts b/lib/admin/migrate.ts index 39d7b2be..a6188d12 100644 --- a/lib/admin/migrate.ts +++ b/lib/admin/migrate.ts @@ -98,7 +98,7 @@ async function migrateAdminJson(): Promise { lastLogin: data.lastLogin ?? null, passwordChangedAt: data.passwordChangedAt ?? now, }; - const configData: AdminConfigData = { passwordHash: data.passwordHash }; + const configData: AdminConfigData = { passwordHash: data.passwordHash, passwordHashFile: undefined }; await ensureStateDir(); const statePath = getStatePath('admin-state.json'); diff --git a/lib/admin/password.ts b/lib/admin/password.ts index 4f4fd7d7..d9dff58a 100644 --- a/lib/admin/password.ts +++ b/lib/admin/password.ts @@ -9,6 +9,7 @@ import { assertWritable, } from './paths'; import type { AdminConfigData, AdminStateData } from './types'; +import { readFileEnv } from '../read-file-env'; const SCRYPT_KEYLEN = 64; const SCRYPT_COST = 16384; // 2^14 @@ -146,7 +147,7 @@ export async function initAdminPassword(): Promise { } const hash = isHashed(envPassword) ? envPassword : await hashPassword(envPassword); - cachedConfig = { passwordHash: hash }; + cachedConfig = { passwordHash: hash, passwordHashFile: undefined }; cachedState = freshState(); await writeConfigData(cachedConfig); await writeStateData(cachedState); @@ -165,7 +166,16 @@ export async function initAdminPassword(): Promise { export async function verifyAdminPassword(password: string): Promise { if (!cachedConfig) cachedConfig = await readConfigData(); if (!cachedConfig) return false; - return verifyPassword(password, cachedConfig.passwordHash); + if (cachedConfig.passwordHash) { + return verifyPassword(password, cachedConfig.passwordHash); + } else if (cachedConfig.passwordHashFile) { + let passwordHash = readFileEnv(cachedConfig.passwordHashFile); + if (passwordHash) { + return verifyPassword(password, passwordHash); + } + } + logger.error('The admin password hash could neither be retrieved from passwordHash nor from passwordHashFile in admin.json'); + return false; } /** @@ -176,7 +186,7 @@ export async function changeAdminPassword(currentPassword: string, newPassword: if (!valid) return false; const hash = await hashPassword(newPassword); - cachedConfig = { passwordHash: hash }; + cachedConfig = { passwordHash: hash, passwordHashFile: undefined }; await writeConfigData(cachedConfig); cachedState = { @@ -205,7 +215,7 @@ export async function setInitialAdminPassword( const existing = await readConfigData(); if (existing && !options.allowOverwrite) return false; const hash = await hashPassword(newPassword); - cachedConfig = { passwordHash: hash }; + cachedConfig = { passwordHash: hash, passwordHashFile: undefined }; cachedState = freshState(); await writeConfigData(cachedConfig); await writeStateData(cachedState); diff --git a/lib/admin/types.ts b/lib/admin/types.ts index b2a7e8bd..2ea23a3a 100644 --- a/lib/admin/types.ts +++ b/lib/admin/types.ts @@ -6,7 +6,8 @@ * is config; mutable timestamps live in AdminStateData. */ export interface AdminConfigData { - passwordHash: string; + passwordHash: string | undefined; + passwordHashFile: string | undefined; } /** From 75602b6a001083b0fb657dffa43cc9670dd55ccc Mon Sep 17 00:00:00 2001 From: dealerweb Date: Wed, 3 Jun 2026 14:34:13 +0200 Subject: [PATCH 2/2] Fix: Settings section gears permanently hijacked the active tab The folder and tag section gears in the sidebar deep-linked into Settings by writing the persisted `settings-active-tab` localStorage key, so the chosen section became the permanent default the main Settings button opened on - indefinitely. Compounding it, the desktop Settings tab list called setActiveTab directly without persisting, so normal navigation never updated the default and the hijacked value could never self-correct. Fix: section gears now write a one-shot sessionStorage key that is consumed on mount (transient deep-link, no persistence); desktop tab clicks go through handleTabSelect like the mobile list, so the last-used tab is saved consistently. Stale/removed tab IDs are still caught by the existing effectiveActiveTab fallback. --- app/(main)/[locale]/settings/page.tsx | 15 ++++++++++++++- components/layout/sidebar.tsx | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/(main)/[locale]/settings/page.tsx b/app/(main)/[locale]/settings/page.tsx index ab7dcad7..efe8d476 100644 --- a/app/(main)/[locale]/settings/page.tsx +++ b/app/(main)/[locale]/settings/page.tsx @@ -335,6 +335,14 @@ const LEGACY_TAB_MAP: Record = { function readPersistedTab(): Tab { try { + // One-shot deep link from the sidebar section gears (Folders / Tags). + // Used only as the initial tab and intentionally NOT written to + // 'settings-active-tab', so a gear click never becomes the persisted + // default that the regular Settings button lands on. Cleared on mount. + const deepLink = sessionStorage.getItem('settings-deep-link-tab'); + if (deepLink) { + return (deepLink in LEGACY_TAB_MAP ? LEGACY_TAB_MAP[deepLink] : deepLink) as Tab; + } const saved = localStorage.getItem('settings-active-tab'); if (!saved) return 'appearance'; if (saved in LEGACY_TAB_MAP) { @@ -360,6 +368,11 @@ export default function SettingsPage() { const { stalwartFeaturesEnabled } = useConfig(); const { isFeatureEnabled } = usePolicyStore(); const [activeTab, setActiveTab] = useState(readPersistedTab); + // Consume the one-shot deep-link key so a section gear only steers this one + // open, never the persisted default for future Settings-button clicks. + useEffect(() => { + try { sessionStorage.removeItem('settings-deep-link-tab'); } catch { /* ignore */ } + }, []); const [mobileShowContent, setMobileShowContent] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [pendingHighlight, setPendingHighlight] = useState<{ tab: Tab; label: string; pluginId?: string } | null>(null); @@ -933,7 +946,7 @@ export default function SettingsPage() { return (