Merge pull request #3079 from appwrite/feat-password-strength-policy-ui

feat: add password strength policy UI
This commit is contained in:
Harsh Mahajan
2026-06-05 16:52:11 +05:30
committed by GitHub
7 changed files with 216 additions and 40 deletions
+2 -2
View File
@@ -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=="],
+1 -1
View File
@@ -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",
+1
View File
@@ -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',
@@ -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 @@
<UpdateUsersLimit project={data.project} policy={data.userLimitPolicy} />
<UpdateSessionLength project={data.project} policy={data.sessionDurationPolicy} />
<UpdateSessionsLimit project={data.project} policy={data.sessionLimitPolicy} />
<PasswordStrengthPolicy project={data.project} policy={data.passwordStrengthPolicy} />
<PasswordPolicies
project={data.project}
dictionaryPolicy={data.passwordDictionaryPolicy}
@@ -22,6 +22,15 @@ const getDefaultEnabledPolicy = (policyId: EmailPolicyId): EnabledPolicy => ({
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,
@@ -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);
const getInitialHistoryLimit = () => (historyPolicy.total > 0 ? historyPolicy.total : 5);
const getInitialHistoryEnabled = () => historyPolicy.total > 0;
const getInitialDictionary = () => dictionaryPolicy.enabled;
const getInitialPersonalDataCheck = () => personalDataPolicy.enabled;
onMount(() => {
// update initial states here in onMount.
const historyValue = historyPolicy.total;
if (historyValue && historyValue > 0) {
passwordHistory = historyValue;
lastValidLimit = historyValue;
}
let savedHistoryLimit = $state(getInitialHistoryLimit());
let savedHistoryEnabled = $state(getInitialHistoryEnabled());
let savedDictionary = $state(getInitialDictionary());
let savedPersonalDataCheck = $state(getInitialPersonalDataCheck());
passwordHistoryEnabled = (historyValue ?? 0) !== 0;
passwordDictionary = dictionaryPolicy.enabled;
authPersonalDataCheck = personalDataPolicy.enabled;
});
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
});
if (
passwordHistoryEnabled !== savedHistoryEnabled ||
(passwordHistoryEnabled && Number(passwordHistoryLimit) !== savedHistoryLimit)
) {
currentSubmit = Submit.AuthPasswordHistoryUpdate;
await projectSdk.updatePasswordHistoryPolicy({
total: passwordHistoryEnabled ? passwordHistoryLimit : null
});
hasAppliedServerChange = true;
trackEvent(Submit.AuthPasswordHistoryUpdate);
}
await projectSdk.updatePasswordDictionaryPolicy({
enabled: passwordDictionary
});
if (passwordDictionary !== savedDictionary) {
currentSubmit = Submit.AuthPasswordDictionaryUpdate;
await projectSdk.updatePasswordDictionaryPolicy({
enabled: passwordDictionary
});
hasAppliedServerChange = true;
trackEvent(Submit.AuthPasswordDictionaryUpdate);
}
await projectSdk.updatePasswordPersonalDataPolicy({
enabled: authPersonalDataCheck
});
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);
}
}
</script>
@@ -114,7 +141,7 @@
autofocus
label="Limit"
id="password-history"
bind:value={passwordHistory}
bind:value={passwordHistoryLimit}
helper="Maximum 20 passwords." />
{/if}
</Layout.Stack>
@@ -0,0 +1,134 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputCheckbox, InputNumber } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { Icon, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import type { Models } from '@appwrite.io/console';
let {
project,
policy
}: {
project: Models.Project;
policy: Models.PolicyPasswordStrength;
} = $props();
const getInitial = () => policy;
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 ||
passwordUppercase !== policy.uppercase ||
passwordLowercase !== policy.lowercase ||
passwordNumber !== policy.number ||
passwordSymbols !== policy.symbols
);
async function updatePasswordStrengthPolicy() {
try {
await sdk.forProject(project.region, project.$id).project.updatePasswordStrengthPolicy({
min: passwordMinLength,
uppercase: passwordUppercase,
lowercase: passwordLowercase,
number: passwordNumber,
symbols: passwordSymbols
});
await invalidate(Dependencies.PROJECT);
addNotification({
type: 'success',
message: 'Updated password strength policy.'
});
trackEvent(Submit.AuthPasswordStrengthUpdate);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.AuthPasswordStrengthUpdate);
}
}
</script>
<Form onSubmit={updatePasswordStrengthPolicy}>
<CardGrid gap="xxl">
<svelte:fragment slot="title">Password strength</svelte:fragment>
Set the minimum requirements users must meet when creating or changing a password.
<svelte:fragment slot="aside">
<Layout.Stack gap="m">
<div class="password-strength-length">
<InputNumber
required
max={256}
min={8}
label="Minimum length"
id="password-strength-min"
bind:value={passwordMinLength}>
<Tooltip slot="info">
<Icon icon={IconInfo} size="s" />
<span slot="tooltip">
Passwords must be between 8 and 256 characters.
</span>
</Tooltip>
</InputNumber>
</div>
<Layout.Stack gap="s">
<Typography.Text size="m" weight="medium">
Character requirements
</Typography.Text>
<div class="password-strength-requirements">
<InputCheckbox
bind:checked={passwordUppercase}
id="password-strength-uppercase"
label="Uppercase letter" />
<InputCheckbox
bind:checked={passwordLowercase}
id="password-strength-lowercase"
label="Lowercase letter" />
<InputCheckbox
bind:checked={passwordNumber}
id="password-strength-number"
label="Number" />
<InputCheckbox
bind:checked={passwordSymbols}
id="password-strength-symbols"
label="Special character" />
</div>
</Layout.Stack>
</Layout.Stack>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!hasChanges} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<style lang="scss">
.password-strength-length {
max-width: 18rem;
}
.password-strength-requirements {
display: grid;
gap: var(--space-3) var(--space-7);
grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 769px) {
.password-strength-requirements {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>