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 };
}
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> {
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();
}
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> {
if (!this.keyDB) throw new Error('Account manager not initialized');

View file

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

View file

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

View file

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