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">
|
<script lang="ts">
|
||||||
import { GroupedListItem, GroupedListSection } from '$lib/components/GroupedList';
|
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 SettingsListItem from './SettingsListItem.svelte';
|
||||||
import {
|
import {
|
||||||
openAccountStore,
|
openAccountStore,
|
||||||
|
@ -66,6 +66,12 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<GroupedListSection>
|
<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>
|
<SettingsListItem icon={UserPlus} route="/account/new">Create Account</SettingsListItem>
|
||||||
|
{/if}
|
||||||
</GroupedListSection>
|
</GroupedListSection>
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
import { tw } from '$lib/tw';
|
import { tw } from '$lib/tw';
|
||||||
import {
|
import {
|
||||||
Bell,
|
Bell,
|
||||||
Cog,
|
|
||||||
DevicePhoneMobile,
|
DevicePhoneMobile,
|
||||||
InformationCircle,
|
InformationCircle,
|
||||||
LockClosed,
|
LockClosed,
|
||||||
QuestionMarkCircle
|
QuestionMarkCircle,
|
||||||
|
User
|
||||||
} from 'svelte-hero-icons';
|
} from 'svelte-hero-icons';
|
||||||
import { scale } from 'svelte/transition';
|
import { scale } from 'svelte/transition';
|
||||||
import SettingsListItem from './SettingsListItem.svelte';
|
import SettingsListItem from './SettingsListItem.svelte';
|
||||||
|
@ -20,11 +20,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let { class: className = '' }: Props = $props();
|
let { class: className = '' }: Props = $props();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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 }}
|
transition:scale={{ duration: 250, start: 0.95 }}
|
||||||
>
|
>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
|
@ -36,10 +35,13 @@
|
||||||
<SettingsAccountSections />
|
<SettingsAccountSections />
|
||||||
|
|
||||||
<GroupedListSection>
|
<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={Bell}>Notifications</GroupedListItem>
|
||||||
<GroupedListItem icon={LockClosed}>Privacy and Security</GroupedListItem>
|
<GroupedListItem icon={LockClosed}>Privacy and Security</GroupedListItem>
|
||||||
<GroupedListItem icon={DevicePhoneMobile}>Devices</GroupedListItem>
|
|
||||||
</GroupedListSection>
|
</GroupedListSection>
|
||||||
|
|
||||||
<GroupedListSection>
|
<GroupedListSection>
|
||||||
|
|
Loading…
Add table
Reference in a new issue