refactor(security): replace screen lock with separate startup and idle lock settings

- Updated security settings to introduce distinct options for enabling startup and idle locks, replacing the previous combined screen lock setting.
- Modified relevant components and contexts to accommodate the new settings structure.
- Implemented migration logic to transition from the legacy screen lock to the new settings, ensuring backward compatibility.
- Enhanced user interface to reflect the changes in security options.
This commit is contained in:
Kang committed 2026-08-27 11:50:54 +08:00
1 parent 5c72bab3a1
commit ff0fad925c
12 files changed
+154 -23

No files matched your search

+12 -4
View File
@@ -47,8 +47,14 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.and_then(Value::as_object_mut)
.ok_or("settings.security is missing or not an object")?;
let was_enabled = security
.get("enable_screen_lock")
let was_startup_enabled = security
.get("enable_startup_lock")
.or_else(|| security.get("enable_screen_lock"))
.and_then(Value::as_bool)
.unwrap_or(false);
let was_idle_enabled = security
.get("enable_idle_lock")
.or_else(|| security.get("enable_screen_lock"))
.and_then(Value::as_bool)
.unwrap_or(false);
let old_idle = security
@@ -56,7 +62,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.and_then(Value::as_u64)
.unwrap_or(0);
(settings, was_enabled, old_idle)
(settings, was_startup_enabled || was_idle_enabled, old_idle)
};
let security = settings
@@ -65,7 +71,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.and_then(Value::as_object_mut)
.ok_or("settings.security is missing or not an object")?;
security.insert("enable_screen_lock".to_string(), Value::Bool(false));
security.insert("enable_startup_lock".to_string(), Value::Bool(false));
security.insert("enable_idle_lock".to_string(), Value::Bool(false));
security.remove("enable_screen_lock");
security.insert(
"idle_lock_minutes".to_string(),
Value::Number(serde_json::Number::from(0)),
+3
View File
@@ -125,6 +125,9 @@ pub fn load_app_settings(app: &AppHandle) -> AppResult<AppSettings> {
if normalize_ai_settings(&mut settings.ai) {
migrated = true;
}
if settings.security.migrate_legacy_screen_lock() {
migrated = true;
}
if migrate_terminal_timestamp_format(&raw_settings, &mut settings.terminal) {
migrated = true;
}
+79 -2
View File
@@ -6,9 +6,14 @@ pub struct SecuritySettings {
#[serde(default = "default_true")]
pub use_os_keyring: bool,
#[serde(default = "default_false")]
pub enable_screen_lock: bool,
pub enable_startup_lock: bool,
#[serde(default = "default_false")]
pub enable_idle_lock: bool,
#[serde(default)]
pub idle_lock_minutes: u32,
/// Legacy combined lock switch. It is read for migration and never written back.
#[serde(rename = "enable_screen_lock", default, skip_serializing)]
legacy_enable_screen_lock: Option<bool>,
/// Master password used to derive the wrapping key for `master.key`.
/// Also serves as the lock-screen password when set.
#[serde(default)]
@@ -25,10 +30,82 @@ impl Default for SecuritySettings {
fn default() -> Self {
Self {
use_os_keyring: true,
enable_screen_lock: false,
enable_startup_lock: false,
enable_idle_lock: false,
idle_lock_minutes: 0,
legacy_enable_screen_lock: None,
master_password: None,
host_key_policy: default_host_key_policy(),
}
}
}
impl SecuritySettings {
/// Migrates the legacy combined screen-lock setting into both independent modes.
///
/// The legacy field is intentionally not serialized, so this is only applied when
/// loading settings or importing an older portable snapshot.
pub fn migrate_legacy_screen_lock(&mut self) -> bool {
let Some(enabled) = self.legacy_enable_screen_lock.take() else {
return false;
};
self.enable_startup_lock = enabled;
self.enable_idle_lock = enabled;
true
}
}
#[cfg(test)]
mod tests {
use super::SecuritySettings;
#[test]
fn defaults_disable_both_lock_modes() {
let settings = SecuritySettings::default();
assert!(!settings.enable_startup_lock);
assert!(!settings.enable_idle_lock);
assert_eq!(settings.idle_lock_minutes, 0);
}
#[test]
fn migrates_legacy_enabled_screen_lock_to_both_modes() {
let mut settings: SecuritySettings = serde_json::from_value(serde_json::json!({
"enable_screen_lock": true,
"idle_lock_minutes": 15
}))
.expect("legacy settings should deserialize");
assert!(settings.migrate_legacy_screen_lock());
assert!(settings.enable_startup_lock);
assert!(settings.enable_idle_lock);
assert_eq!(settings.idle_lock_minutes, 15);
}
#[test]
fn migrates_legacy_disabled_screen_lock_to_both_modes() {
let mut settings: SecuritySettings = serde_json::from_value(serde_json::json!({
"enable_screen_lock": false
}))
.expect("legacy settings should deserialize");
assert!(settings.migrate_legacy_screen_lock());
assert!(!settings.enable_startup_lock);
assert!(!settings.enable_idle_lock);
}
#[test]
fn new_lock_modes_are_serialized_without_legacy_field() {
let settings = SecuritySettings {
enable_startup_lock: true,
enable_idle_lock: false,
..SecuritySettings::default()
};
let value = serde_json::to_value(settings).expect("settings should serialize");
assert_eq!(value["enable_startup_lock"], true);
assert_eq!(value["enable_idle_lock"], false);
assert!(value.get("enable_screen_lock").is_none());
}
}
@@ -182,6 +182,26 @@ mod tests {
);
}
#[test]
fn portable_settings_migrate_legacy_screen_lock_modes() {
let portable = sample_portable_settings();
let mut raw = serde_json::to_value(portable).expect("portable settings should serialize");
let security = raw
.get_mut("security")
.and_then(serde_json::Value::as_object_mut)
.expect("security settings should be an object");
security.remove("enable_startup_lock");
security.remove("enable_idle_lock");
security.insert("enable_screen_lock".to_string(), serde_json::json!(true));
let decoded: PortableAppSettings =
serde_json::from_value(raw).expect("legacy portable settings should deserialize");
let merged = decoded.apply_to(AppSettings::default(), &PortableSnapshotKind::Backup);
assert!(merged.security.enable_startup_lock);
assert!(merged.security.enable_idle_lock);
}
#[test]
fn sync_portable_settings_strip_device_local_paths() {
let mut current = AppSettings::default();
@@ -367,6 +367,8 @@ impl PortableAppSettings {
mut current: AppSettings,
snapshot_kind: &PortableSnapshotKind,
) -> AppSettings {
let mut snapshot_security = self.security;
snapshot_security.migrate_legacy_screen_lock();
let master_password = current.security.master_password.clone();
let ui_state = current.ui.clone();
let device_appearance = current.appearance.clone();
@@ -377,7 +379,7 @@ impl PortableAppSettings {
current.proxy = self.proxy;
current.search = self.search;
current.translation = self.translation;
current.security = self.security;
current.security = snapshot_security;
current.security.master_password = master_password;
current.terminal = self.terminal;
current.interaction = self.interaction;
+1 -1
View File
@@ -385,7 +385,7 @@ fn rebuild_root_menu(
menu.append(&settings_item)?;
menu.append(&minimize_to_tray)?;
if settings.security.enable_screen_lock {
if settings.security.enable_startup_lock || settings.security.enable_idle_lock {
let lock_screen = new_menu_item(app, MENU_LOCK_SCREEN, strings.lock_screen, true)?;
menu.append(&lock_screen)?;
}
+7 -3
View File
@@ -316,7 +316,7 @@ function App() {
// Idle auto-lock
useIdleLock(
appSettings.security.enable_screen_lock ? appSettings.security.idle_lock_minutes : 0,
appSettings.security.enable_idle_lock ? appSettings.security.idle_lock_minutes : 0,
isLocked,
() => setIsLocked(true),
);
@@ -1936,10 +1936,14 @@ function App() {
}, []);
const handleLockScreen = useCallback(() => {
if (appSettings.security.enable_screen_lock) {
if (appSettings.security.enable_startup_lock || appSettings.security.enable_idle_lock) {
setIsLocked(true);
}
}, [appSettings.security.enable_screen_lock, setIsLocked]);
}, [
appSettings.security.enable_idle_lock,
appSettings.security.enable_startup_lock,
setIsLocked,
]);
const persistWorkspaceLayoutNow = useCallback(async () => {
if (!settingsLoaded || !startupRestoreComplete || !terminalWindowsRestoredRef.current) {
+1 -1
View File
@@ -19,7 +19,7 @@ interface LockScreenProps {
}
/**
* Full-screen overlay shown when the app has been locked due to inactivity.
* Full-screen overlay shown when the app has been locked at startup or due to inactivity.
* If a lock password is set, the user must enter it to unlock (verified by backend).
* Otherwise, clicking the unlock button is sufficient.
*/
+19 -5
View File
@@ -115,20 +115,34 @@ export function SecurityTab() {
<SettingSection title={t("settings.sessionSecurity")} contentClassName="space-y-5">
<SettingRow
label={t("settings.enableScreenLock")}
desc={t("settings.enableScreenLockDesc")}
label={t("settings.enableStartupLock")}
desc={t("settings.enableStartupLockDesc")}
>
<SettingSwitch
checked={appSettings.security.enable_screen_lock}
checked={appSettings.security.enable_startup_lock}
onChange={(v) =>
updateAppSettings({
security: { ...appSettings.security, enable_screen_lock: v },
security: { ...appSettings.security, enable_startup_lock: v },
})
}
/>
</SettingRow>
{appSettings.security.enable_screen_lock && (
<SettingRow
label={t("settings.enableIdleLock")}
desc={t("settings.enableIdleLockDesc")}
>
<SettingSwitch
checked={appSettings.security.enable_idle_lock}
onChange={(v) =>
updateAppSettings({
security: { ...appSettings.security, enable_idle_lock: v },
})
}
/>
</SettingRow>
{appSettings.security.enable_idle_lock && (
<SettingRow
label={t("settings.idleLockMinutes")}
desc={t("settings.idleLockMinutesDesc")}
+4 -3
View File
@@ -123,7 +123,8 @@ const DEFAULT_APP_SETTINGS: AppSettings = {
},
security: {
use_os_keyring: true,
enable_screen_lock: false,
enable_startup_lock: false,
enable_idle_lock: false,
idle_lock_minutes: 0,
host_key_policy: "prompt",
},
@@ -453,7 +454,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
setLoggerLevel(normalized.diagnostics.level);
appSettingsLoaded.current = true;
setSettingsLoaded(true);
if (isPrimaryMainWindow() && normalized.security?.enable_screen_lock) {
if (isPrimaryMainWindow() && normalized.security?.enable_startup_lock) {
setIsLocked(true);
}
})
@@ -1184,7 +1185,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
setActiveTabId(restoredTabs[restoredTabs.length - 1].id);
}
if (appSettings.security.enable_screen_lock && isLocked) {
if (appSettings.security.enable_startup_lock && isLocked) {
pendingLockedStartupRestoreTabsRef.current = restoredTabs;
} else {
restoreSessionsForTabs(restoredTabs);
+3 -2
View File
@@ -83,7 +83,8 @@ const DEFAULT_APP_SETTINGS: AppSettings = {
},
security: {
use_os_keyring: true,
enable_screen_lock: false,
enable_startup_lock: false,
enable_idle_lock: false,
idle_lock_minutes: 0,
host_key_policy: "prompt",
},
@@ -317,7 +318,7 @@ export function ChildAppProvider({ children }: { children: ReactNode }) {
}, [appSettings.ui?.language]);
useIdleLock(
appSettings.security.enable_screen_lock
appSettings.security.enable_idle_lock
? appSettings.security.idle_lock_minutes
: 0,
isLocked,
+2 -1
View File
@@ -1273,7 +1273,8 @@ export interface TranslateResult {
export interface SecuritySettings {
use_os_keyring: boolean;
enable_screen_lock: boolean;
enable_startup_lock: boolean;
enable_idle_lock: boolean;
idle_lock_minutes: number;
master_password?: string;
host_key_policy: string;