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:
1 parent
5c72bab3a1
commit
ff0fad925c
12 files changed
+154
-23
No files matched your search
@@ -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)),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Vendored
+2
-1
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user