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:
@@ -749,6 +749,7 @@ const spawnLocalServer = async () => {
|
||||
// warning and persists the flag via /api/config/settings.
|
||||
const lanAccessEnabled = settings.desktopLanAccessEnabled === true;
|
||||
const bindHost = lanAccessEnabled ? '0.0.0.0' : '127.0.0.1';
|
||||
const desktopUiPassword = typeof settings.desktopUiPassword === 'string' ? settings.desktopUiPassword.trim() : '';
|
||||
|
||||
// Probe before starting the server — main() in the server module sets up a
|
||||
// lot of global state before binding, and calling it twice after a listen
|
||||
@@ -773,6 +774,11 @@ const spawnLocalServer = async () => {
|
||||
process.env.OPENCHAMBER_DIST_DIR = resolveWebDistDir();
|
||||
process.env.OPENCHAMBER_RUNTIME = 'desktop';
|
||||
process.env.OPENCHAMBER_DESKTOP_NOTIFY = 'true';
|
||||
if (desktopUiPassword) {
|
||||
process.env.OPENCHAMBER_UI_PASSWORD = desktopUiPassword;
|
||||
} else {
|
||||
delete process.env.OPENCHAMBER_UI_PASSWORD;
|
||||
}
|
||||
process.env.OPENCHAMBER_SKIP_API_COMPRESSION = process.env.OPENCHAMBER_SKIP_API_COMPRESSION || 'true';
|
||||
process.env.NO_PROXY = process.env.NO_PROXY || 'localhost,127.0.0.1';
|
||||
process.env.no_proxy = process.env.no_proxy || 'localhost,127.0.0.1';
|
||||
@@ -782,6 +788,7 @@ const spawnLocalServer = async () => {
|
||||
const handle = await startWebUiServer({
|
||||
port: chosenPort,
|
||||
host: bindHost,
|
||||
uiPassword: desktopUiPassword || null,
|
||||
attachSignals: false,
|
||||
exitOnShutdown: false,
|
||||
onDesktopNotification: (payload) => maybeShowNativeNotification(payload),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -52,6 +52,7 @@ export type DesktopSettings = {
|
||||
// Optional absolute path to `opencode` binary.
|
||||
opencodeBinary?: string;
|
||||
desktopLanAccessEnabled?: boolean;
|
||||
desktopUiPassword?: string;
|
||||
projects?: ProjectEntry[];
|
||||
activeProjectId?: string;
|
||||
approvedDirectories?: string[];
|
||||
|
||||
@@ -759,6 +759,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccess': 'Let other devices on your local network open this app',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessDescription': 'Restarts the app so phones, tablets, and other computers on your Wi-Fi can open it.',
|
||||
'settings.openchamber.desktopNetwork.field.warning': 'Warning: while enabled, the app is reachable by anyone on the same local network.',
|
||||
'settings.openchamber.desktopPassword.field.password': 'Desktop UI Password',
|
||||
'settings.openchamber.desktopPassword.field.passwordPlaceholder': 'No password required',
|
||||
'settings.openchamber.desktopPassword.field.passwordDescription': 'OpenChamber asks after restart, then when the login session expires: after 12 hours, or 7 days with Trust this device. Leave empty to disable login.',
|
||||
'settings.openchamber.desktopNetwork.hint.openAfterRestart': 'After restart, open from another device: ',
|
||||
'settings.openchamber.desktopNetwork.hint.openNow': 'Open from another device: ',
|
||||
'settings.openchamber.desktopNetwork.actions.saveAndRestart': 'Save + Restart',
|
||||
|
||||
@@ -726,6 +726,9 @@ export const settingsDict = {
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccess": "Permitir que otros dispositivos en tu red local abran esta aplicación",
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccessDescription": "Reinicia la aplicación para que los teléfonos, tablets y otros ordenadores en tu Wi-Fi puedan abrirla.",
|
||||
"settings.openchamber.desktopNetwork.field.warning": "Advertencia: mientras esté habilitado, la aplicación es accesible por cualquiera en la misma red local.",
|
||||
"settings.openchamber.desktopPassword.field.password": "Contraseña de UI de escritorio",
|
||||
"settings.openchamber.desktopPassword.field.passwordPlaceholder": "No se requiere contraseña",
|
||||
"settings.openchamber.desktopPassword.field.passwordDescription": "OpenChamber la pide después del reinicio y luego cuando vence la sesión: tras 12 horas, o 7 días con Confiar en este dispositivo. Déjalo vacío para desactivar el inicio de sesión.",
|
||||
"settings.openchamber.desktopNetwork.hint.openAfterRestart": "Después del reinicio, abre desde otro dispositivo: ",
|
||||
"settings.openchamber.desktopNetwork.hint.openNow": "Abrir desde otro dispositivo: ",
|
||||
"settings.openchamber.desktopNetwork.actions.saveAndRestart": "Guardar y reiniciar",
|
||||
|
||||
@@ -726,6 +726,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccess': '로컬 네트워크의 다른 기기에서 이 앱 열기 허용',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessDescription': '휴대폰, 태블릿, Wi-Fi의 다른 컴퓨터에서 열 수 있도록 앱을 다시 시작합니다.',
|
||||
'settings.openchamber.desktopNetwork.field.warning': '경고: 활성화된 동안 같은 로컬 네트워크의 누구나 앱에 접속할 수 있습니다.',
|
||||
'settings.openchamber.desktopPassword.field.password': 'Desktop UI 비밀번호',
|
||||
'settings.openchamber.desktopPassword.field.passwordPlaceholder': '비밀번호 필요 없음',
|
||||
'settings.openchamber.desktopPassword.field.passwordDescription': 'OpenChamber는 다시 시작 후 비밀번호를 요청하고, 이후 로그인 세션이 만료되면 다시 요청합니다. 기본 12시간, 이 디바이스 신뢰 선택 시 7일입니다. 로그인을 끄려면 비워 두세요.',
|
||||
'settings.openchamber.desktopNetwork.hint.openAfterRestart': '다시 시작 후 다른 기기에서 열기: ',
|
||||
'settings.openchamber.desktopNetwork.hint.openNow': '다른 기기에서 열기: ',
|
||||
'settings.openchamber.desktopNetwork.actions.saveAndRestart': '저장 + 다시 시작',
|
||||
|
||||
@@ -583,6 +583,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessAria': 'Zezwól na dostęp LAN do sidecara pulpitu',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessDescription': 'Restartuje aplikację, aby telefony, tablety i inne komputery w Twojej sieci Wi-Fi mogły ją otworzyć.',
|
||||
'settings.openchamber.desktopNetwork.field.warning': 'Ostrzeżenie: po włączeniu aplikacja jest dostępna dla każdego w tej samej sieci lokalnej.',
|
||||
'settings.openchamber.desktopPassword.field.password': 'Hasło UI pulpitu',
|
||||
'settings.openchamber.desktopPassword.field.passwordPlaceholder': 'Hasło nie jest wymagane',
|
||||
'settings.openchamber.desktopPassword.field.passwordDescription': 'OpenChamber pyta po restarcie, a potem po wygaśnięciu sesji logowania: po 12 godzinach albo po 7 dniach z opcją Zaufaj temu urządzeniu. Zostaw puste, aby wyłączyć logowanie.',
|
||||
'settings.openchamber.desktopNetwork.hint.openAfterRestart': 'Po restarcie otwórz z innego urządzenia: ',
|
||||
'settings.openchamber.desktopNetwork.hint.openNow': 'Otwórz z innego urządzenia: ',
|
||||
'settings.openchamber.desktopNetwork.title': 'Dostęp sieciowy pulpitu',
|
||||
|
||||
@@ -726,6 +726,9 @@ export const settingsDict = {
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccess": "Permitir que outros dispositivos na sua rede local abram este aplicativo",
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccessDescription": "Reinicia o aplicativo para que os telefones, tablets e outros computadores em seu Wi-Fi possam abri-lo.",
|
||||
"settings.openchamber.desktopNetwork.field.warning": "Aviso: enquanto estiver habilitado, o aplicativo ficará acessível a qualquer pessoa na mesma rede local.",
|
||||
"settings.openchamber.desktopPassword.field.password": "Senha da UI do desktop",
|
||||
"settings.openchamber.desktopPassword.field.passwordPlaceholder": "Nenhuma senha obrigatória",
|
||||
"settings.openchamber.desktopPassword.field.passwordDescription": "O OpenChamber pede após reiniciar e depois quando a sessão expira: em 12 horas, ou 7 dias com Confiar neste dispositivo. Deixe vazio para desativar o login.",
|
||||
"settings.openchamber.desktopNetwork.hint.openAfterRestart": "Depois do reinício, abra de outro dispositivo: ",
|
||||
"settings.openchamber.desktopNetwork.hint.openNow": "Abrir de outro dispositivo: ",
|
||||
"settings.openchamber.desktopNetwork.actions.saveAndRestart": "Salvar e reiniciar",
|
||||
|
||||
@@ -726,6 +726,9 @@ export const settingsDict = {
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccess": "Дозволити іншим пристроям у локальній мережі відкривати цей застосунок",
|
||||
"settings.openchamber.desktopNetwork.field.allowLanAccessDescription": "Перезапускає застосунок, щоб телефони, планшети та інші комп’ютери в мережі Wi-Fi могли його відкрити.",
|
||||
"settings.openchamber.desktopNetwork.field.warning": "Попередження: якщо це ввімкнено, застосунок доступний усім у тій самій локальній мережі.",
|
||||
"settings.openchamber.desktopPassword.field.password": "Пароль для десктопного UI",
|
||||
"settings.openchamber.desktopPassword.field.passwordPlaceholder": "Пароль не потрібен",
|
||||
"settings.openchamber.desktopPassword.field.passwordDescription": "OpenChamber попросить пароль після перезапуску, а потім коли сесія логіну спливе: через 12 годин або через 7 днів із «Довіряти цьому пристрою». Залиште порожнім, щоб вимкнути логін.",
|
||||
"settings.openchamber.desktopNetwork.hint.openAfterRestart": "Після перезавантаження відкрити з іншого пристрою: ",
|
||||
"settings.openchamber.desktopNetwork.hint.openNow": "Відкрити з іншого пристрою: ",
|
||||
"settings.openchamber.desktopNetwork.actions.saveAndRestart": "Зберегти й перезапустити",
|
||||
|
||||
@@ -726,6 +726,9 @@ export const settingsDict = {
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccess': '允许你本地网络中的其他设备打开此应用',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessDescription': '会重启应用,以便手机、平板和同一 Wi‑Fi 下的其他电脑访问。',
|
||||
'settings.openchamber.desktopNetwork.field.warning': '警告:启用后,同一本地网络中的任何人都可访问此应用。',
|
||||
'settings.openchamber.desktopPassword.field.password': '桌面 UI 密码',
|
||||
'settings.openchamber.desktopPassword.field.passwordPlaceholder': '不需要密码',
|
||||
'settings.openchamber.desktopPassword.field.passwordDescription': 'OpenChamber 会在重启后要求输入密码,之后在登录会话过期时再次要求:12 小时后,或选择“信任此设备”后 7 天。留空可关闭登录。',
|
||||
'settings.openchamber.desktopNetwork.hint.openAfterRestart': '重启后可在其他设备打开:',
|
||||
'settings.openchamber.desktopNetwork.hint.openNow': '可在其他设备打开:',
|
||||
'settings.openchamber.desktopNetwork.actions.saveAndRestart': '保存并重启',
|
||||
|
||||
@@ -115,6 +115,9 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
if (typeof candidate.desktopLanAccessEnabled === 'boolean') {
|
||||
result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled;
|
||||
}
|
||||
if (typeof candidate.desktopUiPassword === 'string') {
|
||||
result.desktopUiPassword = candidate.desktopUiPassword.trim();
|
||||
}
|
||||
if (Array.isArray(candidate.projects)) {
|
||||
const projects = sanitizeProjects(candidate.projects);
|
||||
if (projects) {
|
||||
|
||||
@@ -52,6 +52,17 @@ describe('settings helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts desktopUiPassword as a persisted shared setting', () => {
|
||||
const helpers = createTestHelpers();
|
||||
|
||||
expect(helpers.sanitizeSettingsUpdate({ desktopUiPassword: ' secret ' })).toEqual({
|
||||
desktopUiPassword: 'secret',
|
||||
});
|
||||
expect(helpers.sanitizeSettingsUpdate({ desktopUiPassword: '' })).toEqual({
|
||||
desktopUiPassword: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts mobileKeyboardMode as a persisted shared setting', () => {
|
||||
const helpers = createTestHelpers();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user