feat[wip]: Add password change functionality
Some checks failed
Build & Test / build (20.x) (push) Has been cancelled
Build & Test / build (22.x) (push) Has been cancelled

Add method to update ID key pair encoding for password changes and create
privacy-security page with password change dialog.
This commit is contained in:
Shibo Lyu 2025-04-28 00:54:54 +08:00
parent 8ca572e32e
commit c5716718bf
8 changed files with 120 additions and 15 deletions

View file

@ -76,6 +76,21 @@ class AccountKeyDB {
return { idKeyId, encodedIdKeyPair, actKeyPair }; return { idKeyId, encodedIdKeyPair, actKeyPair };
} }
async updateEncodedIdKeyPair(
idKeyId: string,
encodedIdKeyPair: EncodedBlahKeyPair
): Promise<void> {
const tx = this.db.transaction(IDB_OBJECT_STORE_NAME, 'readwrite');
const currentObject = await tx.store.get(idKeyId);
if (!currentObject) {
await tx.done;
return;
}
currentObject.encodedIdKeyPair = encodedIdKeyPair;
await tx.store.put(currentObject);
await tx.done;
}
async remove(idKeyId: string): Promise<void> { async remove(idKeyId: string): Promise<void> {
await this.db.delete(IDB_OBJECT_STORE_NAME, idKeyId); await this.db.delete(IDB_OBJECT_STORE_NAME, idKeyId);
} }

View file

@ -0,0 +1,3 @@
import accountManager from './manager.svelte';
export { accountManager };

View file

