From 308eef1cff87408778cbd6c6949638d2d1cfeb40 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Sun, 13 Apr 2025 02:23:06 +0800 Subject: [PATCH] feat: Implement AccountManager Replace simple keystore with a full-featured AccountManager that handles identity management, account creation, and authentication. Update account- related UI components to integrate with the new system. --- src/lib/accounts/manager.svelte.ts | 121 ++++++++++++++++++ src/lib/blah/structures/keybox.ts | 8 -- src/lib/keystore.ts | 10 -- .../settings/SettingsAccountSections.svelte | 12 +- src/routes/(app)/settings/SettingsList.svelte | 14 +- 5 files changed, 138 insertions(+), 27 deletions(-) create mode 100644 src/lib/accounts/manager.svelte.ts delete mode 100644 src/lib/blah/structures/keybox.ts delete mode 100644 src/lib/keystore.ts diff --git a/src/lib/accounts/manager.svelte.ts b/src/lib/accounts/manager.svelte.ts new file mode 100644 index 0000000..4b06b1d --- /dev/null +++ b/src/lib/accounts/manager.svelte.ts @@ -0,0 +1,121 @@ +import { type AccountKeyDB, openAccountKeyDB } from './accountKeyDB'; +import { + BlahIdentity, + type BlahIdentityDescription, + type BlahProfile +} from '@blah-im/core/identity'; +import { type IdentityDB, openIdentityDB } from './identityFileDB'; +import { BlahKeyPair } from '@blah-im/core/crypto'; +import { persisted } from 'svelte-persisted-store'; +import { browser } from '$app/environment'; + +export type Account = BlahIdentityDescription & { + holdingIdPrivate: boolean; + holdingPrivateOfActKeyId?: string; +}; + +const localStorageCurrentAccountIdKey = 'weblah-current-account-id-key'; + +class AccountManager { + private keyDB: AccountKeyDB | undefined; + private identityDB: IdentityDB | undefined; + + accounts: Account[] = $state([]); + inProgress: boolean = $state(true); + currentAccountId: string | null = $state(null); + currentAccount: Account | null = $derived( + this.accounts.find((account) => account.id_key === this.currentAccountId) ?? null + ); + + constructor() { + if (browser) { + this.currentAccountId = localStorage.getItem(localStorageCurrentAccountIdKey); + } + + $effect.root(() => { + $effect(() => + this.currentAccountId + ? localStorage.setItem(localStorageCurrentAccountIdKey, this.currentAccountId) + : localStorage.removeItem(localStorageCurrentAccountIdKey) + ); + }); + + (async () => { + this.inProgress = true; + const [keyDB, identityDB] = await Promise.all([openAccountKeyDB(), openIdentityDB()]); + this.keyDB = keyDB; + this.identityDB = identityDB; + await this.loadAccounts(); + this.inProgress = false; + })(); + } + + async loadAccounts() { + if (!this.keyDB || !this.identityDB) throw new Error('Account manager not initialized'); + + this.inProgress = true; + const accountCreds = await this.keyDB.fetchAllAccounts(); + const identityFileMap = await this.identityDB.fetchIdentities( + accountCreds.map((x) => x.idKeyId) + ); + + const accounts: Account[] = accountCreds.flatMap((creds) => { + const identityFile = identityFileMap.get(creds.idKeyId); + if (!identityFile) return []; + return [ + { + ...identityFile, + holdingIdPrivate: !!creds.encodedIdKeyPair, + holdingPrivateOfActKey: creds.actKeyPair.id + } + ]; + }); + + this.accounts = accounts; + this.inProgress = false; + } + + async identityForAccount( + accountOrIdKeyId: Account | string, + password?: string + ): Promise { + if (!this.keyDB || !this.identityDB) throw new Error('Account manager not initialized'); + + const idKeyId = + typeof accountOrIdKeyId === 'string' ? accountOrIdKeyId : accountOrIdKeyId.id_key; + + const identityFile = await this.identityDB.fetchIdentity(idKeyId); + if (!identityFile) throw new Error('Identity file not found'); + + const accountCreds = await this.keyDB.fetchAccount(idKeyId); + const encodedIdKeyPair = accountCreds?.encodedIdKeyPair; + const idKeyPair = encodedIdKeyPair + ? await BlahKeyPair.fromEncoded(encodedIdKeyPair, password) + : undefined; + const actKeyPair = accountCreds?.actKeyPair; + + return await BlahIdentity.fromIdentityDescription(identityFile, idKeyPair, actKeyPair); + } + + async saveIdentityDescription(identity: BlahIdentity) { + if (!this.identityDB) throw new Error('Account manager not initialized'); + + const identityDesc = identity.generateIdentityDescription(); + await this.identityDB.updateIdentity(identityDesc); + await this.loadAccounts(); + } + + async createAccount(profile: BlahProfile, password: string): Promise { + if (!this.keyDB) throw new Error('Account manager not initialized'); + + const idKeyPair = await BlahKeyPair.generate(true); + const actKeyPair = await BlahKeyPair.generate(false); + const identity = await BlahIdentity.create(idKeyPair, actKeyPair, profile); + const encodedIdKeyPair = await idKeyPair.encode(password); + await this.keyDB.addAccount(idKeyPair.id, actKeyPair, encodedIdKeyPair); + await this.saveIdentityDescription(identity); + return idKeyPair.id; + } +} + +export default new AccountManager(); diff --git a/src/lib/blah/structures/keybox.ts b/src/lib/blah/structures/keybox.ts deleted file mode 100644 index 051eaa8..0000000 --- a/src/lib/blah/structures/keybox.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { BlahSignedPayload } from '@blah-im/core/crypto'; - -export type BlahActKeyEntry = { - exp: number; -}; -export type BlahSignedActKeyEntry = BlahSignedPayload; - -export type BlahKeyBox = BlahSignedPayload; diff --git a/src/lib/keystore.ts b/src/lib/keystore.ts deleted file mode 100644 index 7d68322..0000000 --- a/src/lib/keystore.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { persisted } from 'svelte-persisted-store'; -import type { EncodedBlahKeyPair } from '@blah-im/core/crypto'; -import { derived } from 'svelte/store'; - -export const keyStore = persisted('weblah-keypairs', []); -export const currentKeyIndex = persisted('weblah-current-key-index', 0); -export const currentKeyPair = derived( - [keyStore, currentKeyIndex], - ([keyStore, currentKeyIndex]) => keyStore[currentKeyIndex] -); diff --git a/src/routes/(app)/settings/SettingsAccountSections.svelte b/src/routes/(app)/settings/SettingsAccountSections.svelte index afe885a..0e32624 100644 --- a/src/routes/(app)/settings/SettingsAccountSections.svelte +++ b/src/routes/(app)/settings/SettingsAccountSections.svelte @@ -1,6 +1,6 @@
@@ -36,10 +35,13 @@ - General + My Profile + Devices + + + Notifications Privacy and Security - Devices