mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: add passkey deletion functionality and related UI components
This commit is contained in:
@@ -104,6 +104,7 @@ export default function VaultPage(props: VaultPageProps) {
|
|||||||
const [repromptOpen, setRepromptOpen] = useState(false);
|
const [repromptOpen, setRepromptOpen] = useState(false);
|
||||||
const [repromptPassword, setRepromptPassword] = useState('');
|
const [repromptPassword, setRepromptPassword] = useState('');
|
||||||
const [repromptApprovedCipherId, setRepromptApprovedCipherId] = useState<string | null>(null);
|
const [repromptApprovedCipherId, setRepromptApprovedCipherId] = useState<string | null>(null);
|
||||||
|
const [pendingDeletePasskeyIndex, setPendingDeletePasskeyIndex] = useState<number | null>(null);
|
||||||
const [isMobileLayout, setIsMobileLayout] = useState(getInitialIsMobileLayout);
|
const [isMobileLayout, setIsMobileLayout] = useState(getInitialIsMobileLayout);
|
||||||
const [mobilePanel, setMobilePanel] = useState<'list' | 'detail' | 'edit'>('list');
|
const [mobilePanel, setMobilePanel] = useState<'list' | 'detail' | 'edit'>('list');
|
||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||||
@@ -445,6 +446,7 @@ function folderName(id: string | null | undefined): string {
|
|||||||
setLocalError('');
|
setLocalError('');
|
||||||
setAttachmentQueue([]);
|
setAttachmentQueue([]);
|
||||||
setRemovedAttachmentIds({});
|
setRemovedAttachmentIds({});
|
||||||
|
setPendingDeletePasskeyIndex(null);
|
||||||
if (isMobileLayout) setMobilePanel(returnToDetail ? 'detail' : 'list');
|
if (isMobileLayout) setMobilePanel(returnToDetail ? 'detail' : 'list');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,6 +454,18 @@ function folderName(id: string | null | undefined): string {
|
|||||||
setDraft((prev) => (prev ? { ...prev, ...patch } : prev));
|
setDraft((prev) => (prev ? { ...prev, ...patch } : prev));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirmDeleteLoginPasskey(): void {
|
||||||
|
if (pendingDeletePasskeyIndex == null) return;
|
||||||
|
setDraft((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
loginFido2Credentials: prev.loginFido2Credentials.filter((_, index) => index !== pendingDeletePasskeyIndex),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setPendingDeletePasskeyIndex(null);
|
||||||
|
}
|
||||||
|
|
||||||
async function seedSshDefaults(force = false): Promise<void> {
|
async function seedSshDefaults(force = false): Promise<void> {
|
||||||
const ticket = ++sshSeedTicketRef.current;
|
const ticket = ++sshSeedTicketRef.current;
|
||||||
try {
|
try {
|
||||||
@@ -947,6 +961,7 @@ function folderName(id: string | null | undefined): string {
|
|||||||
onUpdateDraftLoginUri={updateDraftLoginUri}
|
onUpdateDraftLoginUri={updateDraftLoginUri}
|
||||||
onUpdateDraftLoginUriMatch={updateDraftLoginUriMatch}
|
onUpdateDraftLoginUriMatch={updateDraftLoginUriMatch}
|
||||||
onReorderDraftLoginUri={reorderDraftLoginUri}
|
onReorderDraftLoginUri={reorderDraftLoginUri}
|
||||||
|
onRequestDeleteLoginPasskey={setPendingDeletePasskeyIndex}
|
||||||
onQueueAttachmentFiles={queueAttachmentFiles}
|
onQueueAttachmentFiles={queueAttachmentFiles}
|
||||||
onToggleExistingAttachmentRemoval={toggleExistingAttachmentRemoval}
|
onToggleExistingAttachmentRemoval={toggleExistingAttachmentRemoval}
|
||||||
onRemoveQueuedAttachment={removeQueuedAttachment}
|
onRemoveQueuedAttachment={removeQueuedAttachment}
|
||||||
@@ -1015,6 +1030,7 @@ function folderName(id: string | null | undefined): string {
|
|||||||
deleteAllFoldersOpen={deleteAllFoldersOpen}
|
deleteAllFoldersOpen={deleteAllFoldersOpen}
|
||||||
repromptOpen={repromptOpen}
|
repromptOpen={repromptOpen}
|
||||||
repromptPassword={repromptPassword}
|
repromptPassword={repromptPassword}
|
||||||
|
deletePasskeyOpen={pendingDeletePasskeyIndex != null}
|
||||||
onConfirmAddField={() => {
|
onConfirmAddField={() => {
|
||||||
if (!draft) return;
|
if (!draft) return;
|
||||||
if (!fieldLabel.trim()) {
|
if (!fieldLabel.trim()) {
|
||||||
@@ -1077,6 +1093,8 @@ function folderName(id: string | null | undefined): string {
|
|||||||
setRepromptPassword('');
|
setRepromptPassword('');
|
||||||
}}
|
}}
|
||||||
onRepromptPasswordChange={setRepromptPassword}
|
onRepromptPasswordChange={setRepromptPassword}
|
||||||
|
onConfirmDeletePasskey={confirmDeleteLoginPasskey}
|
||||||
|
onCancelDeletePasskey={() => setPendingDeletePasskeyIndex(null)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface VaultDialogsProps {
|
|||||||
deleteAllFoldersOpen: boolean;
|
deleteAllFoldersOpen: boolean;
|
||||||
repromptOpen: boolean;
|
repromptOpen: boolean;
|
||||||
repromptPassword: string;
|
repromptPassword: string;
|
||||||
|
deletePasskeyOpen: boolean;
|
||||||
onConfirmAddField: () => void;
|
onConfirmAddField: () => void;
|
||||||
onCancelFieldModal: () => void;
|
onCancelFieldModal: () => void;
|
||||||
onFieldTypeChange: (value: CustomFieldType) => void;
|
onFieldTypeChange: (value: CustomFieldType) => void;
|
||||||
@@ -54,6 +55,8 @@ interface VaultDialogsProps {
|
|||||||
onConfirmReprompt: () => void;
|
onConfirmReprompt: () => void;
|
||||||
onCancelReprompt: () => void;
|
onCancelReprompt: () => void;
|
||||||
onRepromptPasswordChange: (value: string) => void;
|
onRepromptPasswordChange: (value: string) => void;
|
||||||
|
onConfirmDeletePasskey: () => void;
|
||||||
|
onCancelDeletePasskey: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VaultDialogs(props: VaultDialogsProps) {
|
export default function VaultDialogs(props: VaultDialogsProps) {
|
||||||
@@ -181,6 +184,17 @@ export default function VaultDialogs(props: VaultDialogsProps) {
|
|||||||
<input className="input" type="password" value={props.repromptPassword} onInput={(e) => props.onRepromptPasswordChange((e.currentTarget as HTMLInputElement).value)} />
|
<input className="input" type="password" value={props.repromptPassword} onInput={(e) => props.onRepromptPasswordChange((e.currentTarget as HTMLInputElement).value)} />
|
||||||
</label>
|
</label>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={props.deletePasskeyOpen}
|
||||||
|
title={t('txt_delete_passkey')}
|
||||||
|
message={t('txt_are_you_sure_you_want_to_delete_this_passkey')}
|
||||||
|
confirmText={t('txt_delete')}
|
||||||
|
cancelText={t('txt_cancel')}
|
||||||
|
danger
|
||||||
|
onConfirm={props.onConfirmDeletePasskey}
|
||||||
|
onCancel={props.onCancelDeletePasskey}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,15 @@ import {
|
|||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import type { Cipher, Folder, VaultDraft, VaultDraftField } from '@/lib/types';
|
import type { Cipher, Folder, VaultDraft, VaultDraftField } from '@/lib/types';
|
||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
import { CREATE_TYPE_OPTIONS, cipherTypeLabel, createEmptyLoginUri, formatAttachmentSize, toBooleanFieldValue, WEBSITE_MATCH_OPTIONS } from '@/components/vault/vault-page-helpers';
|
import {
|
||||||
|
CREATE_TYPE_OPTIONS,
|
||||||
|
cipherTypeLabel,
|
||||||
|
createEmptyLoginUri,
|
||||||
|
formatAttachmentSize,
|
||||||
|
formatHistoryTime,
|
||||||
|
toBooleanFieldValue,
|
||||||
|
WEBSITE_MATCH_OPTIONS,
|
||||||
|
} from '@/components/vault/vault-page-helpers';
|
||||||
|
|
||||||
interface VaultEditorProps {
|
interface VaultEditorProps {
|
||||||
draft: VaultDraft;
|
draft: VaultDraft;
|
||||||
@@ -44,6 +52,7 @@ interface VaultEditorProps {
|
|||||||
onUpdateDraftLoginUri: (index: number, value: string) => void;
|
onUpdateDraftLoginUri: (index: number, value: string) => void;
|
||||||
onUpdateDraftLoginUriMatch: (index: number, value: number | null) => void;
|
onUpdateDraftLoginUriMatch: (index: number, value: number | null) => void;
|
||||||
onReorderDraftLoginUri: (fromIndex: number, toIndex: number) => void;
|
onReorderDraftLoginUri: (fromIndex: number, toIndex: number) => void;
|
||||||
|
onRequestDeleteLoginPasskey: (index: number) => void;
|
||||||
onQueueAttachmentFiles: (list: FileList | null) => void;
|
onQueueAttachmentFiles: (list: FileList | null) => void;
|
||||||
onToggleExistingAttachmentRemoval: (attachmentId: string) => void;
|
onToggleExistingAttachmentRemoval: (attachmentId: string) => void;
|
||||||
onRemoveQueuedAttachment: (index: number) => void;
|
onRemoveQueuedAttachment: (index: number) => void;
|
||||||
@@ -288,6 +297,42 @@ export default function VaultEditor(props: VaultEditorProps) {
|
|||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
{props.draft.loginFido2Credentials.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="section-head" style={{ marginTop: '18px' }}>
|
||||||
|
<h4>{t('txt_passkeys')}</h4>
|
||||||
|
</div>
|
||||||
|
<div className="attachment-list">
|
||||||
|
{props.draft.loginFido2Credentials.map((credential, index) => {
|
||||||
|
const createdAt = String(credential?.creationDate || '').trim();
|
||||||
|
const label = createdAt
|
||||||
|
? t('txt_passkey_created_at_value', { value: formatHistoryTime(createdAt) })
|
||||||
|
: t('txt_passkey');
|
||||||
|
return (
|
||||||
|
<div key={`login-passkey-${index}`} className="attachment-row">
|
||||||
|
<div className="attachment-main">
|
||||||
|
<div className="attachment-text">
|
||||||
|
<strong>{t('txt_passkey')}</strong>
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="kv-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary small"
|
||||||
|
disabled={props.busy}
|
||||||
|
onClick={() => props.onRequestDeleteLoginPasskey(index)}
|
||||||
|
>
|
||||||
|
<X size={14} className="btn-icon" />
|
||||||
|
{t('txt_remove')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ const messages: Record<Locale, Record<string, string>> = {
|
|||||||
txt_are_you_sure_you_want_to_delete_count_selected_items: "Are you sure you want to delete {count} selected items?",
|
txt_are_you_sure_you_want_to_delete_count_selected_items: "Are you sure you want to delete {count} selected items?",
|
||||||
txt_are_you_sure_you_want_to_delete_count_selected_items_permanently: "Are you sure you want to permanently delete {count} selected items?",
|
txt_are_you_sure_you_want_to_delete_count_selected_items_permanently: "Are you sure you want to permanently delete {count} selected items?",
|
||||||
txt_are_you_sure_you_want_to_delete_this_item: "Are you sure you want to delete this item?",
|
txt_are_you_sure_you_want_to_delete_this_item: "Are you sure you want to delete this item?",
|
||||||
|
txt_are_you_sure_you_want_to_delete_this_passkey: "Are you sure you want to delete this passkey?",
|
||||||
txt_are_you_sure_you_want_to_log_out: "Are you sure you want to log out?",
|
txt_are_you_sure_you_want_to_log_out: "Are you sure you want to log out?",
|
||||||
txt_authenticator_key: "Authenticator Key",
|
txt_authenticator_key: "Authenticator Key",
|
||||||
txt_authorized_devices: "Authorized Devices",
|
txt_authorized_devices: "Authorized Devices",
|
||||||
@@ -352,6 +353,7 @@ const messages: Record<Locale, Record<string, string>> = {
|
|||||||
txt_delete_all_invite_codes_active_inactive: "Delete all invite codes (active/inactive)?",
|
txt_delete_all_invite_codes_active_inactive: "Delete all invite codes (active/inactive)?",
|
||||||
txt_delete_all_invites: "Delete all invites",
|
txt_delete_all_invites: "Delete all invites",
|
||||||
txt_delete_item: "Delete Item",
|
txt_delete_item: "Delete Item",
|
||||||
|
txt_delete_passkey: "Delete Passkey",
|
||||||
txt_delete_item_failed: "Delete item failed",
|
txt_delete_item_failed: "Delete item failed",
|
||||||
txt_delete_permanently: "Delete Permanently",
|
txt_delete_permanently: "Delete Permanently",
|
||||||
txt_archive: "Archive",
|
txt_archive: "Archive",
|
||||||
@@ -572,6 +574,7 @@ const messages: Record<Locale, Record<string, string>> = {
|
|||||||
txt_password_hint_load_failed: "Failed to load password hint",
|
txt_password_hint_load_failed: "Failed to load password hint",
|
||||||
txt_password_hint_too_long: "Password hint must be 120 characters or fewer",
|
txt_password_hint_too_long: "Password hint must be 120 characters or fewer",
|
||||||
txt_passkey: "Passkey",
|
txt_passkey: "Passkey",
|
||||||
|
txt_passkeys: "Passkeys",
|
||||||
txt_passkey_created_at_value: "Created on {value}",
|
txt_passkey_created_at_value: "Created on {value}",
|
||||||
txt_phone: "Phone",
|
txt_phone: "Phone",
|
||||||
txt_please_input_email_and_password: "Please input email and password",
|
txt_please_input_email_and_password: "Please input email and password",
|
||||||
@@ -1163,6 +1166,7 @@ const zhCNOverrides: Record<string, string> = {
|
|||||||
txt_no_name: '(无名称)',
|
txt_no_name: '(无名称)',
|
||||||
txt_are_you_sure_you_want_to_log_out: '确认要退出登录吗?',
|
txt_are_you_sure_you_want_to_log_out: '确认要退出登录吗?',
|
||||||
txt_delete_item: '删除项目',
|
txt_delete_item: '删除项目',
|
||||||
|
txt_delete_passkey: '删除通行密钥',
|
||||||
txt_delete_selected_items: '删除所选项目',
|
txt_delete_selected_items: '删除所选项目',
|
||||||
txt_move_selected_items: '移动所选项目',
|
txt_move_selected_items: '移动所选项目',
|
||||||
txt_create_folder: '创建文件夹',
|
txt_create_folder: '创建文件夹',
|
||||||
@@ -1226,6 +1230,7 @@ const zhCNOverrides: Record<string, string> = {
|
|||||||
txt_are_you_sure_you_want_to_delete_count_selected_items: '确认删除所选的 {count} 个项目?',
|
txt_are_you_sure_you_want_to_delete_count_selected_items: '确认删除所选的 {count} 个项目?',
|
||||||
txt_are_you_sure_you_want_to_delete_count_selected_items_permanently: '确认永久删除所选的 {count} 个项目?',
|
txt_are_you_sure_you_want_to_delete_count_selected_items_permanently: '确认永久删除所选的 {count} 个项目?',
|
||||||
txt_are_you_sure_you_want_to_delete_this_item: '确认删除此项目?',
|
txt_are_you_sure_you_want_to_delete_this_item: '确认删除此项目?',
|
||||||
|
txt_are_you_sure_you_want_to_delete_this_passkey: '确认删除这个通行密钥?',
|
||||||
txt_authenticator_key: '验证器密钥',
|
txt_authenticator_key: '验证器密钥',
|
||||||
txt_brand: '品牌',
|
txt_brand: '品牌',
|
||||||
txt_bulk_delete_failed: '批量删除失败',
|
txt_bulk_delete_failed: '批量删除失败',
|
||||||
@@ -1327,6 +1332,7 @@ const zhCNOverrides: Record<string, string> = {
|
|||||||
txt_password_hint_load_failed: '加载密码提示失败',
|
txt_password_hint_load_failed: '加载密码提示失败',
|
||||||
txt_password_hint_too_long: '密码提示最多只能输入 120 个字符',
|
txt_password_hint_too_long: '密码提示最多只能输入 120 个字符',
|
||||||
txt_passkey: '通行密钥',
|
txt_passkey: '通行密钥',
|
||||||
|
txt_passkeys: '通行密钥',
|
||||||
txt_passkey_created_at_value: '创建于 {value}',
|
txt_passkey_created_at_value: '创建于 {value}',
|
||||||
txt_phone: '电话',
|
txt_phone: '电话',
|
||||||
txt_please_input_email_and_password: '请输入邮箱和密码',
|
txt_please_input_email_and_password: '请输入邮箱和密码',
|
||||||
|
|||||||
Reference in New Issue
Block a user