Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup password related components #4474

Merged
merged 5 commits into from
Jun 19, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React from 'react';
import autoBind from 'react-autobind';
import DocumentTitle from 'react-document-title';
import { observer } from 'mobx-react';
import sessionStore from "js/stores/session";
import {observer} from 'mobx-react';
import sessionStore from 'js/stores/session';
import {actions} from '../actions';
import bem, {makeBem} from 'js/bem';
import TextBox from 'js/components/common/textBox';
import PasswordStrength from 'js/components/passwordStrength';
import {stringToColor} from 'utils';
import PasswordStrength from 'js/components/passwordStrength.component';
import {stringToColor} from 'js/utils';
import {ROOT_URL} from 'js/constants';
import {withRouter} from 'js/router/legacy';
import type {WithRouterProps} from 'jsapp/js/router/legacy';
import './accountSettings.scss';

bem.AccountSettings = makeBem(null, 'account-settings');
Expand All @@ -18,44 +18,70 @@ bem.AccountSettings__right = makeBem(bem.AccountSettings, 'right');
bem.AccountSettings__item = makeBem(bem.FormModal, 'item');
bem.AccountSettings__actions = makeBem(bem.AccountSettings, 'actions');

const ChangePassword = class ChangePassword extends React.Component {
constructor(props) {
interface PasswordErrors {
currentPassword?: string;
newPassword?: string;
verifyPassword?: string;
}

interface ChangePasswordRouteState {
errors: PasswordErrors;
currentPassword: string;
newPassword: string;
verifyPassword: string;
}

const FIELD_REQUIRED_ERROR = t('This field is required.');

const ChangePasswordRoute = class ChangePassword extends React.Component<
WithRouterProps,
ChangePasswordRouteState
> {
errors: PasswordErrors = {};

constructor(props: WithRouterProps) {
super(props);
this.errors = {};
this.state = {
errors: this.errors,
currentPassword: '',
newPassword: '',
verifyPassword: ''
verifyPassword: '',
};
autoBind(this);
}

validateRequired(what) {
if (!this.state[what]) {
this.errors[what] = t('This field is required.');
}
}

close() {
this.props.router.navigate(-1)
this.props.router.navigate(-1);
}

changePassword() {
savePassword() {
this.errors = {};
this.validateRequired('currentPassword');
this.validateRequired('newPassword');
this.validateRequired('verifyPassword');

if (!this.state.currentPassword) {
this.errors.currentPassword = FIELD_REQUIRED_ERROR;
}
if (!this.state.newPassword) {
this.errors.newPassword = FIELD_REQUIRED_ERROR;
}
if (!this.state.verifyPassword) {
this.errors.verifyPassword = FIELD_REQUIRED_ERROR;
}

if (this.state.newPassword !== this.state.verifyPassword) {
this.errors['newPassword'] = t('This field must match the Verify Password field.');
this.errors.newPassword = t(
'This field must match the Verify Password field.'
);
}
if (Object.keys(this.errors).length === 0) {
actions.auth.changePassword(this.state.currentPassword, this.state.newPassword);
actions.auth.changePassword(
this.state.currentPassword,
this.state.newPassword
);
}
this.setState({errors: this.errors});
}

onChangePasswordFailed(jqXHR) {
onChangePasswordFailed(jqXHR: JQuery.jqXHR) {
if (jqXHR.responseJSON.current_password) {
this.errors.currentPassword = jqXHR.responseJSON.current_password;
}
Expand All @@ -65,52 +91,43 @@ const ChangePassword = class ChangePassword extends React.Component {
this.setState({errors: this.errors});
}

onChangePasswordCompleted() {
this.close();
}

currentPasswordChange(val) {
onCurrentPasswordChange(val: string) {
this.setState({currentPassword: val});
}

newPasswordChange(val) {
onNewPasswordChange(val: string) {
this.setState({newPassword: val});
}

verifyPasswordChange(val) {
onVerifyPasswordChange(val: string) {
this.setState({verifyPassword: val});
}

render() {
if(!sessionStore.isLoggedIn) {
if (!sessionStore.isLoggedIn) {
return null;
}

var accountName = sessionStore.currentAccount.username;
var initialsStyle = {
background: `#${stringToColor(accountName)}`
};
const accountName = sessionStore.currentAccount.username;
const initialsStyle = {background: `#${stringToColor(accountName)}`};

return (
<DocumentTitle title={`${accountName} | KoboToolbox`}>
<bem.AccountSettings>
<bem.AccountSettings__actions>
<bem.KoboButton
onClick={this.changePassword}
m={['blue']}
>
<bem.KoboButton onClick={this.savePassword.bind(this)} m='blue'>
{t('Save Password')}
</bem.KoboButton>

<button
onClick={this.close}
onClick={this.close.bind(this)}
className='account-settings-close mdl-button mdl-button--icon'
>
<i className='k-icon k-icon-close'/>
<i className='k-icon k-icon-close' />
</button>
</bem.AccountSettings__actions>

<bem.AccountSettings__item m={'column'}>
<bem.AccountSettings__item m='column'>
<bem.AccountSettings__item m='username'>
<bem.AccountBox__initials style={initialsStyle}>
{accountName.charAt(0)}
Expand All @@ -130,7 +147,7 @@ const ChangePassword = class ChangePassword extends React.Component {
type='password'
errors={this.state.errors.currentPassword}
value={this.state.currentPassword}
onChange={this.currentPasswordChange}
onChange={this.onCurrentPasswordChange.bind(this)}
/>

<a
Expand All @@ -148,16 +165,15 @@ const ChangePassword = class ChangePassword extends React.Component {
type='password'
errors={this.state.errors.newPassword}
value={this.state.newPassword}
onChange={this.newPasswordChange}
onChange={this.onNewPasswordChange.bind(this)}
/>
</bem.AccountSettings__item>

{
this.state.newPassword !== '' &&
{this.state.newPassword !== '' && (
<bem.AccountSettings__item>
<PasswordStrength password={this.state.newPassword} />
</bem.AccountSettings__item>
}
)}

<bem.AccountSettings__item>
<TextBox
Expand All @@ -166,7 +182,7 @@ const ChangePassword = class ChangePassword extends React.Component {
type='password'
errors={this.state.errors.verifyPassword}
value={this.state.verifyPassword}
onChange={this.verifyPasswordChange}
onChange={this.onVerifyPasswordChange.bind(this)}
/>
</bem.AccountSettings__item>
</bem.AccountSettings__item>
Expand All @@ -175,6 +191,6 @@ const ChangePassword = class ChangePassword extends React.Component {
</DocumentTitle>
);
}
}
};

export default observer(withRouter(ChangePassword));
export default observer(withRouter(ChangePasswordRoute));
6 changes: 3 additions & 3 deletions jsapp/js/account/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {Navigate, Route} from 'react-router-dom';
import RequireAuth from 'js/router/requireAuth';
import {ROUTES} from 'js/router/routerConstants';

const ChangePassword = React.lazy(
() => import(/* webpackPrefetch: true */ './changePassword')
const ChangePasswordRoute = React.lazy(
() => import(/* webpackPrefetch: true */ './changePasswordRoute.component')
);
const SecurityRoute = React.lazy(
() => import(/* webpackPrefetch: true */ './security/securityRoute.component')
Expand Down Expand Up @@ -70,7 +70,7 @@ export default function routes() {
path={ACCOUNT_ROUTES.CHANGE_PASSWORD}
element={
<RequireAuth>
<ChangePassword />
<ChangePasswordRoute />
</RequireAuth>
}
/>
Expand Down
1 change: 1 addition & 0 deletions jsapp/js/actions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export namespace actions {
loggedin: GenericCallbackDefinition;
};
logout: GenericDefinition;
changePassword: GenericDefinition;
};
const survey: object;
const search: object;
Expand Down
7 changes: 0 additions & 7 deletions jsapp/js/bemComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,13 +286,6 @@ bem.ToggleSwitch__input = makeBem(bem.ToggleSwitch, 'input', 'input');
bem.ToggleSwitch__slider = makeBem(bem.ToggleSwitch, 'slider', 'span');
bem.ToggleSwitch__label = makeBem(bem.ToggleSwitch, 'label', 'span');

bem.PasswordStrength = makeBem(null, 'password-strength');
bem.PasswordStrength__title = makeBem(bem.PasswordStrength, 'title');
bem.PasswordStrength__bar = makeBem(bem.PasswordStrength, 'bar');
bem.PasswordStrength__indicator = makeBem(bem.PasswordStrength, 'indicator');
bem.PasswordStrength__messages = makeBem(bem.PasswordStrength, 'messages', 'ul');
bem.PasswordStrength__message = makeBem(bem.PasswordStrength, 'message', 'li');

bem.Breadcrumbs = makeBem(null, 'breadcrumbs');
bem.Breadcrumbs__crumb = makeBem(bem.Breadcrumbs, 'crumb', 'a');
bem.Breadcrumbs__divider = makeBem(bem.Breadcrumbs, 'divider', 'i');
Expand Down
115 changes: 115 additions & 0 deletions jsapp/js/components/passwordStrength.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from 'react';
import zxcvbn from 'zxcvbn';
import styles from './passwordStrength.module.scss';
import classNames from 'classnames';

interface PasswordStrengthProps {
password: string;
}

export default function PasswordStrength(props: PasswordStrengthProps) {
/**
* strings for zxcvbn 4.4.2 package
* copied from https://github.com/dropbox/zxcvbn/blob/master/src/feedback.coffee
* not the most concise approach, but hopefully the most intuitive; could
* be removed if https://github.com/dropbox/zxcvbn/pull/124 is ever merged
*/
const feedbackTranslations: {[key: string]: string} = {
'Use a few words, avoid common phrases': t(
'Use a few words, avoid common phrases'
),
'No need for symbols, digits, or uppercase letters': t(
'No need for symbols, digits, or uppercase letters'
),
'Add another word or two. Uncommon words are better.': t(
'Add another word or two. Uncommon words are better.'
),
'Straight rows of keys are easy to guess': t(
'Straight rows of keys are easy to guess'
),
'Short keyboard patterns are easy to guess': t(
'Short keyboard patterns are easy to guess'
),
'Use a longer keyboard pattern with more turns': t(
'Use a longer keyboard pattern with more turns'
),
'Repeats like "aaa" are easy to guess': t(
'Repeats like "aaa" are easy to guess'
),
'Repeats like "abcabcabc" are only slightly harder to guess than "abc"': t(
'Repeats like "abcabcabc" are only slightly harder to guess than "abc"'
),
'Avoid repeated words and characters': t(
'Avoid repeated words and characters'
),
'Sequences like abc or 6543 are easy to guess': t(
'Sequences like abc or 6543 are easy to guess'
),
'Avoid sequences': t('Avoid sequences'),
'Recent years are easy to guess': t('Recent years are easy to guess'),
'Avoid recent years': t('Avoid recent years'),
'Avoid years that are associated with you': t(
'Avoid years that are associated with you'
),
'Dates are often easy to guess': t('Dates are often easy to guess'),
'Avoid dates and years that are associated with you': t(
'Avoid dates and years that are associated with you'
),
'This is a top-10 common password': t('This is a top-10 common password'),
'This is a top-100 common password': t('This is a top-100 common password'),
'This is a very common password': t('This is a very common password'),
'This is similar to a commonly used password': t(
'This is similar to a commonly used password'
),
'A word by itself is easy to guess': t('A word by itself is easy to guess'),
'Names and surnames by themselves are easy to guess': t(
'Names and surnames by themselves are easy to guess'
),
'Common names and surnames are easy to guess': t(
'Common names and surnames are easy to guess'
),
"Capitalization doesn't help very much": t(
"Capitalization doesn't help very much"
),
'All-uppercase is almost as easy to guess as all-lowercase': t(
'All-uppercase is almost as easy to guess as all-lowercase'
),
"Reversed words aren't much harder to guess": t(
"Reversed words aren't much harder to guess"
),
"Predictable substitutions like '@' instead of 'a' don't help very much": t(
"Predictable substitutions like '@' instead of 'a' don't help very much"
),
};

const report = zxcvbn(props.password);

return (
<div className={styles.root}>
<div className={styles.title}>{t('Password strength')}</div>

<div className={styles.bar} data-password-score={report.score}>
<div className={styles.indicator} />
</div>

{report.feedback.warning || report.feedback.suggestions.length > 0 ? (
<ul className={styles.messages}>
{report.feedback.warning && (
<li className={classNames([styles.message, styles.messageWarning])}>
{feedbackTranslations[report.feedback.warning]}
</li>
)}

{report.feedback.suggestions.length > 0 &&
report.feedback.suggestions.map((suggestion, index) => (
<li className={styles.message} key={index}>
{feedbackTranslations[suggestion]}
</li>
))}
</ul>
) : (
<ul className={classNames([styles.messages, styles.messagesNone])} />
)}
</div>
);
}