This commit is contained in:
Linus Rath
2026-06-03 19:34:20 +02:00
5 changed files with 33 additions and 9 deletions
+14 -1
View File
@@ -336,6 +336,14 @@ const LEGACY_TAB_MAP: Record<string, Tab> = {
function readPersistedTab(): Tab { function readPersistedTab(): Tab {
try { 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'); const saved = localStorage.getItem('settings-active-tab');
if (!saved) return 'appearance'; if (!saved) return 'appearance';
if (saved in LEGACY_TAB_MAP) { if (saved in LEGACY_TAB_MAP) {
@@ -361,6 +369,11 @@ export default function SettingsPage() {
const { stalwartFeaturesEnabled } = useConfig(); const { stalwartFeaturesEnabled } = useConfig();
const { isFeatureEnabled } = usePolicyStore(); const { isFeatureEnabled } = usePolicyStore();
const [activeTab, setActiveTab] = useState<Tab>(readPersistedTab); const [activeTab, setActiveTab] = useState<Tab>(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 [mobileShowContent, setMobileShowContent] = useState(false);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [pendingHighlight, setPendingHighlight] = useState<{ tab: Tab; label: string; pluginId?: string } | null>(null); const [pendingHighlight, setPendingHighlight] = useState<{ tab: Tab; label: string; pluginId?: string } | null>(null);
@@ -934,7 +947,7 @@ export default function SettingsPage() {
return ( return (
<div key={tab.id}> <div key={tab.id}>
<button <button
onClick={() => setActiveTab(tab.id)} onClick={() => handleTabSelect(tab.id)}
className={cn( className={cn(
'w-full text-left px-3 py-2 rounded-md text-sm transition-colors duration-150 flex items-center gap-2.5', 'w-full text-left px-3 py-2 rounded-md text-sm transition-colors duration-150 flex items-center gap-2.5',
effectiveActiveTab === tab.id effectiveActiveTab === tab.id
+2 -2
View File
@@ -904,11 +904,11 @@ export function Sidebar({
}; };
const openFolderSettings = () => { const openFolderSettings = () => {
try { localStorage.setItem('settings-active-tab', 'folders'); } catch { /* */ } try { sessionStorage.setItem('settings-deep-link-tab', 'folders'); } catch { /* */ }
router.push('/settings'); router.push('/settings');
}; };
const openKeywordSettings = () => { const openKeywordSettings = () => {
try { localStorage.setItem('settings-active-tab', 'keywords'); } catch { /* */ } try { sessionStorage.setItem('settings-deep-link-tab', 'keywords'); } catch { /* */ }
router.push('/settings'); router.push('/settings');
}; };
+1 -1
View File
@@ -98,7 +98,7 @@ async function migrateAdminJson(): Promise<boolean> {
lastLogin: data.lastLogin ?? null, lastLogin: data.lastLogin ?? null,
passwordChangedAt: data.passwordChangedAt ?? now, passwordChangedAt: data.passwordChangedAt ?? now,
}; };
const configData: AdminConfigData = { passwordHash: data.passwordHash }; const configData: AdminConfigData = { passwordHash: data.passwordHash, passwordHashFile: undefined };
await ensureStateDir(); await ensureStateDir();
const statePath = getStatePath('admin-state.json'); const statePath = getStatePath('admin-state.json');
+14 -4
View File
@@ -9,6 +9,7 @@ import {
assertWritable, assertWritable,
} from './paths'; } from './paths';
import type { AdminConfigData, AdminStateData } from './types'; import type { AdminConfigData, AdminStateData } from './types';
import { readFileEnv } from '../read-file-env';
const SCRYPT_KEYLEN = 64; const SCRYPT_KEYLEN = 64;
const SCRYPT_COST = 16384; // 2^14 const SCRYPT_COST = 16384; // 2^14
@@ -146,7 +147,7 @@ export async function initAdminPassword(): Promise<boolean> {
} }
const hash = isHashed(envPassword) ? envPassword : await hashPassword(envPassword); const hash = isHashed(envPassword) ? envPassword : await hashPassword(envPassword);
cachedConfig = { passwordHash: hash }; cachedConfig = { passwordHash: hash, passwordHashFile: undefined };
cachedState = freshState(); cachedState = freshState();
await writeConfigData(cachedConfig); await writeConfigData(cachedConfig);
await writeStateData(cachedState); await writeStateData(cachedState);
@@ -165,7 +166,16 @@ export async function initAdminPassword(): Promise<boolean> {
export async function verifyAdminPassword(password: string): Promise<boolean> { export async function verifyAdminPassword(password: string): Promise<boolean> {
if (!cachedConfig) cachedConfig = await readConfigData(); if (!cachedConfig) cachedConfig = await readConfigData();
if (!cachedConfig) return false; 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; if (!valid) return false;
const hash = await hashPassword(newPassword); const hash = await hashPassword(newPassword);
cachedConfig = { passwordHash: hash }; cachedConfig = { passwordHash: hash, passwordHashFile: undefined };
await writeConfigData(cachedConfig); await writeConfigData(cachedConfig);
cachedState = { cachedState = {
@@ -205,7 +215,7 @@ export async function setInitialAdminPassword(
const existing = await readConfigData(); const existing = await readConfigData();
if (existing && !options.allowOverwrite) return false; if (existing && !options.allowOverwrite) return false;
const hash = await hashPassword(newPassword); const hash = await hashPassword(newPassword);
cachedConfig = { passwordHash: hash }; cachedConfig = { passwordHash: hash, passwordHashFile: undefined };
cachedState = freshState(); cachedState = freshState();
await writeConfigData(cachedConfig); await writeConfigData(cachedConfig);
await writeStateData(cachedState); await writeStateData(cachedState);
+2 -1
View File
@@ -6,7 +6,8 @@
* is config; mutable timestamps live in AdminStateData. * is config; mutable timestamps live in AdminStateData.
*/ */
export interface AdminConfigData { export interface AdminConfigData {
passwordHash: string; passwordHash: string | undefined;
passwordHashFile: string | undefined;
} }
/** /**