fix: account isolation, auto-import signer certs, and no-key error handling #35
This commit is contained in:
@@ -11,6 +11,7 @@ import { useFilterStore } from '@/stores/filter-store';
|
||||
import { DEFAULT_SEARCH_FILTERS } from '@/lib/jmap/search-utils';
|
||||
import { useIdentityStore } from '@/stores/identity-store';
|
||||
import { useVacationStore } from '@/stores/vacation-store';
|
||||
import { useSmimeStore } from '@/stores/smime-store';
|
||||
|
||||
// Minimal snapshot shapes — we only capture what we need
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -117,6 +118,7 @@ export function clearAllStores(): void {
|
||||
useVacationStore.getState().clearState();
|
||||
useCalendarStore.getState().clearState();
|
||||
useFilterStore.getState().clearState();
|
||||
useSmimeStore.getState().clearState();
|
||||
}
|
||||
|
||||
/** Evict cached state for one account */
|
||||
|
||||
@@ -148,4 +148,50 @@ describe('key-storage', () => {
|
||||
expect(certs.find(c => c.id === id)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('accountId filtering', () => {
|
||||
it('listKeyRecords filters by accountId', async () => {
|
||||
const id1 = uid();
|
||||
const id2 = uid();
|
||||
await saveKeyRecord(makeKeyRecord({ id: id1, email: `${id1}@a.com`, accountId: 'acct-1' }));
|
||||
await saveKeyRecord(makeKeyRecord({ id: id2, email: `${id2}@b.com`, accountId: 'acct-2' }));
|
||||
|
||||
const acct1Records = await listKeyRecords('acct-1');
|
||||
expect(acct1Records.find(r => r.id === id1)).toBeDefined();
|
||||
expect(acct1Records.find(r => r.id === id2)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('listKeyRecords includes records without accountId when filtering', async () => {
|
||||
const id1 = uid();
|
||||
const id2 = uid();
|
||||
await saveKeyRecord(makeKeyRecord({ id: id1, email: `${id1}@a.com` }));
|
||||
await saveKeyRecord(makeKeyRecord({ id: id2, email: `${id2}@b.com`, accountId: 'acct-1' }));
|
||||
|
||||
const acct1Records = await listKeyRecords('acct-1');
|
||||
expect(acct1Records.find(r => r.id === id1)).toBeDefined();
|
||||
expect(acct1Records.find(r => r.id === id2)).toBeDefined();
|
||||
});
|
||||
|
||||
it('listPublicCerts filters by accountId', async () => {
|
||||
const id1 = uid();
|
||||
const id2 = uid();
|
||||
await savePublicCert(makePublicCert({ id: id1, email: `${id1}@a.com`, accountId: 'acct-1' }));
|
||||
await savePublicCert(makePublicCert({ id: id2, email: `${id2}@b.com`, accountId: 'acct-2' }));
|
||||
|
||||
const acct1Certs = await listPublicCerts('acct-1');
|
||||
expect(acct1Certs.find(c => c.id === id1)).toBeDefined();
|
||||
expect(acct1Certs.find(c => c.id === id2)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('listPublicCerts includes certs without accountId when filtering', async () => {
|
||||
const id1 = uid();
|
||||
const id2 = uid();
|
||||
await savePublicCert(makePublicCert({ id: id1, email: `${id1}@a.com` }));
|
||||
await savePublicCert(makePublicCert({ id: id2, email: `${id2}@b.com`, accountId: 'acct-1' }));
|
||||
|
||||
const acct1Certs = await listPublicCerts('acct-1');
|
||||
expect(acct1Certs.find(c => c.id === id1)).toBeDefined();
|
||||
expect(acct1Certs.find(c => c.id === id2)).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,9 @@ beforeEach(() => {
|
||||
defaultSignIdentity: {},
|
||||
defaultEncrypt: false,
|
||||
rememberUnlockedKeys: false,
|
||||
autoImportSignerCerts: false,
|
||||
autoImportSignerCerts: true,
|
||||
accountPreferences: {},
|
||||
currentAccountId: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
import type { SmimeKeyRecord, SmimePublicCert } from './types';
|
||||
|
||||
const DB_NAME = 'smime-store';
|
||||
const DB_VERSION = 1;
|
||||
const DB_VERSION = 2;
|
||||
const KEY_RECORDS_STORE = 'key-records';
|
||||
const PUBLIC_CERTS_STORE = 'public-certs';
|
||||
|
||||
function openDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
request.onupgradeneeded = () => {
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(KEY_RECORDS_STORE)) {
|
||||
const oldVersion = event.oldVersion;
|
||||
if (oldVersion < 1) {
|
||||
const keyStore = db.createObjectStore(KEY_RECORDS_STORE, { keyPath: 'id' });
|
||||
keyStore.createIndex('email', 'email', { unique: false });
|
||||
}
|
||||
if (!db.objectStoreNames.contains(PUBLIC_CERTS_STORE)) {
|
||||
keyStore.createIndex('accountId', 'accountId', { unique: false });
|
||||
const certStore = db.createObjectStore(PUBLIC_CERTS_STORE, { keyPath: 'id' });
|
||||
certStore.createIndex('email', 'email', { unique: false });
|
||||
certStore.createIndex('accountId', 'accountId', { unique: false });
|
||||
}
|
||||
if (oldVersion >= 1 && oldVersion < 2) {
|
||||
// Add accountId index to existing stores
|
||||
const tx = request.transaction!;
|
||||
const keyStore = tx.objectStore(KEY_RECORDS_STORE);
|
||||
if (!keyStore.indexNames.contains('accountId')) {
|
||||
keyStore.createIndex('accountId', 'accountId', { unique: false });
|
||||
}
|
||||
const certStore = tx.objectStore(PUBLIC_CERTS_STORE);
|
||||
if (!certStore.indexNames.contains('accountId')) {
|
||||
certStore.createIndex('accountId', 'accountId', { unique: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
@@ -62,9 +75,11 @@ export async function getKeyRecordForEmail(email: string): Promise<SmimeKeyRecor
|
||||
});
|
||||
}
|
||||
|
||||
export async function listKeyRecords(): Promise<SmimeKeyRecord[]> {
|
||||
export async function listKeyRecords(accountId?: string): Promise<SmimeKeyRecord[]> {
|
||||
const db = await openDB();
|
||||
return txPromise(db, KEY_RECORDS_STORE, 'readonly', (s) => s.getAll());
|
||||
const all = await txPromise<SmimeKeyRecord[]>(db, KEY_RECORDS_STORE, 'readonly', (s) => s.getAll());
|
||||
if (!accountId) return all;
|
||||
return all.filter((r) => r.accountId === accountId || !r.accountId);
|
||||
}
|
||||
|
||||
export async function deleteKeyRecord(id: string): Promise<void> {
|
||||
@@ -90,9 +105,11 @@ export async function getPublicCertForEmail(email: string): Promise<SmimePublicC
|
||||
});
|
||||
}
|
||||
|
||||
export async function listPublicCerts(): Promise<SmimePublicCert[]> {
|
||||
export async function listPublicCerts(accountId?: string): Promise<SmimePublicCert[]> {
|
||||
const db = await openDB();
|
||||
return txPromise(db, PUBLIC_CERTS_STORE, 'readonly', (s) => s.getAll());
|
||||
const all = await txPromise<SmimePublicCert[]>(db, PUBLIC_CERTS_STORE, 'readonly', (s) => s.getAll());
|
||||
if (!accountId) return all;
|
||||
return all.filter((c) => c.accountId === accountId || !c.accountId);
|
||||
}
|
||||
|
||||
export async function deletePublicCert(id: string): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Stored record for an imported S/MIME private key + certificate. */
|
||||
export interface SmimeKeyRecord {
|
||||
id: string;
|
||||
accountId?: string;
|
||||
email: string;
|
||||
certificate: ArrayBuffer; // DER-encoded X.509 leaf cert
|
||||
certificateChain: ArrayBuffer[]; // DER-encoded intermediates
|
||||
@@ -34,6 +35,7 @@ export interface SmimeUnlockedKey {
|
||||
/** A recipient or contact public certificate. */
|
||||
export interface SmimePublicCert {
|
||||
id: string;
|
||||
accountId?: string;
|
||||
email: string;
|
||||
certificate: ArrayBuffer; // DER-encoded X.509
|
||||
issuer: string;
|
||||
|
||||
Reference in New Issue
Block a user