Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 71 additions & 67 deletions src/renderer/src/components/security-settings-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,45 @@
import { type ReactNode } from 'react'
import { useState, type ReactNode } from 'react'

import { CheckIcon } from 'lucide-react'
import { toast } from 'sonner'
import { LockIcon } from 'lucide-react'
import { useShallow } from 'zustand/react/shallow'

import { notvex } from '@/lib/ipc'
import { usePrefsStore } from '@/store/prefs.store'
import { ChangePasswordDialog } from './change-password-dialog'
import { KeyFileSetting } from './settings/KeyFileSetting'
import { Button } from './ui/button'
import { Checkbox } from './ui/checkbox'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue
} from './ui/select'
import { Separator } from './ui/separator'

interface Props {
open: boolean
onClose: () => void
}

async function handleOpenBackupsFolder(): Promise<void> {
const res = await notvex.vault.openBackupsFolder()
if (!res.success) toast.error(res.error)
}

export function SecuritySettingsDialog({ open, onClose }: Props): ReactNode {
const { setPref } = usePrefsStore()
const { lockOnMinimize, allowScreenCapture } = usePrefsStore(
const { autoLockMinutes, lockOnMinimize, allowScreenCapture } = usePrefsStore(
useShallow((s) => ({
autoLockMinutes: s.autoLockMinutes,
lockOnMinimize: s.lockOnMinimize,
allowScreenCapture: s.allowScreenCapture
}))
)

const [changePasswordOpen, setChangePasswordOpen] = useState(false)

const handleAutoLockChange = (minutes: string): void => {
void setPref('autoLockMinutes', Number(minutes))
}

const handleLockOnMinimize = (checked: boolean): void => {
void setPref('lockOnMinimize', checked)
}
Expand Down Expand Up @@ -58,6 +67,26 @@ export function SecuritySettingsDialog({ open, onClose }: Props): ReactNode {
Auto-lock
</p>

<SettingRow
label="Timeout"
description="Automatically lock the vault after a period of inactivity."
>
<Select value={String(autoLockMinutes)} onValueChange={handleAutoLockChange}>
<SelectTrigger className="w-40" size="sm">
<SelectValue placeholder="Select a lock-out time" />
</SelectTrigger>
<SelectContent position="popper">
<SelectGroup>
<SelectItem value="0">Never</SelectItem>
<SelectItem value="5">After 5 minutes</SelectItem>
<SelectItem value="15">After 15 minutes</SelectItem>
<SelectItem value="30">After 30 minutes</SelectItem>
<SelectItem value="60">After 1 hour</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</SettingRow>

<SettingRow
label="Lock when minimized"
description="Lock the vault when the app window is minimized."
Expand All @@ -67,22 +96,6 @@ export function SecuritySettingsDialog({ open, onClose }: Props): ReactNode {
onCheckedChange={(v) => handleLockOnMinimize(v === true)}
/>
</SettingRow>

<SettingRow
label="Lock on system sleep"
description="Always active. Cannot be disabled."
readonly
>
<ReadonlyCheck />
</SettingRow>

<SettingRow
label="Lock on screen lock"
description="Always active. Cannot be disabled."
readonly
>
<ReadonlyCheck />
</SettingRow>
</section>

<Separator />
Expand All @@ -106,80 +119,71 @@ export function SecuritySettingsDialog({ open, onClose }: Props): ReactNode {

<Separator />

{/* ── Key file ── */}
<KeyFileSetting />

<Separator />

{/* ── Backups ── */}
{/* ── Authentication ── */}
<section className="space-y-3">
<p className="text-muted-foreground text-xs font-semibold tracking-[0.08em] uppercase">
Backups
</p>
<p className="text-muted-foreground text-xs">
Plain point-in-time copies of your vault file, created automatically before
format-changing updates. Safe to delete — not a restore feature, just browsable via
your file explorer.
Authentication
</p>
<Button
variant="ghost"
size="sm"
onClick={() => {
void handleOpenBackupsFolder()
}}
<SettingRow
label="Password"
description="Change the password used to unlock this vault."
>
Open backups folder
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => {
onClose()
setChangePasswordOpen(true)
}}
>
<LockIcon />
Change password
</Button>
</SettingRow>

<KeyFileSetting />
</section>

<Separator />

{/* ── Unlock attempts ── */}
{/* ── Always-on protections ── */}
<section className="space-y-3">
<p className="text-muted-foreground text-xs font-semibold tracking-[0.08em] uppercase">
Unlock attempts
Always-on protections
</p>
<p className="text-muted-foreground text-xs">
Notvex always locks the vault when the system sleeps or the screen locks, and applies
a progressive delay after repeated failed unlock attempts. These protections are built
in and cannot be turned off.
</p>

<SettingRow
label="Progressive delay after failed unlock attempts"
description="Always active. Cannot be disabled."
readonly
>
<ReadonlyCheck />
</SettingRow>
</section>
</div>
</DialogContent>

<ChangePasswordDialog
open={changePasswordOpen}
onClose={() => setChangePasswordOpen(false)}
/>
</Dialog>
)
}

function SettingRow({
label,
description,
readonly = false,
children
}: {
label: string
description?: string
readonly?: boolean
children: React.ReactNode
}): ReactNode {
return (
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-0.5">
<p className={`text-sm ${readonly ? 'text-muted-foreground' : ''}`}>{label}</p>
<p className="text-sm">{label}</p>
{description && <p className="text-muted-foreground text-xs">{description}</p>}
</div>
<div className="mt-0.5 shrink-0">{children}</div>
</div>
)
}

function ReadonlyCheck(): ReactNode {
return (
<div className="bg-primary/20 border-primary/30 flex h-4 w-4 items-center justify-center rounded border">
<CheckIcon className="text-primary h-3 w-3" />
</div>
)
}
15 changes: 7 additions & 8 deletions src/renderer/src/components/settings/KeyFileSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,13 @@ export function KeyFileSetting(): ReactNode {

return (
<section className="space-y-3">
<p className="text-muted-foreground text-xs font-semibold tracking-[0.08em] uppercase">
Key file
</p>

<p className="text-muted-foreground text-xs">
A key file adds a second factor to unlock your vault. You need both your password and the
key file to access your notes. If you lose the key file, access is permanently lost.
</p>
<div className="flex-1 space-y-0.5">
<p className="text-sm">Key file</p>
<p className="text-muted-foreground text-xs">
A key file adds a second factor to unlock your vault. You need both your password and the
key file to access your notes. If you lose the key file, access is permanently lost.
</p>
</div>

{keyFileStep === 'idle' && (
<>
Expand Down
69 changes: 10 additions & 59 deletions src/renderer/src/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,10 @@ import { useCreateNote } from '@/hooks/use-create-note'
import { useIsDev } from '@/hooks/use-is-dev'
import { notvex } from '@/lib/ipc'
import { cn, truncatePath } from '@/lib/utils'
import { usePrefsStore } from '@/store/prefs.store'
import { useUiStore } from '@/store/ui.store'
import { useVaultStore } from '@/store/vault.store'
import type { Tag } from '@shared/types'
import { AppLogo } from './AppLogo'
import { ChangePasswordDialog } from './change-password-dialog'
import { AppVersionDialog } from './dialogs/AppVersionDialog'
import { SecuritySettingsDialog } from './security-settings-dialog'
import { TagCreateModal } from './tags/TagCreateModal'
Expand All @@ -49,14 +47,6 @@ import {
} from './ui/dropdown-menu'
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from './ui/input-group'
import { Kbd } from './ui/kbd'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue
} from './ui/select'
import {
Sidebar as ShadcnSidebar,
SidebarContent,
Expand Down Expand Up @@ -84,6 +74,11 @@ const handleSaveCopy = async (): Promise<void> => {
if (result.data) toast.success('Copy saved successfully')
}

const handleOpenBackupsFolder = async (): Promise<void> => {
const res = await notvex.vault.openBackupsFolder()
if (!res.success) toast.error(res.error)
}

export function Sidebar(): React.ReactNode {
const { loadTags, loadTagCounts, setStatus, setActiveNoteId, setNotes } = useVaultStore()
const { tags, tagCounts, notes, currentVaultPath } = useVaultStore(
Expand All @@ -107,17 +102,13 @@ export function Sidebar(): React.ReactNode {
}))
)

const { setPref } = usePrefsStore()
const autoLockMinutes = usePrefsStore((s) => s.autoLockMinutes)

const handleNewNote = useCreateNote()
const { isCopied, copyToClipboard } = useCopyToClipboard()
const isDev = useIsDev()

const searchContainerRef = useRef<HTMLDivElement>(null)
const [appVersionOpen, setAppVersionOpen] = useState(false)
const [settingsPopoverOpen, setSettingsPopoverOpen] = useState(false)
const [changePasswordOpen, setChangePasswordOpen] = useState(false)
const [securitySettingsOpen, setSecuritySettingsOpen] = useState(false)
const [createModalOpen, setCreateModalOpen] = useState(false)
const [createModalKey, setCreateModalKey] = useState(0)
Expand All @@ -141,10 +132,6 @@ export function Sidebar(): React.ReactNode {
setNotes([])
}

const handleAutoLockChange = (minutes: string): void => {
void setPref('autoLockMinutes', Number(minutes))
}

const allNotesCount = notes.filter((n) => !n.isTrashed).length
const pinnedCount = notes.filter((n) => n.isPinned && !n.isTrashed).length
const trashCount = notes.filter((n) => n.isTrashed).length
Expand Down Expand Up @@ -184,28 +171,8 @@ export function Sidebar(): React.ReactNode {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" side="right" sideOffset={4} className="w-64">
<DropdownMenuGroup>
<DropdownMenuLabel>Auto-lock</DropdownMenuLabel>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<Select value={String(autoLockMinutes)} onValueChange={handleAutoLockChange}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a lock-out time" />
</SelectTrigger>
<SelectContent position="popper">
<SelectGroup>
<SelectItem value="0">Never</SelectItem>
<SelectItem value="5">After 5 minutes</SelectItem>
<SelectItem value="15">After 15 minutes</SelectItem>
<SelectItem value="30">After 30 minutes</SelectItem>
<SelectItem value="60">After 1 hour</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</DropdownMenuItem>
</DropdownMenuGroup>
{currentVaultPath && (
<>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>Vault Location</DropdownMenuLabel>
<DropdownMenuItem>
Expand All @@ -221,29 +188,17 @@ export function Sidebar(): React.ReactNode {
</InputGroupAddon>
</InputGroup>
</DropdownMenuItem>
<DropdownMenuItem
role="button"
className="text-primary"
onClick={handleSaveCopy}
>
<DropdownMenuItem className="text-primary" onClick={handleSaveCopy}>
Save a vault copy as...
</DropdownMenuItem>
<DropdownMenuItem className="text-primary" onClick={handleOpenBackupsFolder}>
Open backups folder
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>Security</DropdownMenuLabel>
<DropdownMenuItem
role="button"
className="text-primary"
onClick={() => {
setChangePasswordOpen(true)
setSettingsPopoverOpen(false)
}}
>
Change password
</DropdownMenuItem>
<DropdownMenuItem
role="button"
className="text-primary"
Expand Down Expand Up @@ -401,10 +356,6 @@ export function Sidebar(): React.ReactNode {
</SidebarFooter>

<AppVersionDialog open={appVersionOpen} onClose={() => setAppVersionOpen(false)} />
<ChangePasswordDialog
open={changePasswordOpen}
onClose={() => setChangePasswordOpen(false)}
/>
<SecuritySettingsDialog
open={securitySettingsOpen}
onClose={() => setSecuritySettingsOpen(false)}
Expand Down
Loading