@ -106,6 +106,24 @@ class AccountManager {
await this.loadAccounts(); await this.loadAccounts();
} }
async changePassword(
accountOrIdKeyId: Account | string,
oldPassword: string,
newPassword: string
) {
if (!this.keyDB) throw new Error('Account manager not initialized');
const idKeyId =
typeof accountOrIdKeyId === 'string' ? accountOrIdKeyId : accountOrIdKeyId.id_key;
const accountCreds = await this.keyDB.fetchAccount(idKeyId);
const encodedIdKeyPair = accountCreds?.encodedIdKeyPair;
if (!encodedIdKeyPair) throw new Error('No encoded ID key pair found');
const idKeyPair = await BlahKeyPair.fromEncoded(encodedIdKeyPair, oldPassword);
const newEncodedIdKeyPair = await idKeyPair.encode(newPassword);
await this.keyDB.updateEncodedIdKeyPair(idKeyId, newEncodedIdKeyPair);
}
async createAccount(profile: BlahProfile, password: string): Promise<string> { async createAccount(profile: BlahProfile, password: string): Promise<string> {
if (!this.keyDB) throw new Error('Account manager not initialized'); if (!this.keyDB) throw new Error('Account manager not initialized');

View file

@ -1,8 +1,4 @@
import { import { BlahIdentity, blahIdentityDescriptionSchema } from '@blah-im/core/identity';
BlahIdentity,
blahIdentityDescriptionSchema,
type BlahIdentityDescription
} from '@blah-im/core/identity';
export function idURLToUsername(idURL: string): string { export function idURLToUsername(idURL: string): string {
const url = new URL(idURL); const url = new URL(idURL);

View file

@ -41,7 +41,9 @@
<GroupedListSection> <GroupedListSection>
<GroupedListItem icon={Bell}>Notifications</GroupedListItem> <GroupedListItem icon={Bell}>Notifications</GroupedListItem>
<GroupedListItem icon={LockClosed}>Privacy and Security</GroupedListItem> <SettingsListItem icon={LockClosed} route="/privacy-security">
Privacy and Security
</SettingsListItem>
</GroupedListSection> </GroupedListSection>
<GroupedListSection> <GroupedListSection>

View file

@ -1,12 +1,10 @@
<script lang="ts"> <script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte'; import PageHeader from '$lib/components/PageHeader.svelte';
import accountsManager from '$lib/accounts/manager.svelte'; import { accountManager } from '$lib/accounts';
import { import {
GroupedListContainer, GroupedListContainer,
GroupedListSection, GroupedListSection,
GroupedListInputItem, GroupedListInputItem
GroupedListContent,
GroupedListItem
} from '$lib/components/GroupedList'; } from '$lib/components/GroupedList';
import type { BlahIdentity, BlahProfile } from '@blah-im/core/identity'; import type { BlahIdentity, BlahProfile } from '@blah-im/core/identity';
import ProfilePicture from '$lib/components/ProfilePicture.svelte'; import ProfilePicture from '$lib/components/ProfilePicture.svelte';
@ -17,7 +15,7 @@
import type { Node } from 'prosemirror-model'; import type { Node } from 'prosemirror-model';
import UsernameItem from './UsernameItem.svelte'; import UsernameItem from './UsernameItem.svelte';
const currentAccount = $derived(accountsManager.currentAccount); const currentAccount = $derived(accountManager.currentAccount);
let identity: BlahIdentity | null = $state(null); let identity: BlahIdentity | null = $state(null);
let profile: BlahProfile | null = $state(null); let profile: BlahProfile | null = $state(null);
let initialBio: Node | null = $state(null); let initialBio: Node | null = $state(null);
@ -29,7 +27,7 @@
const snapshot = $state.snapshot(currentAccount.profile.signee.payload); const snapshot = $state.snapshot(currentAccount.profile.signee.payload);
profile = snapshot; profile = snapshot;
initialBio = blahRichTextToProseMirrorDoc([snapshot.bio ?? ''], messageSchema); initialBio = blahRichTextToProseMirrorDoc([snapshot.bio ?? ''], messageSchema);
accountsManager.identityForAccount(currentAccount).then((x) => { accountManager.identityForAccount(currentAccount).then((x) => {
identity = x; identity = x;
}); });
} }
@ -39,9 +37,9 @@
if (!currentAccount || !profile) return; if (!currentAccount || !profile) return;
isBusy = true; isBusy = true;
const identity = await accountsManager.identityForAccount(currentAccount); const identity = await accountManager.identityForAccount(currentAccount);
await identity.updateProfile(profile); await identity.updateProfile(profile);
await accountsManager.saveIdentity(identity); await accountManager.saveIdentity(identity);
isBusy = false; isBusy = false;
} }
</script> </script>
@ -67,7 +65,7 @@
<GroupedListSection header="Bio"> <GroupedListSection header="Bio">
<RichTextInput <RichTextInput
class="text-ss-primary p-4 shadow-none ring-0" class="text-ss-primary px-4 py-3 shadow-none ring-0"
schema={messageSchema} schema={messageSchema}
onDocChange={(doc) => profile && (profile.bio = doc.textContent)} onDocChange={(doc) => profile && (profile.bio = doc.textContent)}
placeholder="a 25 yo. artist from Paris." placeholder="a 25 yo. artist from Paris."

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { GroupedListContainer } from '$lib/components/GroupedList';
import GroupedListItem from '$lib/components/GroupedList/GroupedListItem.svelte';
import GroupedListSection from '$lib/components/GroupedList/GroupedListSection.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import { DocumentDuplicate, Key } from 'svelte-hero-icons';
import ChangePasswordDialog from './ChangePasswordDialog.svelte';
let showChangePasswordDialog = $state(false);
</script>
<PageHeader>
<h3 class="flex-1">Privacy & Security</h3>
</PageHeader>
<GroupedListContainer>
<GroupedListSection>
<GroupedListItem icon={Key} onclick={() => (showChangePasswordDialog = true)}>
Change Password
</GroupedListItem>
</GroupedListSection>
<GroupedListSection>
<GroupedListItem icon={DocumentDuplicate}>Backup Account</GroupedListItem>
</GroupedListSection>
</GroupedListContainer>
<ChangePasswordDialog bind:open={showChangePasswordDialog} />

View file

@ -0,0 +1,45 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import Dialog from '$lib/components/Dialog.svelte';
import GroupedListContainer from '$lib/components/GroupedList/GroupedListContainer.svelte';
import GroupedListInputItem from '$lib/components/GroupedList/GroupedListInputItem.svelte';
import GroupedListSection from '$lib/components/GroupedList/GroupedListSection.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
interface Props {
open: boolean;
}
let { open = $bindable() }: Props = $props();
let oldPassword = $state('');
</script>
<Dialog bind:open class="h-1/2">
<PageHeader>
<h3 class="grow">Change Password</h3>
<Button variant="primary">Next</Button>
</PageHeader>
<GroupedListContainer>
<div class="my-10 space-y-2 px-8 text-center">
<p>
On this device, anyone who knows this password have <em>full access</em> to your account.
</p>
<p>To change your password, enter your current password first.</p>
</div>
<GroupedListSection>
<GroupedListInputItem>
Current Password
<input type="password" bind:value={oldPassword} placeholder="Current Password" />
</GroupedListInputItem>
{#snippet footer()}
<p>
Note: password is per device. If you granted other devices <em>full access</em>, you need
to change password on these devices too.
</p>
{/snippet}
</GroupedListSection>
</GroupedListContainer>
</Dialog>