Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/lib/actions/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
}
}
</script>
Expand All @@ -114,7 +141,7 @@
autofocus
label="Limit"
id="password-history"
bind:value={passwordHistory}
bind:value={passwordHistoryLimit}
helper="Maximum 20 passwords." />
{/if}
</Layout.Stack>
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Loading