mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +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 [repromptPassword, setRepromptPassword] = useState('');
|
||||
const [repromptApprovedCipherId, setRepromptApprovedCipherId] = useState<string | null>(null);
|
||||
const [pendingDeletePasskeyIndex, setPendingDeletePasskeyIndex] = useState<number | null>(null);
|
||||
const [isMobileLayout, setIsMobileLayout] = useState(getInitialIsMobileLayout);
|
||||
const [mobilePanel, setMobilePanel] = useState<'list' | 'detail' | 'edit'>('list');
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
@@ -445,6 +446,7 @@ function folderName(id: string | null | undefined): string {
|
||||
setLocalError('');
|
||||
setAttachmentQueue([]);
|
||||
setRemovedAttachmentIds({});
|
||||
setPendingDeletePasskeyIndex(null);
|
||||
if (isMobileLayout) setMobilePanel(returnToDetail ? 'detail' : 'list');
|
||||
}
|
||||
|
||||
@@ -452,6 +454,18 @@ function folderName(id: string | null | undefined): string {
|
||||
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> {
|
||||
const ticket = ++sshSeedTicketRef.current;
|
||||
try {
|
||||
@@ -947,6 +961,7 @@ function folderName(id: string | null | undefined): string {
|
||||
onUpdateDraftLoginUri={updateDraftLoginUri}
|
||||
onUpdateDraftLoginUriMatch={updateDraftLoginUriMatch}
|
||||
onReorderDraftLoginUri={reorderDraftLoginUri}
|
||||
onRequestDeleteLoginPasskey={setPendingDeletePasskeyIndex}
|
||||
onQueueAttachmentFiles={queueAttachmentFiles}
|
||||
onToggleExistingAttachmentRemoval={toggleExistingAttachmentRemoval}
|
||||
onRemoveQueuedAttachment={removeQueuedAttachment}
|
||||
@@ -1015,6 +1030,7 @@ function folderName(id: string | null | undefined): string {
|
||||
deleteAllFoldersOpen={deleteAllFoldersOpen}
|
||||
repromptOpen={repromptOpen}
|
||||
repromptPassword={repromptPassword}
|
||||
deletePasskeyOpen={pendingDeletePasskeyIndex != null}
|
||||
onConfirmAddField={() => {
|
||||
if (!draft) return;
|
||||
if (!fieldLabel.trim()) {
|
||||
@@ -1077,6 +1093,8 @@ function folderName(id: string | null | undefined): string {
|
||||
setRepromptPassword('');
|
||||
}}
|
||||
onRepromptPasswordChange={setRepromptPassword}
|
||||
onConfirmDeletePasskey={confirmDeleteLoginPasskey}
|
||||
onCancelDeletePasskey={() => setPendingDeletePasskeyIndex(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ interface VaultDialogsProps {
|
||||
deleteAllFoldersOpen: boolean;
|
||||
repromptOpen: boolean;
|
||||
repromptPassword: string;
|
||||
deletePasskeyOpen: boolean;
|
||||
onConfirmAddField: () => void;
|
||||
onCancelFieldModal: () => void;
|
||||
onFieldTypeChange: (value: CustomFieldType) => void;
|
||||
@@ -54,6 +55,8 @@ interface VaultDialogsProps {
|
||||
onConfirmReprompt: () => void;
|
||||
onCancelReprompt: () => void;
|
||||
onRepromptPasswordChange: (value: string) => void;
|
||||
onConfirmDeletePasskey: () => void;
|
||||
onCancelDeletePasskey: () => void;
|
||||
}
|
||||
|
||||
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)} />
|
||||
</label>
|
||||
</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 type { Cipher, Folder, VaultDraft, VaultDraftField } from '@/lib/types';
|
||||
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 {
|
||||
draft: VaultDraft;
|
||||
@@ -44,6 +52,7 @@ interface VaultEditorProps {
|
||||
onUpdateDraftLoginUri: (index: number, value: string) => void;
|
||||
onUpdateDraftLoginUriMatch: (index: number, value: number | null) => void;
|
||||
onReorderDraftLoginUri: (fromIndex: number, toIndex: number) => void;
|
||||
onRequestDeleteLoginPasskey: (index: number) => void;
|
||||
onQueueAttachmentFiles: (list: FileList | null) => void;
|
||||
onToggleExistingAttachmentRemoval: (attachmentId: string) => void;
|
||||
onRemoveQueuedAttachment: (index: number) => void;
|
||||
@@ -288,6 +297,42 @@ export default function VaultEditor(props: VaultEditorProps) {
|
||||
))}
|
||||
</SortableContext>
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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_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_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_authenticator_key: "Authenticator Key",
|
||||
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_invites: "Delete all invites",
|
||||
txt_delete_item: "Delete Item",
|
||||
txt_delete_passkey: "Delete Passkey",
|
||||
txt_delete_item_failed: "Delete item failed",
|
||||
txt_delete_permanently: "Delete Permanently",
|
||||
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_too_long: "Password hint must be 120 characters or fewer",
|
||||
txt_passkey: "Passkey",
|
||||
txt_passkeys: "Passkeys",
|
||||
txt_passkey_created_at_value: "Created on {value}",
|
||||
txt_phone: "Phone",
|
||||
txt_please_input_email_and_password: "Please input email and password",
|
||||
@@ -1163,6 +1166,7 @@ const zhCNOverrides: Record<string, string> = {
|
||||
txt_no_name: '(无名称)',
|
||||
txt_are_you_sure_you_want_to_log_out: '确认要退出登录吗?',
|
||||
txt_delete_item: '删除项目',
|
||||
txt_delete_passkey: '删除通行密钥',
|
||||
txt_delete_selected_items: '删除所选项目',
|
||||
txt_move_selected_items: '移动所选项目',
|
||||
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_permanently: '确认永久删除所选的 {count} 个项目?',
|
||||
txt_are_you_sure_you_want_to_delete_this_item: '确认删除此项目?',
|
||||
txt_are_you_sure_you_want_to_delete_this_passkey: '确认删除这个通行密钥?',
|
||||
txt_authenticator_key: '验证器密钥',
|
||||
txt_brand: '品牌',
|
||||
txt_bulk_delete_failed: '批量删除失败',
|
||||
@@ -1327,6 +1332,7 @@ const zhCNOverrides: Record<string, string> = {
|
||||
txt_password_hint_load_failed: '加载密码提示失败',
|
||||
txt_password_hint_too_long: '密码提示最多只能输入 120 个字符',
|
||||
txt_passkey: '通行密钥',
|
||||
txt_passkeys: '通行密钥',
|
||||
txt_passkey_created_at_value: '创建于 {value}',
|
||||
txt_phone: '电话',
|
||||
txt_please_input_email_and_password: '请输入邮箱和密码',
|
||||
|
||||
Reference in New Issue
Block a user