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
7 changes: 5 additions & 2 deletions apps/desktop/src/main/vault-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,13 @@ export class VaultService {
return this.vault;
}

async totpEntry(itemId: string): Promise<TotpEntry> {
/** Null for items that carry no one-time-code seed, e.g. a plain login. */
async totpEntry(itemId: string): Promise<TotpEntry | null> {
const vault = this.current();
const item = await vault.getItem(itemId);
if (!item) throw new Error('item not found');
const fields = await vault.listFields(itemId);
if (!fields.some((field) => field.purpose === 'totp_seed')) return null;
const numericField = async (name: string, fallback: number): Promise<number> => {
if (!fields.some((field) => field.name === name)) return fallback;
const value = Number(await vault.revealField(itemId, name));
Expand All @@ -107,6 +109,7 @@ export class VaultService {
async totpList(): Promise<TotpEntry[]> {
const vault = this.current();
const items = await vault.listItems({ kind: 'totp' });
return Promise.all(items.map((item) => this.totpEntry(item.id)));
const entries = await Promise.all(items.map((item) => this.totpEntry(item.id)));
return entries.filter((entry): entry is TotpEntry => entry !== null);
}
}
16 changes: 12 additions & 4 deletions apps/desktop/src/renderer/src/components/ItemDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ export const ItemDetail = ({
onChanged: () => void;
onDeleted: () => void;
}) => {
const [fields, setFields] = useState<VaultFieldMeta[]>([]);
// Keyed by item so a pending load never renders the previous item's fields.
const [loaded, setLoaded] = useState<{ itemId: string; fields: VaultFieldMeta[] }>({
itemId: item.id,
fields: [],
});
const [urls, setUrls] = useState<string[]>([]);
const [tags, setTags] = useState<VaultTag[]>([]);
const [revealed, setRevealed] = useState<Record<string, string>>({});
Expand All @@ -53,7 +57,7 @@ export const ItemDetail = ({

const refresh = useCallback(async () => {
setRevealed({});
setFields(await dcrypt.fields.list(item.id));
setLoaded({ itemId: item.id, fields: await dcrypt.fields.list(item.id) });
setUrls(await dcrypt.organize.urls(item.id));
setTags(await dcrypt.organize.tags(item.id));
}, [item.id]);
Expand All @@ -62,6 +66,7 @@ export const ItemDetail = ({
void refresh();
}, [refresh]);

const fields = loaded.itemId === item.id ? loaded.fields : [];
const hasTotpSeed = fields.some((field) => field.purpose === 'totp_seed');

useEffect(() => {
Expand All @@ -70,16 +75,19 @@ export const ItemDetail = ({
return;
}
let cancelled = false;
let timer: ReturnType<typeof setInterval> | undefined;
const tick = async () => {
try {
const entry = await dcrypt.totp.code(item.id);
if (!cancelled) setTotp(entry);
if (cancelled) return;
setTotp(entry);
if (!entry) clearInterval(timer);
} catch {
// vault locked mid-refresh
}
};
void tick();
const timer = setInterval(() => void tick(), 1000);
timer = setInterval(() => void tick(), 1000);
return () => {
cancelled = true;
clearInterval(timer);
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ export interface DcryptApi {
remove(itemId: string, name: string): Promise<void>;
};
totp: {
code(itemId: string): Promise<TotpEntry>;
/** Null when the item carries no one-time-code seed. */
code(itemId: string): Promise<TotpEntry | null>;
list(): Promise<TotpEntry[]>;
importUri(uri: string): Promise<VaultItem>;
};
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/commands/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ const totp = async (argv: ParsedArgs, prompter: Inquirerer): Promise<void> => {
try {
const item = await findItem(vault, first);
const fields = await vault.listFields(item.id);
if (!fields.some((field) => field.purpose === 'totp_seed')) {
throw new CliError(`"${item.title}" has no one-time-code secret`);
}
const numeric = async (name: string, fallback: number): Promise<number> => {
if (!fields.some((field) => field.name === name)) return fallback;
const value = Number(await vault.revealField(item.id, name));
Expand Down
Loading