mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #3079 from appwrite/feat-password-strength-policy-ui
feat: add password strength policy UI
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
+64
-37
@@ -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>
|
||||
|
||||
+134
@@ -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>
|
||||
Reference in New Issue
Block a user