mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-01 00:31:08 +00:00
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.
This commit is contained in:
parent
99a6ea0459
commit
308eef1cff
5 changed files with 138 additions and 27 deletions
121
src/lib/accounts/manager.svelte.ts
Normal file
121
src/lib/accounts/manager.svelte.ts
Normal file
|
@ -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<BlahIdentity> {
|
||||
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<string> {
|
||||
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();
|
|
@ -1,8 +0,0 @@
|
|||
import type { BlahSignedPayload } from '@blah-im/core/crypto';
|
||||
|
||||
export type BlahActKeyEntry = {
|
||||
exp: number;
|
||||
};
|
||||
export type BlahSignedActKeyEntry = BlahSignedPayload<BlahActKeyEntry>;
|
||||
|
||||
export type BlahKeyBox = BlahSignedPayload<BlahSignedActKeyEntry[]>;
|
|
@ -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<EncodedBlahKeyPair[]>('weblah-keypairs', []);
|
||||
export const currentKeyIndex = persisted<number>('weblah-current-key-index', 0);
|
||||
export const currentKeyPair = derived(
|
||||
[keyStore, currentKeyIndex],
|
||||
([keyStore, currentKeyIndex]) => keyStore[currentKeyIndex]
|
||||
);
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { GroupedListItem, GroupedListSection } from '$lib/components/GroupedList';
|
||||
import { ArrowRightEndOnRectangle, UserPlus } from 'svelte-hero-icons';
|
||||
import { ArrowRightEndOnRectangle, Plus, UserPlus } from 'svelte-hero-icons';
|
||||
import SettingsListItem from './SettingsListItem.svelte';
|
||||
import {
|
||||
openAccountStore,
|
||||
|
@ -66,6 +66,12 @@
|
|||
{/if}
|
||||
|
||||
<GroupedListSection>
|
||||
<SettingsListItem icon={ArrowRightEndOnRectangle} route="/account/add">Sign in</SettingsListItem>
|
||||
{#if ($accountStore?.length ?? 0) > 0}
|
||||
<SettingsListItem icon={Plus} route="/account/add">Add Account</SettingsListItem>
|
||||
{:else}
|
||||
<SettingsListItem icon={ArrowRightEndOnRectangle} route="/account/add">
|
||||
Sign in
|
||||
</SettingsListItem>
|
||||
<SettingsListItem icon={UserPlus} route="/account/new">Create Account</SettingsListItem>
|
||||
{/if}
|
||||
</GroupedListSection>
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
import { tw } from '$lib/tw';
|
||||
import {
|
||||
Bell,
|
||||
Cog,
|
||||
DevicePhoneMobile,
|
||||
InformationCircle,
|
||||
LockClosed,
|
||||
QuestionMarkCircle
|
||||
QuestionMarkCircle,
|
||||
User
|
||||
} from 'svelte-hero-icons';
|
||||
import { scale } from 'svelte/transition';
|
||||
import SettingsListItem from './SettingsListItem.svelte';
|
||||
|
@ -20,11 +20,10 @@
|
|||
}
|
||||
|
||||
let { class: className = '' }: Props = $props();
|
||||
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={tw('flex flex-col bg-sb-secondary shadow-md', className)}
|
||||
class={tw('bg-sb-secondary flex flex-col shadow-md', className)}
|
||||
transition:scale={{ duration: 250, start: 0.95 }}
|
||||
>
|
||||
<PageHeader>
|
||||
|
@ -36,10 +35,13 @@
|
|||
<SettingsAccountSections />
|
||||
|
||||
<GroupedListSection>
|
||||
<SettingsListItem icon={Cog} route="">General</SettingsListItem>
|
||||
<SettingsListItem icon={User} route="">My Profile</SettingsListItem>
|
||||
<GroupedListItem icon={DevicePhoneMobile}>Devices</GroupedListItem>
|
||||
</GroupedListSection>
|
||||
|
||||
<GroupedListSection>
|
||||
<GroupedListItem icon={Bell}>Notifications</GroupedListItem>
|
||||
<GroupedListItem icon={LockClosed}>Privacy and Security</GroupedListItem>
|
||||
<GroupedListItem icon={DevicePhoneMobile}>Devices</GroupedListItem>
|
||||
</GroupedListSection>
|
||||
|
||||
<GroupedListSection>
|
||||
|
|
Loading…
Add table
Reference in a new issue