Merge branch 'main' of https://github.com/bulwarkmail/webmail
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user