mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-18 13:28:12 -04:00
Tweak passkey create and auth flow, replace literal item type with references (#1404)
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
import type { Passkey } from '@/utils/dist/core/models/vault';
|
||||
|
||||
type PasskeyBlockProps = {
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Passkey icon component.
|
||||
*/
|
||||
const PasskeyIcon: React.FC<{ className?: string }> = ({ className = "w-5 h-5" }) => (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Display passkey information for an item in view mode.
|
||||
*/
|
||||
const PasskeyBlock: React.FC<PasskeyBlockProps> = ({ itemId }) => {
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dbContext?.sqliteClient || !itemId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const itemPasskeys = dbContext.sqliteClient.getPasskeysByItemId(itemId);
|
||||
setPasskeys(itemPasskeys);
|
||||
} catch (err) {
|
||||
console.error('Error loading passkeys:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [dbContext?.sqliteClient, itemId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center p-2">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (passkeys.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||||
{t('passkeys.passkey')}
|
||||
</h2>
|
||||
{passkeys.map((passkey) => (
|
||||
<div
|
||||
key={passkey.Id}
|
||||
className="p-3 rounded bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<PasskeyIcon className="w-5 h-5 text-gray-600 dark:text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="mb-1">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{passkey.DisplayName || t('passkeys.passkey')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1 mb-2">
|
||||
{passkey.RpId && (
|
||||
<div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{t('passkeys.site')}:{' '}
|
||||
</span>
|
||||
<span className="text-sm text-gray-900 dark:text-white">
|
||||
{passkey.RpId}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{t('passkeys.helpText')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeyBlock;
|
||||
export { PasskeyIcon };
|
||||
@@ -0,0 +1,185 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
import type { Passkey } from '@/utils/dist/core/models/vault';
|
||||
|
||||
import { PasskeyIcon } from './PasskeyBlock';
|
||||
|
||||
type PasskeyEditorProps = {
|
||||
itemId: string;
|
||||
passkeyIdsMarkedForDeletion: string[];
|
||||
onPasskeyMarkedForDeletion: (passkeyIds: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit passkey information for an item (supports marking for deletion).
|
||||
* Passkeys cannot be manually created or edited - only deleted.
|
||||
*/
|
||||
const PasskeyEditor: React.FC<PasskeyEditorProps> = ({
|
||||
itemId,
|
||||
passkeyIdsMarkedForDeletion,
|
||||
onPasskeyMarkedForDeletion
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dbContext?.sqliteClient || !itemId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const itemPasskeys = dbContext.sqliteClient.getPasskeysByItemId(itemId);
|
||||
setPasskeys(itemPasskeys);
|
||||
} catch (err) {
|
||||
console.error('Error loading passkeys:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [dbContext?.sqliteClient, itemId]);
|
||||
|
||||
/**
|
||||
* Mark a passkey for deletion.
|
||||
*/
|
||||
const handleMarkForDeletion = (passkeyId: string): void => {
|
||||
if (!passkeyIdsMarkedForDeletion.includes(passkeyId)) {
|
||||
onPasskeyMarkedForDeletion([...passkeyIdsMarkedForDeletion, passkeyId]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Undo marking a passkey for deletion.
|
||||
*/
|
||||
const handleUndoDeletion = (passkeyId: string): void => {
|
||||
onPasskeyMarkedForDeletion(passkeyIdsMarkedForDeletion.filter(id => id !== passkeyId));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center p-2">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (passkeys.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
{t('passkeys.passkey')}
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{passkeys.map((passkey) => {
|
||||
const isMarkedForDeletion = passkeyIdsMarkedForDeletion.includes(passkey.Id);
|
||||
|
||||
if (isMarkedForDeletion) {
|
||||
return (
|
||||
<div
|
||||
key={passkey.Id}
|
||||
className="p-3 rounded bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<PasskeyIcon className="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-red-900 dark:text-red-100">
|
||||
{t('passkeys.passkeyMarkedForDeletion')}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleUndoDeletion(passkey.Id)}
|
||||
className="text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
title={t('common.undo')}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3 7v6h6" />
|
||||
<path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-red-800 dark:text-red-200">
|
||||
{t('passkeys.passkeyWillBeDeleted')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={passkey.Id}
|
||||
className="p-3 rounded bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<PasskeyIcon className="w-5 h-5 text-gray-600 dark:text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{passkey.DisplayName || t('passkeys.passkey')}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMarkForDeletion(passkey.Id)}
|
||||
className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
|
||||
title={t('common.delete')}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
<line x1="10" y1="11" x2="10" y2="17" />
|
||||
<line x1="14" y1="11" x2="14" y2="17" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1 mb-2">
|
||||
{passkey.RpId && (
|
||||
<div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{t('passkeys.site')}:{' '}
|
||||
</span>
|
||||
<span className="text-sm text-gray-900 dark:text-white">
|
||||
{passkey.RpId}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{t('passkeys.helpText')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeyEditor;
|
||||
@@ -5,6 +5,8 @@ import FieldBlock from './FieldBlock';
|
||||
import HeaderBlock from './HeaderBlock';
|
||||
import LoginCredentialsBlock from './LoginCredentialsBlock';
|
||||
import NotesBlock from './NotesBlock';
|
||||
import PasskeyBlock from './PasskeyBlock';
|
||||
import PasskeyEditor from './PasskeyEditor';
|
||||
import TotpBlock from './TotpBlock';
|
||||
|
||||
export {
|
||||
@@ -15,5 +17,7 @@ export {
|
||||
AliasBlock,
|
||||
NotesBlock,
|
||||
AttachmentBlock,
|
||||
FieldBlock
|
||||
FieldBlock,
|
||||
PasskeyBlock,
|
||||
PasskeyEditor
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { Item } from '@/utils/dist/core/models/vault';
|
||||
import { FieldKey } from '@/utils/dist/core/models/vault';
|
||||
import { FieldKey, ItemTypes } from '@/utils/dist/core/models/vault';
|
||||
import SqliteClient from '@/utils/SqliteClient';
|
||||
|
||||
type ItemIconProps = {
|
||||
@@ -232,12 +232,12 @@ const getCardIcon = (brand: CardBrand): React.FC<{ className?: string }> => {
|
||||
*/
|
||||
const ItemIcon: React.FC<ItemIconProps> = ({ item, className = 'w-8 h-8' }) => {
|
||||
// For Note type, always show note icon
|
||||
if (item.ItemType === 'Note') {
|
||||
if (item.ItemType === ItemTypes.Note) {
|
||||
return <NoteIcon className={className} />;
|
||||
}
|
||||
|
||||
// For CreditCard type, detect card brand and show appropriate icon
|
||||
if (item.ItemType === 'CreditCard') {
|
||||
if (item.ItemType === ItemTypes.CreditCard) {
|
||||
const cardNumberField = item.Fields?.find(f => f.FieldKey === FieldKey.CardNumber);
|
||||
const cardNumber = cardNumberField?.Value
|
||||
? (Array.isArray(cardNumberField.Value) ? cardNumberField.Value[0] : cardNumberField.Value)
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ItemType } from '@/utils/dist/core/models/vault';
|
||||
import { ItemTypes } from '@/utils/dist/core/models/vault';
|
||||
|
||||
/**
|
||||
* Item type option configuration.
|
||||
@@ -17,7 +18,7 @@ type ItemTypeOption = {
|
||||
*/
|
||||
const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [
|
||||
{
|
||||
type: 'Login',
|
||||
type: ItemTypes.Login,
|
||||
titleKey: 'itemTypes.login.title',
|
||||
iconSvg: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -26,7 +27,7 @@ const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'Alias',
|
||||
type: ItemTypes.Alias,
|
||||
titleKey: 'itemTypes.alias.title',
|
||||
iconSvg: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -35,7 +36,7 @@ const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'CreditCard',
|
||||
type: ItemTypes.CreditCard,
|
||||
titleKey: 'itemTypes.creditCard.title',
|
||||
iconSvg: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -44,7 +45,7 @@ const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'Note',
|
||||
type: ItemTypes.Note,
|
||||
titleKey: 'itemTypes.note.title',
|
||||
iconSvg: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -105,7 +106,7 @@ const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({
|
||||
</svg>
|
||||
</button>
|
||||
{/* Regenerate alias button - icon only for flexibility */}
|
||||
{selectedType === 'Alias' && !isEditMode && onRegenerateAlias && (
|
||||
{selectedType === ItemTypes.Alias && !isEditMode && onRegenerateAlias && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import AttachmentUploader from '@/entrypoints/popup/components/Credentials/Details/AttachmentUploader';
|
||||
import PasskeyEditor from '@/entrypoints/popup/components/Credentials/Details/PasskeyEditor';
|
||||
import TotpEditor from '@/entrypoints/popup/components/Credentials/Details/TotpEditor';
|
||||
import Modal from '@/entrypoints/popup/components/Dialogs/Modal';
|
||||
import AddFieldMenu from '@/entrypoints/popup/components/Forms/AddFieldMenu';
|
||||
@@ -23,13 +24,13 @@ import useAliasGenerator from '@/entrypoints/popup/hooks/useAliasGenerator';
|
||||
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
|
||||
import type { Item, ItemField, ItemType, FieldType, Attachment, TotpCode } from '@/utils/dist/core/models/vault';
|
||||
import { FieldCategories, FieldTypes, getSystemFieldsForItemType, isFieldShownByDefault } from '@/utils/dist/core/models/vault';
|
||||
import { FieldCategories, FieldTypes, ItemTypes, getSystemFieldsForItemType, isFieldShownByDefault } from '@/utils/dist/core/models/vault';
|
||||
|
||||
// Valid item types from the shared model
|
||||
const VALID_ITEM_TYPES: ItemType[] = ['Login', 'Alias', 'CreditCard', 'Note'];
|
||||
const VALID_ITEM_TYPES: ItemType[] = [ItemTypes.Login, ItemTypes.Alias, ItemTypes.CreditCard, ItemTypes.Note];
|
||||
|
||||
// Default item type for new items
|
||||
const DEFAULT_ITEM_TYPE: ItemType = 'Login';
|
||||
const DEFAULT_ITEM_TYPE: ItemType = ItemTypes.Login;
|
||||
|
||||
/**
|
||||
* Temporary custom field definition (before persisting to database)
|
||||
@@ -107,6 +108,9 @@ const ItemAddEdit: React.FC = () => {
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const [originalAttachmentIds, setOriginalAttachmentIds] = useState<string[]>([]);
|
||||
|
||||
// Passkeys state (only IDs marked for deletion - passkeys cannot be created/edited manually)
|
||||
const [passkeyIdsMarkedForDeletion, setPasskeyIdsMarkedForDeletion] = useState<string[]>([]);
|
||||
|
||||
/**
|
||||
* Get all applicable system fields for the current item type.
|
||||
*/
|
||||
@@ -432,6 +436,13 @@ const ItemAddEdit: React.FC = () => {
|
||||
originalTotpCodeIds,
|
||||
totpCodes
|
||||
);
|
||||
|
||||
// Delete passkeys marked for deletion
|
||||
if (passkeyIdsMarkedForDeletion.length > 0) {
|
||||
for (const passkeyId of passkeyIdsMarkedForDeletion) {
|
||||
await dbContext.sqliteClient!.deletePasskeyById(passkeyId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await dbContext.sqliteClient!.createItem(updatedItem, attachments, totpCodes);
|
||||
}
|
||||
@@ -445,7 +456,7 @@ const ItemAddEdit: React.FC = () => {
|
||||
} catch (err) {
|
||||
console.error('Error saving item:', err);
|
||||
}
|
||||
}, [item, fieldValues, applicableSystemFields, customFields, dbContext, isEditMode, executeVaultMutationAsync, navigate, originalAttachmentIds, attachments, originalTotpCodeIds, totpCodes]);
|
||||
}, [item, fieldValues, applicableSystemFields, customFields, dbContext, isEditMode, executeVaultMutationAsync, navigate, originalAttachmentIds, attachments, originalTotpCodeIds, totpCodes, passkeyIdsMarkedForDeletion]);
|
||||
|
||||
/**
|
||||
* Handle delete action.
|
||||
@@ -807,6 +818,15 @@ const ItemAddEdit: React.FC = () => {
|
||||
))}
|
||||
</FormSection>
|
||||
|
||||
{/* Passkey Section - only show in edit mode for items with passkeys */}
|
||||
{isEditMode && item.HasPasskey && (
|
||||
<PasskeyEditor
|
||||
itemId={item.Id}
|
||||
passkeyIdsMarkedForDeletion={passkeyIdsMarkedForDeletion}
|
||||
onPasskeyMarkedForDeletion={setPasskeyIdsMarkedForDeletion}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Render fields grouped by category */}
|
||||
{Object.keys(groupedSystemFields).map(category => {
|
||||
const categoryFields = groupedSystemFields[category];
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
TotpBlock,
|
||||
AttachmentBlock,
|
||||
FieldBlock
|
||||
FieldBlock,
|
||||
PasskeyBlock
|
||||
} from '@/entrypoints/popup/components/Credentials/Details';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
@@ -16,6 +17,7 @@ import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import type { Item } from '@/utils/dist/core/models/vault';
|
||||
import { ItemTypes } from '@/utils/dist/core/models/vault';
|
||||
import { groupFieldsByCategory } from '@/utils/dist/core/models/vault';
|
||||
|
||||
/**
|
||||
@@ -158,10 +160,15 @@ const ItemDetails: React.FC = (): React.ReactElement => {
|
||||
</div>
|
||||
|
||||
{/* TOTP codes - only for Login and Alias types, shown at top */}
|
||||
{(item.ItemType === 'Login' || item.ItemType === 'Alias') && (
|
||||
{(item.ItemType === ItemTypes.Login || item.ItemType === ItemTypes.Alias) && (
|
||||
<TotpBlock itemId={item.Id} />
|
||||
)}
|
||||
|
||||
{/* Passkeys - only for Login and Alias types */}
|
||||
{(item.ItemType === ItemTypes.Login || item.ItemType === ItemTypes.Alias) && item.HasPasskey && (
|
||||
<PasskeyBlock itemId={item.Id} />
|
||||
)}
|
||||
|
||||
{/* Render fields dynamically by category */}
|
||||
{Object.keys(groupedFields).length > 0 && (
|
||||
<>
|
||||
|
||||
@@ -15,11 +15,12 @@ import { useVaultLockRedirect } from '@/entrypoints/popup/hooks/useVaultLockRedi
|
||||
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
|
||||
import { PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants';
|
||||
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
import type { Passkey } from '@/utils/dist/core/models/vault';
|
||||
import { extractDomain, extractRootDomain, filterCredentials, AutofillMatchingMode } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
import type { Credential, Passkey } from '@/utils/dist/core/models/vault';
|
||||
import { PasskeyAuthenticator } from '@/utils/passkey/PasskeyAuthenticator';
|
||||
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';
|
||||
import type { CreateRequest, PasskeyCreateCredentialResponse, PendingPasskeyCreateRequest } from '@/utils/passkey/types';
|
||||
import { SqliteClient } from '@/utils/SqliteClient';
|
||||
|
||||
import { storage } from "#imports";
|
||||
|
||||
@@ -38,7 +39,9 @@ const PasskeyCreate: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { isLocked } = useVaultLockRedirect();
|
||||
const [existingPasskeys, setExistingPasskeys] = useState<Array<Passkey & { Username?: string | null; ServiceName?: string | null }>>([]);
|
||||
const [matchingCredentials, setMatchingCredentials] = useState<Credential[]>([]);
|
||||
const [selectedPasskeyToReplace, setSelectedPasskeyToReplace] = useState<string | null>(null);
|
||||
const [selectedCredentialToAttach, setSelectedCredentialToAttach] = useState<string | null>(null);
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [localLoading, setLocalLoading] = useState(false);
|
||||
const [showBypassDialog, setShowBypassDialog] = useState(false);
|
||||
@@ -123,9 +126,43 @@ const PasskeyCreate: React.FC = () => {
|
||||
}
|
||||
|
||||
setExistingPasskeys(filtered);
|
||||
// If no existing passkeys for this user, go straight to create form
|
||||
|
||||
// If no existing passkeys for this user, check for matching credentials
|
||||
if (filtered.length === 0) {
|
||||
setShowCreateForm(true);
|
||||
// Get all credentials and filter for matches
|
||||
const allCredentials = dbContext.sqliteClient.getAllCredentials();
|
||||
|
||||
/*
|
||||
* Filter credentials that:
|
||||
* 1. Match the RP origin URL
|
||||
* 2. Have username/password (are login credentials)
|
||||
* 3. Don't already have a passkey
|
||||
*/
|
||||
const credentialsWithoutPasskeys = allCredentials.filter(cred => {
|
||||
// Must have username or password to be a login credential
|
||||
if (!cred.Username && !cred.Password) {
|
||||
return false;
|
||||
}
|
||||
// Check if this credential already has a passkey
|
||||
return !cred.HasPasskey;
|
||||
});
|
||||
|
||||
// Use the credential matcher to find matching credentials for the origin
|
||||
let matches: Credential[] = [];
|
||||
if (credentialsWithoutPasskeys.length > 0) {
|
||||
matches = await filterCredentials(
|
||||
credentialsWithoutPasskeys,
|
||||
data.origin,
|
||||
data.publicKey.rp.name || '',
|
||||
AutofillMatchingMode.URL_SUBDOMAIN
|
||||
);
|
||||
setMatchingCredentials(matches);
|
||||
}
|
||||
|
||||
// If no matching credentials, go straight to create form
|
||||
if (matches.length === 0) {
|
||||
setShowCreateForm(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,6 +210,7 @@ const PasskeyCreate: React.FC = () => {
|
||||
*/
|
||||
const handleCreateNew = () : void => {
|
||||
setSelectedPasskeyToReplace(null);
|
||||
setSelectedCredentialToAttach(null);
|
||||
setShowCreateForm(true);
|
||||
};
|
||||
|
||||
@@ -181,6 +219,16 @@ const PasskeyCreate: React.FC = () => {
|
||||
*/
|
||||
const handleSelectReplace = (passkeyId: string) : void => {
|
||||
setSelectedPasskeyToReplace(passkeyId);
|
||||
setSelectedCredentialToAttach(null);
|
||||
setShowCreateForm(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle when user selects an existing credential to attach the passkey to
|
||||
*/
|
||||
const handleSelectCredential = (credentialId: string) : void => {
|
||||
setSelectedCredentialToAttach(credentialId);
|
||||
setSelectedPasskeyToReplace(null);
|
||||
setShowCreateForm(true);
|
||||
};
|
||||
|
||||
@@ -316,6 +364,33 @@ const PasskeyCreate: React.FC = () => {
|
||||
AdditionalData: null
|
||||
});
|
||||
}
|
||||
} else if (selectedCredentialToAttach) {
|
||||
// Attach passkey to existing credential/item
|
||||
/**
|
||||
* Create the Passkey linked to the existing item
|
||||
* Convert userId from base64 string to byte array for database storage
|
||||
*/
|
||||
let userHandleBytes: Uint8Array | null = null;
|
||||
if (stored.userId) {
|
||||
try {
|
||||
userHandleBytes = PasskeyHelper.base64urlToBytes(stored.userId);
|
||||
} catch {
|
||||
// If conversion fails, store as null
|
||||
userHandleBytes = null;
|
||||
}
|
||||
}
|
||||
|
||||
await dbContext.sqliteClient!.createPasskey({
|
||||
Id: newPasskeyGuid,
|
||||
ItemId: selectedCredentialToAttach,
|
||||
RpId: stored.rpId,
|
||||
UserHandle: userHandleBytes,
|
||||
PublicKey: JSON.stringify(stored.publicKey),
|
||||
PrivateKey: JSON.stringify(stored.privateKey),
|
||||
DisplayName: request.publicKey.user.displayName || request.publicKey.user.name || '',
|
||||
PrfKey: stored.prfSecret ? PasskeyHelper.base64urlToBytes(stored.prfSecret) : undefined,
|
||||
AdditionalData: null
|
||||
});
|
||||
} else {
|
||||
// Create new item and passkey
|
||||
const itemId = await dbContext.sqliteClient!.createCredential(
|
||||
@@ -503,7 +578,7 @@ const PasskeyCreate: React.FC = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Step 1: Show existing passkeys selection or create new option */}
|
||||
{/* Step 1a: Show existing passkeys selection or create new option */}
|
||||
{!showCreateForm && existingPasskeys.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
@@ -570,6 +645,87 @@ const PasskeyCreate: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 1b: Show matching credentials to attach passkey to (when no existing passkeys) */}
|
||||
{!showCreateForm && existingPasskeys.length === 0 && matchingCredentials.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleCreateNew}
|
||||
ref={createNewButtonRef}
|
||||
>
|
||||
{t('passkeys.create.createNewPasskey')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleFallback}
|
||||
>
|
||||
{t('passkeys.create.useBrowserPasskey')}
|
||||
</Button>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300 dark:border-gray-600" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400">
|
||||
{t('common.or')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{t('passkeys.create.selectExistingLogin')}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{t('passkeys.create.selectExistingLoginDescription')}
|
||||
</p>
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto border rounded-lg p-2 bg-gray-50 dark:bg-gray-800">
|
||||
{matchingCredentials.map((credential) => (
|
||||
<button
|
||||
key={credential.Id}
|
||||
onClick={() => handleSelectCredential(credential.Id)}
|
||||
className="w-full p-3 text-left rounded-lg border cursor-pointer transition-colors bg-white border-gray-200 hover:bg-gray-100 hover:border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:hover:bg-gray-600 dark:hover:border-gray-500 focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
{credential.Logo && (
|
||||
<img
|
||||
src={SqliteClient.imgSrcFromBytes(credential.Logo)}
|
||||
alt=""
|
||||
className="w-8 h-8 rounded mr-3 flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900 dark:text-white text-sm truncate">
|
||||
{credential.ServiceName}
|
||||
</div>
|
||||
{credential.Username && (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 truncate">
|
||||
{credential.Username}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<svg className="w-5 h-5 text-gray-400 flex-shrink-0 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Show create form with display name */}
|
||||
{showCreateForm && (
|
||||
<div className="space-y-4">
|
||||
@@ -581,29 +737,44 @@ const PasskeyCreate: React.FC = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<FormInput
|
||||
id="displayName"
|
||||
label={t('passkeys.create.titleLabel')}
|
||||
value={displayName}
|
||||
onChange={setDisplayName}
|
||||
placeholder={t('passkeys.create.titlePlaceholder')}
|
||||
ref={displayNameInputRef}
|
||||
/>
|
||||
{selectedCredentialToAttach && (
|
||||
<Alert variant="info">
|
||||
{t('passkeys.create.attachingToCredential', {
|
||||
serviceName: matchingCredentials.find(c => c.Id === selectedCredentialToAttach)?.ServiceName || ''
|
||||
})}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!selectedCredentialToAttach && (
|
||||
<FormInput
|
||||
id="displayName"
|
||||
label={t('passkeys.create.titleLabel')}
|
||||
value={displayName}
|
||||
onChange={setDisplayName}
|
||||
placeholder={t('passkeys.create.titlePlaceholder')}
|
||||
ref={displayNameInputRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleCreate}
|
||||
>
|
||||
{selectedPasskeyToReplace ? t('passkeys.create.confirmReplace') : t('passkeys.create.createButton')}
|
||||
{selectedPasskeyToReplace
|
||||
? t('passkeys.create.confirmReplace')
|
||||
: selectedCredentialToAttach
|
||||
? t('passkeys.create.attachPasskey')
|
||||
: t('passkeys.create.createButton')}
|
||||
</Button>
|
||||
|
||||
{existingPasskeys.length > 0 ? (
|
||||
{(existingPasskeys.length > 0 || matchingCredentials.length > 0) ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowCreateForm(false);
|
||||
setSelectedPasskeyToReplace(null);
|
||||
setSelectedCredentialToAttach(null);
|
||||
}}
|
||||
>
|
||||
{t('common.back')}
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"next": "Next",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"undo": "Undo",
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"create": "Create",
|
||||
@@ -525,7 +526,11 @@
|
||||
"selectPasskeyToReplace": "Select a passkey to replace:",
|
||||
"createNewPasskey": "Create New Passkey",
|
||||
"replacingPasskey": "Replacing passkey: {{displayName}}",
|
||||
"confirmReplace": "Confirm Replace"
|
||||
"confirmReplace": "Confirm Replace",
|
||||
"selectExistingLogin": "Add to existing login:",
|
||||
"selectExistingLoginDescription": "Attach this passkey to an existing login credential for this site.",
|
||||
"attachingToCredential": "Adding passkey to: {{serviceName}}",
|
||||
"attachPasskey": "Add Passkey"
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey Provider",
|
||||
|
||||
@@ -3066,8 +3066,10 @@ export class SqliteClient {
|
||||
|
||||
const currentDateTime = dateFormatter.now();
|
||||
|
||||
// 1. Move all items in this folder to trash (set DeletedAt) and clear FolderId
|
||||
// so that when restored, items won't reference a deleted folder
|
||||
/*
|
||||
* 1. Move all items in this folder to trash (set DeletedAt) and clear FolderId
|
||||
* so that when restored, items won't reference a deleted folder
|
||||
*/
|
||||
const itemsQuery = `
|
||||
UPDATE Items
|
||||
SET DeletedAt = ?,
|
||||
|
||||
@@ -322,16 +322,20 @@ type ItemTag = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Item types supported by the vault
|
||||
* - Login: Username/password credentials with optional notes
|
||||
* - Alias: Login with pre-filled alias identity fields (email, name, etc.)
|
||||
* - CreditCard: Payment card information
|
||||
* - Note: Secure notes
|
||||
* Item types supported by the vault.
|
||||
*/
|
||||
type ItemType = 'Login' | 'Alias' | 'CreditCard' | 'Note';
|
||||
declare const ItemTypes: {
|
||||
readonly Login: "Login";
|
||||
readonly Alias: "Alias";
|
||||
readonly CreditCard: "CreditCard";
|
||||
readonly Note: "Note";
|
||||
};
|
||||
/**
|
||||
* Item type union derived from ItemTypes constant
|
||||
*/
|
||||
type ItemType = typeof ItemTypes[keyof typeof ItemTypes];
|
||||
/**
|
||||
* Item type representing vault entries in the new field-based data model.
|
||||
* Replaces the old Credential type.
|
||||
*/
|
||||
type Item = {
|
||||
Id: string;
|
||||
@@ -361,7 +365,6 @@ type ItemField = {
|
||||
};
|
||||
/**
|
||||
* Field types for rendering and validation.
|
||||
* Single source of truth - the type is derived from this constant.
|
||||
*/
|
||||
declare const FieldTypes: {
|
||||
readonly Text: "Text";
|
||||
@@ -551,4 +554,4 @@ type FieldHistory = {
|
||||
*/
|
||||
declare const MAX_FIELD_HISTORY_RECORDS = 10;
|
||||
|
||||
export { type Alias, type Attachment, type Credential, type EncryptionKey, FieldCategories, type FieldCategory, type FieldHistory, FieldKey, type FieldKeyValue, type FieldType, FieldTypes, type Item, type ItemField, type ItemTag, type ItemTagRef, type ItemType, type ItemTypeFieldConfig, MAX_FIELD_HISTORY_RECORDS, type Passkey, type PasswordSettings, type SystemFieldDefinition, SystemFieldRegistry, type Tag, type TotpCode, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
|
||||
export { type Alias, type Attachment, type Credential, type EncryptionKey, FieldCategories, type FieldCategory, type FieldHistory, FieldKey, type FieldKeyValue, type FieldType, FieldTypes, type Item, type ItemField, type ItemTag, type ItemTagRef, type ItemType, type ItemTypeFieldConfig, ItemTypes, MAX_FIELD_HISTORY_RECORDS, type Passkey, type PasswordSettings, type SystemFieldDefinition, SystemFieldRegistry, type Tag, type TotpCode, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
|
||||
|
||||
@@ -152,6 +152,12 @@ var FieldKey = {
|
||||
};
|
||||
|
||||
// src/vault/Item.ts
|
||||
var ItemTypes = {
|
||||
Login: "Login",
|
||||
Alias: "Alias",
|
||||
CreditCard: "CreditCard",
|
||||
Note: "Note"
|
||||
};
|
||||
var FieldTypes = {
|
||||
Text: "Text",
|
||||
Password: "Password",
|
||||
@@ -483,6 +489,6 @@ function isSystemFieldPrefix(fieldKey) {
|
||||
// src/vault/FieldHistory.ts
|
||||
var MAX_FIELD_HISTORY_RECORDS = 10;
|
||||
|
||||
export { FieldCategories, FieldKey, FieldTypes, MAX_FIELD_HISTORY_RECORDS, SystemFieldRegistry, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
|
||||
export { FieldCategories, FieldKey, FieldTypes, ItemTypes, MAX_FIELD_HISTORY_RECORDS, SystemFieldRegistry, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
|
||||
//# sourceMappingURL=index.js.map
|
||||
//# sourceMappingURL=index.js.map
|
||||
@@ -322,16 +322,20 @@ type ItemTag = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Item types supported by the vault
|
||||
* - Login: Username/password credentials with optional notes
|
||||
* - Alias: Login with pre-filled alias identity fields (email, name, etc.)
|
||||
* - CreditCard: Payment card information
|
||||
* - Note: Secure notes
|
||||
* Item types supported by the vault.
|
||||
*/
|
||||
type ItemType = 'Login' | 'Alias' | 'CreditCard' | 'Note';
|
||||
declare const ItemTypes: {
|
||||
readonly Login: "Login";
|
||||
readonly Alias: "Alias";
|
||||
readonly CreditCard: "CreditCard";
|
||||
readonly Note: "Note";
|
||||
};
|
||||
/**
|
||||
* Item type union derived from ItemTypes constant
|
||||
*/
|
||||
type ItemType = typeof ItemTypes[keyof typeof ItemTypes];
|
||||
/**
|
||||
* Item type representing vault entries in the new field-based data model.
|
||||
* Replaces the old Credential type.
|
||||
*/
|
||||
type Item = {
|
||||
Id: string;
|
||||
@@ -361,7 +365,6 @@ type ItemField = {
|
||||
};
|
||||
/**
|
||||
* Field types for rendering and validation.
|
||||
* Single source of truth - the type is derived from this constant.
|
||||
*/
|
||||
declare const FieldTypes: {
|
||||
readonly Text: "Text";
|
||||
@@ -551,4 +554,4 @@ type FieldHistory = {
|
||||
*/
|
||||
declare const MAX_FIELD_HISTORY_RECORDS = 10;
|
||||
|
||||
export { type Alias, type Attachment, type Credential, type EncryptionKey, FieldCategories, type FieldCategory, type FieldHistory, FieldKey, type FieldKeyValue, type FieldType, FieldTypes, type Item, type ItemField, type ItemTag, type ItemTagRef, type ItemType, type ItemTypeFieldConfig, MAX_FIELD_HISTORY_RECORDS, type Passkey, type PasswordSettings, type SystemFieldDefinition, SystemFieldRegistry, type Tag, type TotpCode, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
|
||||
export { type Alias, type Attachment, type Credential, type EncryptionKey, FieldCategories, type FieldCategory, type FieldHistory, FieldKey, type FieldKeyValue, type FieldType, FieldTypes, type Item, type ItemField, type ItemTag, type ItemTagRef, type ItemType, type ItemTypeFieldConfig, ItemTypes, MAX_FIELD_HISTORY_RECORDS, type Passkey, type PasswordSettings, type SystemFieldDefinition, SystemFieldRegistry, type Tag, type TotpCode, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
|
||||
|
||||
@@ -152,6 +152,12 @@ var FieldKey = {
|
||||
};
|
||||
|
||||
// src/vault/Item.ts
|
||||
var ItemTypes = {
|
||||
Login: "Login",
|
||||
Alias: "Alias",
|
||||
CreditCard: "CreditCard",
|
||||
Note: "Note"
|
||||
};
|
||||
var FieldTypes = {
|
||||
Text: "Text",
|
||||
Password: "Password",
|
||||
@@ -483,6 +489,6 @@ function isSystemFieldPrefix(fieldKey) {
|
||||
// src/vault/FieldHistory.ts
|
||||
var MAX_FIELD_HISTORY_RECORDS = 10;
|
||||
|
||||
export { FieldCategories, FieldKey, FieldTypes, MAX_FIELD_HISTORY_RECORDS, SystemFieldRegistry, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
|
||||
export { FieldCategories, FieldKey, FieldTypes, ItemTypes, MAX_FIELD_HISTORY_RECORDS, SystemFieldRegistry, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
|
||||
//# sourceMappingURL=index.js.map
|
||||
//# sourceMappingURL=index.js.map
|
||||
@@ -1,19 +1,20 @@
|
||||
/**
|
||||
* Item types supported by the vault
|
||||
* - Login: Username/password credentials with optional notes
|
||||
* - Alias: Login with pre-filled alias identity fields (email, name, etc.)
|
||||
* - CreditCard: Payment card information
|
||||
* - Note: Secure notes
|
||||
* Item types supported by the vault.
|
||||
*/
|
||||
export type ItemType =
|
||||
| 'Login'
|
||||
| 'Alias'
|
||||
| 'CreditCard'
|
||||
| 'Note';
|
||||
export const ItemTypes = {
|
||||
Login: 'Login',
|
||||
Alias: 'Alias',
|
||||
CreditCard: 'CreditCard',
|
||||
Note: 'Note',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Item type union derived from ItemTypes constant
|
||||
*/
|
||||
export type ItemType = typeof ItemTypes[keyof typeof ItemTypes];
|
||||
|
||||
/**
|
||||
* Item type representing vault entries in the new field-based data model.
|
||||
* Replaces the old Credential type.
|
||||
*/
|
||||
export type Item = {
|
||||
Id: string;
|
||||
@@ -45,7 +46,6 @@ export type ItemField = {
|
||||
|
||||
/**
|
||||
* Field types for rendering and validation.
|
||||
* Single source of truth - the type is derived from this constant.
|
||||
*/
|
||||
export const FieldTypes = {
|
||||
Text: 'Text',
|
||||
|
||||
Reference in New Issue
Block a user