feat: add desktop UI password setting

Adds optional desktop UI password protection
Starts Electron with the saved UI password
Explains login session duration in settings
This commit is contained in:
Bohdan Triapitsyn
2026-05-25 15:20:56 +03:00
parent 1b141b17da
commit 89bce715c5
12 changed files with 78 additions and 4 deletions
@@ -2,6 +2,7 @@ import * as React from 'react';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { getDesktopLanAddress, isDesktopLocalOriginActive, isDesktopShell, restartDesktopApp } from '@/lib/desktop';
import { useI18n } from '@/lib/i18n';
@@ -10,6 +11,8 @@ export const DesktopNetworkSettings: React.FC = () => {
const isLocalDesktop = isDesktopShell() && isDesktopLocalOriginActive();
const [savedValue, setSavedValue] = React.useState(false);
const [draftValue, setDraftValue] = React.useState(false);
const [savedPassword, setSavedPassword] = React.useState('');
const [draftPassword, setDraftPassword] = React.useState('');
const [isLoading, setIsLoading] = React.useState(true);
const [isSaving, setIsSaving] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
@@ -32,14 +35,20 @@ export const DesktopNetworkSettings: React.FC = () => {
throw new Error(t('settings.openchamber.desktopNetwork.error.loadFailed'));
}
const data = (await response.json().catch(() => null)) as null | { desktopLanAccessEnabled?: unknown };
const data = (await response.json().catch(() => null)) as null | {
desktopLanAccessEnabled?: unknown;
desktopUiPassword?: unknown;
};
if (cancelled) {
return;
}
const enabled = data?.desktopLanAccessEnabled === true;
const password = typeof data?.desktopUiPassword === 'string' ? data.desktopUiPassword : '';
setSavedValue(enabled);
setDraftValue(enabled);
setSavedPassword(password);
setDraftPassword(password);
setError(null);
} catch (cause) {
if (!cancelled) {
@@ -77,7 +86,7 @@ export const DesktopNetworkSettings: React.FC = () => {
};
}, [draftValue, isLocalDesktop]);
const isDirty = draftValue !== savedValue;
const isDirty = draftValue !== savedValue || draftPassword !== savedPassword;
const currentPort = React.useMemo(() => {
if (typeof window === 'undefined') {
return null;
@@ -107,7 +116,10 @@ export const DesktopNetworkSettings: React.FC = () => {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ desktopLanAccessEnabled: draftValue }),
body: JSON.stringify({
desktopLanAccessEnabled: draftValue,
desktopUiPassword: draftPassword,
}),
});
if (!response.ok) {
@@ -115,6 +127,7 @@ export const DesktopNetworkSettings: React.FC = () => {
}
setSavedValue(draftValue);
setSavedPassword(draftPassword);
const restarted = await restartDesktopApp();
if (!restarted) {
@@ -124,7 +137,7 @@ export const DesktopNetworkSettings: React.FC = () => {
setError(cause instanceof Error ? cause.message : t('settings.openchamber.desktopNetwork.error.saveFailed'));
setIsSaving(false);
}
}, [draftValue, isDirty, t]);
}, [draftPassword, draftValue, isDirty, t]);
if (!isLocalDesktop) {
return null;
@@ -137,6 +150,24 @@ export const DesktopNetworkSettings: React.FC = () => {
</div>
<section className="space-y-2 px-2 pb-2 pt-0">
<div className="space-y-1 py-1.5">
<label className="typography-ui-label text-foreground" htmlFor="desktop-ui-password">
{t('settings.openchamber.desktopPassword.field.password')}
</label>
<Input
id="desktop-ui-password"
type="password"
className="h-7 max-w-sm"
value={draftPassword}
onChange={(event) => setDraftPassword(event.target.value)}
placeholder={t('settings.openchamber.desktopPassword.field.passwordPlaceholder')}
disabled={isLoading || isSaving}
/>
<div className="typography-micro text-muted-foreground/70">
{t('settings.openchamber.desktopPassword.field.passwordDescription')}
</div>
</div>
<div
className="group flex cursor-pointer items-start gap-2 py-1.5"
role="button"