feat: add auto-lock feature with customizable timeout settings and update UI for security preferences

This commit is contained in:
shuaiplus
2026-04-24 15:27:46 +08:00
parent d40b0514fd
commit acd59a7387
6 changed files with 233 additions and 49 deletions
+4
View File
@@ -45,6 +45,7 @@ export interface AppMainRoutesProps {
users: AdminUser[];
invites: AdminInvite[];
totpEnabled: boolean;
lockTimeoutMinutes: 0 | 1 | 5 | 15 | 30;
authorizedDevices: AuthorizedDevice[];
authorizedDevicesLoading: boolean;
onNavigate: (path: string) => void;
@@ -96,6 +97,7 @@ export interface AppMainRoutesProps {
onGetRecoveryCode: (masterPassword: string) => Promise<string>;
onGetApiKey: (masterPassword: string) => Promise<string>;
onRotateApiKey: (masterPassword: string) => Promise<string>;
onLockTimeoutChange: (minutes: 0 | 1 | 5 | 15 | 30) => void;
onRefreshAuthorizedDevices: () => Promise<void>;
onRenameAuthorizedDevice: (device: AuthorizedDevice, name: string) => Promise<void>;
onRevokeDeviceTrust: (device: AuthorizedDevice) => void;
@@ -222,6 +224,7 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
<SettingsPage
profile={props.profile}
totpEnabled={props.totpEnabled}
lockTimeoutMinutes={props.lockTimeoutMinutes}
onChangePassword={props.onChangePassword}
onSavePasswordHint={props.onSavePasswordHint}
onEnableTotp={props.onEnableTotp}
@@ -229,6 +232,7 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
onGetRecoveryCode={props.onGetRecoveryCode}
onGetApiKey={props.onGetApiKey}
onRotateApiKey={props.onRotateApiKey}
onLockTimeoutChange={props.onLockTimeoutChange}
onNotify={props.onNotify}
/>
</Suspense>
+46 -19
View File
@@ -9,6 +9,7 @@ import ConfirmDialog from '@/components/ConfirmDialog';
interface SettingsPageProps {
profile: Profile;
totpEnabled: boolean;
lockTimeoutMinutes: 0 | 1 | 5 | 15 | 30;
onChangePassword: (currentPassword: string, nextPassword: string, nextPassword2: string) => Promise<void>;
onSavePasswordHint: (masterPasswordHint: string) => Promise<void>;
onEnableTotp: (secret: string, token: string) => Promise<void>;
@@ -16,9 +17,18 @@ interface SettingsPageProps {
onGetRecoveryCode: (masterPassword: string) => Promise<string>;
onGetApiKey: (masterPassword: string) => Promise<string>;
onRotateApiKey: (masterPassword: string) => Promise<string>;
onLockTimeoutChange: (minutes: 0 | 1 | 5 | 15 | 30) => void;
onNotify?: (type: 'success' | 'error', text: string) => void;
}
const LOCK_TIMEOUT_OPTIONS = [
{ value: 1, labelKey: 'txt_lock_after_1_minute' },
{ value: 5, labelKey: 'txt_lock_after_5_minutes' },
{ value: 15, labelKey: 'txt_lock_after_15_minutes' },
{ value: 30, labelKey: 'txt_lock_after_30_minutes' },
{ value: 0, labelKey: 'txt_lock_after_never' },
] as const;
function randomBase32Secret(length: number): string {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let out = '';
@@ -124,25 +134,42 @@ export default function SettingsPage(props: SettingsPageProps) {
return (
<div className="stack">
<section className="card">
<h3>{t('txt_profile')}</h3>
<label className="field">
<span>{t('txt_password_hint_optional')}</span>
<input
className="input"
maxLength={120}
value={passwordHint}
placeholder={t('txt_password_hint_placeholder')}
onInput={(e) => setPasswordHint((e.currentTarget as HTMLInputElement).value)}
/>
<div className="field-help">{t('txt_password_hint_register_help')}</div>
</label>
<button
type="button"
className="btn btn-secondary"
onClick={() => void props.onSavePasswordHint(passwordHint)}
>
{t('txt_save_profile')}
</button>
<h3>{t('txt_security_preferences')}</h3>
<div className="field-grid">
<label className="field">
<span>{t('txt_auto_lock')}</span>
<select
className="input"
value={String(props.lockTimeoutMinutes)}
onInput={(e) => props.onLockTimeoutChange(Number((e.currentTarget as HTMLSelectElement).value) as 0 | 1 | 5 | 15 | 30)}
>
{LOCK_TIMEOUT_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{t(option.labelKey)}
</option>
))}
</select>
<div className="field-help">{t('txt_auto_lock_description')}</div>
</label>
<label className="field">
<span>{t('txt_password_hint_optional')}</span>
<input
className="input"
maxLength={120}
value={passwordHint}
placeholder={t('txt_password_hint_placeholder')}
onInput={(e) => setPasswordHint((e.currentTarget as HTMLInputElement).value)}
/>
<div className="field-help">{t('txt_password_hint_register_help')}</div>
<button
type="button"
className="btn btn-secondary"
onClick={() => void props.onSavePasswordHint(passwordHint)}
>
{t('txt_save_profile')}
</button>
</label>
</div>
</section>
<section className="card">