Skip to content

Commit

Permalink
Merge pull request #4474 from kobotoolbox/4461-initial-code-cleanup
Browse files Browse the repository at this point in the history
Cleanup password related components
  • Loading branch information
magicznyleszek committed Jun 19, 2023
2 parents f04322d + b83a118 commit c0b6964
Show file tree
Hide file tree
Showing 13 changed files with 334 additions and 289 deletions.
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>
);
}

0 comments on commit c0b6964

Please sign in to comment.