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..caaf7085bb --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/auth/security/passwordStrengthPolicy.svelte @@ -0,0 +1,134 @@ + + + + + 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 + + + + + + + + + + + + + Update + + + + +