Skip to content

Commit

Permalink
confirm password initial draft
Browse files Browse the repository at this point in the history
  • Loading branch information
wlee221 committed Nov 7, 2022
1 parent ea6d226 commit 6e2a79d
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import React from 'react';

import { Logger } from 'aws-amplify';
import { changePassword, translate } from '@aws-amplify/ui';
import {
changePassword,
confirmPasswordMatch,
FieldValidator,
getDefaultPasswordValidator,
translate,
} from '@aws-amplify/ui';

import { useAuth } from '../../../internal';
import { View, Flex } from '../../../primitives';
Expand All @@ -19,12 +25,77 @@ const logger = new Logger('ChangePassword');
function ChangePassword({
onSuccess,
onError,
validate,
}: ChangePasswordProps): JSX.Element | null {
const [errorMessage, setErrorMessage] = React.useState<string>(null);
const [formValues, setFormValues] = React.useState<FormValues>({});
const [blurredFields, setBlurredFields] = React.useState<
Record<string, boolean>
>({});
const [_validationError, setValidationError] = React.useState<
Record<string, string[]>
>({});

const { user, isLoading } = useAuth();

/** Validator */
const defaultValidators = React.useMemo(
() => getDefaultPasswordValidator(),
[]
);

const passwordValidators: FieldValidator[] = React.useMemo(
() => validate ?? defaultValidators,
[validate, defaultValidators]
);

const validateNewPassword = (newPassword: string): string[] => {
// return if field isn't filled out or hasn't been blurred yet
if (!newPassword || !blurredFields[newPassword]) return;

const errors: string[] = [];
passwordValidators.forEach((validator) => {
const error = validator(newPassword);
if (error) {
errors.push(error);
}
});
return errors;
};

const validateConfirmPassword = (
newPassword: string,
confirmPassword: string
): string[] => {
// return if newPassword isn't filled out or hasn't been blurred yet
if (!newPassword || !blurredFields[newPassword]) return;
// return if confirmPassword isn't filled out or hasn't been blurred yet
if (!confirmPassword || !blurredFields[confirmPassword]) return;

const error = confirmPasswordMatch(newPassword, confirmPassword);
return error ? [error] : null;
};

const validateFields = (formValues: FormValues): void => {
const { newPassword, confirmPassword } = formValues;

// new password validation
const newPasswordErrors = validateNewPassword(newPassword);

// confirm password validation
const confirmPasswordErrors = validateConfirmPassword(
newPassword,
confirmPassword
);

if (newPasswordErrors?.length >= 0 && confirmPasswordErrors?.length >= 0) {
setValidationError({
newPassword: newPasswordErrors,
confirmPassword: confirmPasswordErrors,
});
}
};

/** Return null if Auth.getCurrentAuthenticatedUser is still in progress */
if (isLoading) {
return null;
Expand All @@ -47,7 +118,17 @@ function ChangePassword({
event.preventDefault();

const { name, value } = event.target;
setFormValues({ ...formValues, [name]: value });

const newFormValues = { ...formValues, [name]: value };
validateFields(newFormValues);
setFormValues(newFormValues);
};

const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
event.preventDefault();

const { name } = event.target;
setBlurredFields({ ...blurredFields, [name]: true });
};

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
Expand Down Expand Up @@ -76,13 +157,15 @@ function ChangePassword({
isRequired
label={currentPasswordLabel}
name="currentPassword"
onBlur={handleBlur}
onChange={handleChange}
/>
<DefaultNewPassword
autoComplete="new-password"
isRequired
label={newPasswordLabel}
name="newPassword"
onBlur={handleBlur}
onChange={handleChange}
/>
<DefaultSubmitButton type="submit">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { FieldValidator } from '@aws-amplify/ui';

export interface ChangePasswordProps {
// callback once password is successfully updated
onSuccess?: () => void;
// callback when there's an error
onError?: (error: Error) => void;
// custom password validation
validate?: FieldValidator[];
}
3 changes: 2 additions & 1 deletion packages/ui/src/helpers/accountSettings/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { changePassword } from './utils';
export * from './utils';
export * from './validator';
12 changes: 11 additions & 1 deletion packages/ui/src/helpers/accountSettings/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AmplifyUser } from '../../types';
import { Auth } from 'aws-amplify';
import { Amplify, Auth } from 'aws-amplify';

import { getLogger } from '../utils';

Expand Down Expand Up @@ -30,3 +30,13 @@ export const changePassword = async ({
return Promise.reject(e);
}
};

export const getAmplifyConfig = () => {
return Amplify.configure();
};

export const getPasswordSettings = () => {
// need to cast to any because `Amplify.configure()` isn't typed properly
const config = getAmplifyConfig() as any;
return config?.aws_cognito_password_protection_settings;
};
84 changes: 84 additions & 0 deletions packages/ui/src/helpers/accountSettings/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
ConfirmPasswordValidator,
FieldValidator,
MinLengthValidator,
} from '../../types/accountSettings/validator';
import { hasAllowedSpecialChars } from '../authenticator';
import { getPasswordSettings } from './utils';

// TODO: consolidate these with Authenticator validators
export const minLength: MinLengthValidator = (minLength: number) => {
return (field: string) => {
if (field?.length < minLength) {
return `Password must have at least ${minLength} characters`;
}
};
};

export const hasLowerCase: FieldValidator = (field: string) => {
if (!/[a-z]/.test(field)) {
return 'Password must have lower case letters';
}
};

export const hasUpperCase: FieldValidator = (field: string) => {
if (!/[a-z]/.test(field)) {
return 'Password must have lower case letters';
}
};

export const hasNumbers: FieldValidator = (field: string) => {
if (!/[0-9]/.test(field)) {
return 'Password must have numbers';
}
};

export const hasSpecialChars = (field: string) => {
if (!hasAllowedSpecialChars(field)) {
return 'Password must have special characters';
}
};

export const confirmPasswordMatch: ConfirmPasswordValidator = (
newPassword,
confirmPassword
) => {
if (newPassword !== confirmPassword) {
return 'Your passwords must match';
}
};

export const getDefaultPasswordValidator = (): FieldValidator[] => {
const validators: FieldValidator[] = [];
const passwordSettings = getPasswordSettings();

const minLengthPoilicy = passwordSettings?.passwordPolicyMinLength as number;
if (minLengthPoilicy) {
validators.push(minLength(minLengthPoilicy));
}

const passwordPolicy = passwordSettings?.passwordPolicyCharacters as string[];
if (passwordPolicy) {
passwordPolicy.forEach((policyName) => {
switch (policyName) {
case 'REQUIRES_LOWERCASE': {
validators.push(hasLowerCase);
}
case 'REQUIRES_UPPERCASE': {
validators.push(hasUpperCase);
}
case 'REQUIRES_NUMBERS': {
validators.push(hasNumbers);
}
case 'REQUIRES_SYMBOLS': {
validators.push(hasSpecialChars);
break;
}
default: {
break;
}
}
});
return validators;
}
};
2 changes: 1 addition & 1 deletion packages/ui/src/helpers/authenticator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const listenToAuthHub = (
);
};

export const hasSpecialChars = (password: string) =>
export const hasAllowedSpecialChars = (password: string) =>
ALLOWED_SPECIAL_CHARACTERS.some((char) => password.includes(char));

export const getTotpCodeURL = (
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/machines/authenticator/defaultServices.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Amplify, Auth } from 'aws-amplify';
import { hasSpecialChars } from '../../helpers';
import { hasAllowedSpecialChars } from '../../helpers';

import {
AuthChallengeName,
Expand Down Expand Up @@ -113,7 +113,7 @@ export const defaultServices = {
break;
case 'REQUIRES_SYMBOLS':
// https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-policies.html
if (!hasSpecialChars(password))
if (!hasAllowedSpecialChars(password))
password_complexity.push('Password must have special characters');
break;
default:
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/types/accountSettings/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './validator';
8 changes: 8 additions & 0 deletions packages/ui/src/types/accountSettings/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type FieldValidator = (password: string) => string;

export type MinLengthValidator = (minLength: number) => FieldValidator;

export type ConfirmPasswordValidator = (
newPassword: string,
confirmPassword: string
) => string;
1 change: 1 addition & 0 deletions packages/ui/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './authenticator';
export * from './accountSettings';
export * from './primitives';
export * from './util';

0 comments on commit 6e2a79d

Please sign in to comment.