From e1d025470b080604e30a488a37ee5d8e4dd51604 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Thu, 8 May 2025 23:53:59 +0800 Subject: [PATCH] feat: Implement password change feature with multistep flow --- src/lib/components/Button.svelte | 5 +- .../ChangePasswordDialog.svelte | 200 +++++++++++++++--- 2 files changed, 180 insertions(+), 25 deletions(-) diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte index 9b9658f..dc02f82 100644 --- a/src/lib/components/Button.svelte +++ b/src/lib/components/Button.svelte @@ -6,7 +6,10 @@ variant?: 'primary' | 'secondary'; class?: string; children?: import('svelte').Snippet; - } & Omit; + } & Omit< + ({ href: string } & HTMLAnchorAttributes) | ({ href?: undefined } & HTMLButtonAttributes), + 'class' + >; let { variant = 'secondary', class: externalClass, children, ...rest }: Props = $props(); diff --git a/src/routes/(app)/settings/privacy-security/ChangePasswordDialog.svelte b/src/routes/(app)/settings/privacy-security/ChangePasswordDialog.svelte index bc72336..ae9bf93 100644 --- a/src/routes/(app)/settings/privacy-security/ChangePasswordDialog.svelte +++ b/src/routes/(app)/settings/privacy-security/ChangePasswordDialog.svelte @@ -5,6 +5,7 @@ import GroupedListInputItem from '$lib/components/GroupedList/GroupedListInputItem.svelte'; import GroupedListSection from '$lib/components/GroupedList/GroupedListSection.svelte'; import PageHeader from '$lib/components/PageHeader.svelte'; + import { accountManager } from '$lib/accounts'; interface Props { open: boolean; @@ -12,34 +13,185 @@ let { open = $bindable() }: Props = $props(); + let step: 0 | 1 | 2 = $state(0); + let error: 'old-incorrect' | 'new-empty' | 'new-mismatch' | null = $state(null); let oldPassword = $state(''); + let newPassword = $state(''); + let repeatPassword = $state(''); + + function reset() { + oldPassword = ''; + newPassword = ''; + repeatPassword = ''; + step = 0; + error = null; + } + + $effect.pre(() => { + if (!open) { + // Reset all fields and state when dialog is closed + reset(); + } + }); + + async function nextStep(e: Event) { + e.preventDefault(); + error = null; + + switch (step) { + case 0: + if (oldPassword.length === 0) { + error = 'old-incorrect'; + return; + } + + try { + if (!accountManager.currentAccountId) { + throw new Error('No current account selected'); + } + + await accountManager.identityForAccount(accountManager.currentAccountId, oldPassword); + + step = 1; + } catch (err) { + console.error('Password verification failed:', err); + error = 'old-incorrect'; + } + break; + case 1: + if (newPassword.length === 0) { + error = 'new-empty'; + return; + } + + if (newPassword !== repeatPassword) { + error = 'new-mismatch'; + return; + } + + try { + if (!accountManager.currentAccountId) { + throw new Error('No current account selected'); + } + + await accountManager.changePassword( + accountManager.currentAccountId, + oldPassword, + newPassword + ); + + step = 2; + } catch (err) { + console.error('Password change failed:', err); + error = 'old-incorrect'; + } + break; + case 2: + open = false; + } + } + + const nextButtonText = $derived.by(() => { + switch (step) { + case 0: + return 'Next'; + case 1: + return 'Change'; + case 2: + return 'Done'; + } + }); +{#snippet errorHint()} + {#if error} +

+ {#if error === 'old-incorrect'} + Incorrect password + {:else if error === 'new-empty'} + Password cannot be empty + {:else if error === 'new-mismatch'} + Passwords do not match + {/if} +

+ {/if} +{/snippet} + - -

Change Password

- -
+
+ +

Change Password

+ +
- -
-

- On this device, anyone who knows this password have full access to your account. -

-

To change your password, enter your current password first.

-
+ + {#if step === 0} +
+

+ On this device, anyone who knows this password have full access to your account. +

+

To change your password, enter your current password first.

+
- - - Current Password - - - {#snippet footer()} -

- Note: password is per device. If you granted other devices full access, you need - to change password on these devices too. -

- {/snippet} -
-
+ + + Current Password + + + {#snippet footer()} + {@render errorHint()} +

+ Note: password is per device. If you granted other devices full access, you + need to change password on these devices too. +

+ {/snippet} +
+ {:else if step === 1} +
+

Now enter your new password

+

+ Make sure it's unique and secure, and remember it. You'll lose access to your + account if you forget it. +

+
+ + + + New Password + + + + Repeat + + + + {#snippet footer()} + {@render errorHint()} + {/snippet} + + {:else if step === 2} +
+

Password changed successfully!

+

+ Remember to change it on all other devices to which you granted full access. +

+
+ {/if} +
+