From 2d517c74e15a63d1520eb8289d22727a4ef65922 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Fri, 5 Jun 2026 16:14:15 +0530 Subject: [PATCH 1/3] feat: add password strength policy UI --- bun.lock | 4 +- package.json | 2 +- src/lib/actions/analytics.ts | 1 + .../auth/security/+page.svelte | 2 + .../auth/security/+page.ts | 12 ++ .../auth/security/passwordPolicies.svelte | 107 +++++++----- .../security/passwordStrengthPolicy.svelte | 152 ++++++++++++++++++ 7 files changed, 237 insertions(+), 43 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte diff --git a/bun.lock b/bun.lock index 0693ff74cd..283e22f525 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@82069e6", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@5d0672f", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", @@ -125,7 +125,7 @@ "@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="], - "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@82069e6", { "dependencies": { "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@5d0672f", { "dependencies": { "json-bigint": "1.0.0" } }], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], diff --git a/package.json b/package.json index 0d5b784155..7e46c5ff27 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@82069e6", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@5d0672f", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 63d5cf42d4..04286611ee 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -272,6 +272,7 @@ export enum Submit { AuthStatusUpdate = 'submit_auth_status_update', AuthPasswordHistoryUpdate = 'submit_auth_password_history_limit_update', AuthPasswordDictionaryUpdate = 'submit_auth_password_dictionary_update', + AuthPasswordStrengthUpdate = 'submit_auth_password_strength_update', AuthPersonalDataCheckUpdate = 'submit_auth_personal_data_check_update', AuthAliasedEmailsUpdate = 'submit_auth_aliased_emails_update', AuthDisposableEmailsUpdate = 'submit_auth_disposable_emails_update', diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/+page.svelte index c1ac1c210e..3bc8b22f79 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/+page.svelte @@ -7,6 +7,7 @@ import UpdateSessionLength from './updateSessionLength.svelte'; import UpdateSessionsLimit from './updateSessionsLimit.svelte'; import PasswordPolicies from './passwordPolicies.svelte'; + import PasswordStrengthPolicy from './passwordStrengthPolicy.svelte'; import SessionSecurity from './sessionSecurity.svelte'; import UpdateSignupEmailSecurity from './updateSignupEmailSecurity.svelte'; @@ -17,6 +18,7 @@ + ({ enabled: false }); +const getDefaultPasswordStrengthPolicy = (): Models.PolicyPasswordStrength => ({ + $id: ProjectPolicyId.Passwordstrength, + min: 8, + uppercase: false, + lowercase: false, + number: false, + symbols: false +}); + export const load: PageLoad = async ({ depends, params }) => { depends(Dependencies.PROJECT); @@ -40,6 +49,9 @@ export const load: PageLoad = async ({ depends, params }) => { passwordHistoryPolicy: policiesById[ ProjectPolicyId.Passwordhistory ] as Models.PolicyPasswordHistory, + passwordStrengthPolicy: + (policiesById[ProjectPolicyId.Passwordstrength] as Models.PolicyPasswordStrength) ?? + getDefaultPasswordStrengthPolicy(), passwordPersonalDataPolicy: policiesById[ ProjectPolicyId.Passwordpersonaldata ] as Models.PolicyPasswordPersonalData, diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte index caa766d3ca..b59d65469e 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte @@ -8,7 +8,6 @@ import { sdk } from '$lib/stores/sdk'; import { Typography, Link, Layout } from '@appwrite.io/pink-svelte'; import type { Models } from '@appwrite.io/console'; - import { onMount } from 'svelte'; let { project, @@ -22,72 +21,100 @@ personalDataPolicy: Models.PolicyPasswordPersonalData; } = $props(); - let lastValidLimit = $state(5); - let passwordHistory = $state(5); - let passwordDictionary = $state(false); - let passwordHistoryEnabled = $state(false); - let authPersonalDataCheck = $state(false); - - onMount(() => { - // update initial states here in onMount. - const historyValue = historyPolicy.total; - if (historyValue && historyValue > 0) { - passwordHistory = historyValue; - lastValidLimit = historyValue; - } + const getInitialHistoryLimit = () => (historyPolicy.total > 0 ? historyPolicy.total : 5); + const getInitialHistoryEnabled = () => historyPolicy.total > 0; + const getInitialDictionary = () => dictionaryPolicy.enabled; + const getInitialPersonalDataCheck = () => personalDataPolicy.enabled; - passwordHistoryEnabled = (historyValue ?? 0) !== 0; - passwordDictionary = dictionaryPolicy.enabled; - authPersonalDataCheck = personalDataPolicy.enabled; - }); + let savedHistoryLimit = $state(getInitialHistoryLimit()); + let savedHistoryEnabled = $state(getInitialHistoryEnabled()); + let savedDictionary = $state(getInitialDictionary()); + let savedPersonalDataCheck = $state(getInitialPersonalDataCheck()); + + let lastValidHistoryLimit = $state(getInitialHistoryLimit()); + let passwordHistoryLimit = $state(getInitialHistoryLimit()); + let passwordDictionary = $state(getInitialDictionary()); + let passwordHistoryEnabled = $state(getInitialHistoryEnabled()); + let authPersonalDataCheck = $state(getInitialPersonalDataCheck()); $effect(() => { // restore last valid limit when enabling - if (passwordHistoryEnabled && passwordHistory < 1) { - passwordHistory = lastValidLimit; + if (passwordHistoryEnabled && passwordHistoryLimit < 1) { + passwordHistoryLimit = lastValidHistoryLimit; + } + + if (passwordHistoryLimit > 0) { + lastValidHistoryLimit = passwordHistoryLimit; } }); const hasChanges = $derived.by(() => { - const dictChanged = passwordDictionary !== dictionaryPolicy.enabled; - const dataCheckChanged = authPersonalDataCheck !== personalDataPolicy.enabled; - const historyChanged = passwordHistoryEnabled !== (historyPolicy.total !== 0); + const dictChanged = passwordDictionary !== savedDictionary; + const dataCheckChanged = authPersonalDataCheck !== savedPersonalDataCheck; + const historyChanged = passwordHistoryEnabled !== savedHistoryEnabled; const limitChanged = - passwordHistoryEnabled && Number(passwordHistory) !== historyPolicy.total; + passwordHistoryEnabled && Number(passwordHistoryLimit) !== savedHistoryLimit; return historyChanged || dictChanged || dataCheckChanged || limitChanged; }); async function updatePasswordPolicies() { + let currentSubmit = Submit.AuthPasswordHistoryUpdate; + let hasAppliedServerChange = false; + try { const projectSdk = sdk.forProject(project.region, project.$id).project; - await projectSdk.updatePasswordHistoryPolicy({ - total: passwordHistoryEnabled ? passwordHistory : null - }); - - await projectSdk.updatePasswordDictionaryPolicy({ - enabled: passwordDictionary - }); - - await projectSdk.updatePasswordPersonalDataPolicy({ - enabled: authPersonalDataCheck - }); + if ( + passwordHistoryEnabled !== savedHistoryEnabled || + (passwordHistoryEnabled && Number(passwordHistoryLimit) !== savedHistoryLimit) + ) { + currentSubmit = Submit.AuthPasswordHistoryUpdate; + await projectSdk.updatePasswordHistoryPolicy({ + total: passwordHistoryEnabled ? passwordHistoryLimit : null + }); + hasAppliedServerChange = true; + trackEvent(Submit.AuthPasswordHistoryUpdate); + } + + if (passwordDictionary !== savedDictionary) { + currentSubmit = Submit.AuthPasswordDictionaryUpdate; + await projectSdk.updatePasswordDictionaryPolicy({ + enabled: passwordDictionary + }); + hasAppliedServerChange = true; + trackEvent(Submit.AuthPasswordDictionaryUpdate); + } + + if (authPersonalDataCheck !== savedPersonalDataCheck) { + currentSubmit = Submit.AuthPersonalDataCheckUpdate; + await projectSdk.updatePasswordPersonalDataPolicy({ + enabled: authPersonalDataCheck + }); + hasAppliedServerChange = true; + trackEvent(Submit.AuthPersonalDataCheckUpdate); + } + + savedHistoryLimit = passwordHistoryLimit; + savedHistoryEnabled = passwordHistoryEnabled; + savedDictionary = passwordDictionary; + savedPersonalDataCheck = authPersonalDataCheck; await invalidate(Dependencies.PROJECT); addNotification({ type: 'success', message: 'Updated password policies.' }); - trackEvent(Submit.AuthPasswordHistoryUpdate); - trackEvent(Submit.AuthPasswordDictionaryUpdate); - trackEvent(Submit.AuthPersonalDataCheckUpdate); } catch (error) { + if (hasAppliedServerChange) { + await invalidate(Dependencies.PROJECT); + } + addNotification({ type: 'error', message: error.message }); - trackError(error, Submit.AuthPasswordHistoryUpdate); + trackError(error, currentSubmit); } } @@ -114,7 +141,7 @@ autofocus label="Limit" id="password-history" - bind:value={passwordHistory} + bind:value={passwordHistoryLimit} helper="Maximum 20 passwords." /> {/if} diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte new file mode 100644 index 0000000000..52e069bd9e --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte @@ -0,0 +1,152 @@ + + +
+ + Password strength + Set the minimum requirements users must meet when creating or changing a password. + + + +
+ + + + + Passwords must be between 8 and 256 characters. + + + +
+ + + Character requirements + +
+ + + + +
+
+
+
+ + + + +
+
+ + From 1567f30e32f1547da6f004007bf1f76a649dc17b Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Fri, 5 Jun 2026 16:30:53 +0530 Subject: [PATCH 2/3] address comments --- .../security/passwordStrengthPolicy.svelte | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte index 52e069bd9e..d4773ac8f2 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte @@ -24,27 +24,19 @@ const getInitialNumber = () => policy.number; const getInitialSymbols = () => policy.symbols; - let savedMinLength = $state(getInitialMinLength()); - let savedUppercase = $state(getInitialUppercase()); - let savedLowercase = $state(getInitialLowercase()); - let savedNumber = $state(getInitialNumber()); - let savedSymbols = $state(getInitialSymbols()); - let passwordMinLength = $state(getInitialMinLength()); let passwordUppercase = $state(getInitialUppercase()); let passwordLowercase = $state(getInitialLowercase()); let passwordNumber = $state(getInitialNumber()); let passwordSymbols = $state(getInitialSymbols()); - const hasChanges = $derived.by(() => { - return ( - passwordMinLength !== savedMinLength || - passwordUppercase !== savedUppercase || - passwordLowercase !== savedLowercase || - passwordNumber !== savedNumber || - passwordSymbols !== savedSymbols - ); - }); + const hasChanges = $derived( + Number(passwordMinLength) !== policy.min || + passwordUppercase !== policy.uppercase || + passwordLowercase !== policy.lowercase || + passwordNumber !== policy.number || + passwordSymbols !== policy.symbols + ); async function updatePasswordStrengthPolicy() { try { @@ -56,12 +48,6 @@ symbols: passwordSymbols }); - savedMinLength = passwordMinLength; - savedUppercase = passwordUppercase; - savedLowercase = passwordLowercase; - savedNumber = passwordNumber; - savedSymbols = passwordSymbols; - await invalidate(Dependencies.PROJECT); addNotification({ type: 'success', From 6c69a1882f47983c8651e1d39daeb93bebb3e4cf Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Fri, 5 Jun 2026 16:34:12 +0530 Subject: [PATCH 3/3] all five inlined --- .../auth/security/passwordStrengthPolicy.svelte | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte index d4773ac8f2..caaf7085bb 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte @@ -18,17 +18,13 @@ policy: Models.PolicyPasswordStrength; } = $props(); - const getInitialMinLength = () => policy.min; - const getInitialUppercase = () => policy.uppercase; - const getInitialLowercase = () => policy.lowercase; - const getInitialNumber = () => policy.number; - const getInitialSymbols = () => policy.symbols; + const getInitial = () => policy; - let passwordMinLength = $state(getInitialMinLength()); - let passwordUppercase = $state(getInitialUppercase()); - let passwordLowercase = $state(getInitialLowercase()); - let passwordNumber = $state(getInitialNumber()); - let passwordSymbols = $state(getInitialSymbols()); + let passwordMinLength = $state(getInitial().min); + let passwordUppercase = $state(getInitial().uppercase); + let passwordLowercase = $state(getInitial().lowercase); + let passwordNumber = $state(getInitial().number); + let passwordSymbols = $state(getInitial().symbols); const hasChanges = $derived( Number(passwordMinLength) !== policy.min ||