Rewrite: Reset Login Form (#18237)

Co-authored-by: Gabriel Henriques <gabriel.henriques@rocket.chat>
Co-authored-by: Martin <martin.schoeler@rocket.chat>
Co-authored-by: dougfabris <deefabris@gmail.com>
pull/19366/head
Guilherme Gazzo 6 years ago committed by GitHub
parent 878c4c8fa7
commit b950f17e42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      app/lib/server/lib/PasswordPolicyClass.js
  2. 4
      app/lib/server/startup/userDataStream.js
  3. 4
      app/models/server/models/_BaseDb.js
  4. 4
      app/theme/client/imports/general/base_old.css
  5. 2
      app/ui-login/client/index.js
  6. 37
      app/ui-login/client/reset-password/resetPassword.html
  7. 123
      app/ui-login/client/reset-password/resetPassword.js
  8. 6
      client/contexts/ServerContext.ts
  9. 112
      client/login/ResetPassword/ResetPassword.js
  10. 12
      client/login/ResetPassword/ResetPassword.stories.js
  11. 12
      client/login/index.js
  12. 1
      client/main.js
  13. 6
      packages/rocketchat-i18n/i18n/en.i18n.json
  14. 6
      server/methods/getPasswordPolicy.js

@ -98,28 +98,28 @@ class PasswordPolicy {
if (this.enabled) {
data.enabled = true;
if (this.minLength >= 1) {
data.policy.push('get-password-policy-minLength', { minLength: this.minLength });
data.policy.push(['get-password-policy-minLength', { minLength: this.minLength }]);
}
if (this.maxLength >= 1) {
data.policy.push('get-password-policy-maxLength', { maxLength: this.maxLength });
data.policy.push(['get-password-policy-maxLength', { maxLength: this.maxLength }]);
}
if (this.forbidRepeatingCharacters) {
data.policy.push('get-password-policy-forbidRepeatingCharacters');
data.policy.push(['get-password-policy-forbidRepeatingCharacters']);
}
if (this.forbidRepeatingCharactersCount) {
data.policy.push('get-password-policy-forbidRepeatingCharactersCount', { forbidRepeatingCharactersCount: this.forbidRepeatingCharactersCount });
data.policy.push(['get-password-policy-forbidRepeatingCharactersCount', { forbidRepeatingCharactersCount: this.forbidRepeatingCharactersCount }]);
}
if (this.mustContainAtLeastOneLowercase) {
data.policy.push('get-password-policy-mustContainAtLeastOneLowercase');
data.policy.push(['get-password-policy-mustContainAtLeastOneLowercase']);
}
if (this.mustContainAtLeastOneUppercase) {
data.policy.push('get-password-policy-mustContainAtLeastOneUppercase');
data.policy.push(['get-password-policy-mustContainAtLeastOneUppercase']);
}
if (this.mustContainAtLeastOneNumber) {
data.policy.push('get-password-policy-mustContainAtLeastOneNumber');
data.policy.push(['get-password-policy-mustContainAtLeastOneNumber']);
}
if (this.mustContainAtLeastOneSpecialCharacter) {
data.policy.push('get-password-policy-mustContainAtLeastOneSpecialCharacter');
data.policy.push(['get-password-policy-mustContainAtLeastOneSpecialCharacter']);
}
}
return data;

@ -84,10 +84,10 @@ if (disableOplog) {
});
}
Users.on('change', ({ clientAction, id, data, diff }) => {
Users.on('change', ({ clientAction, id, data, diff, unset }) => {
switch (clientAction) {
case 'updated':
Notifications.notifyUserInThisInstance(id, 'userData', { diff, type: clientAction });
Notifications.notifyUserInThisInstance(id, 'userData', { diff, unset, type: clientAction });
if (disableOplog) {
processOnChange(diff, id);

@ -239,11 +239,12 @@ export class BaseDb extends EventEmitter {
}
}
}
const unset = {};
if (op.o.$unset) {
for (const key in op.o.$unset) {
if (op.o.$unset.hasOwnProperty(key)) {
diff[key] = undefined;
unset[key] = 1;
}
}
}
@ -253,6 +254,7 @@ export class BaseDb extends EventEmitter {
clientAction: 'updated',
id,
diff,
unset,
oplog: true,
});
return;

@ -3419,7 +3419,7 @@
}
}
& a {
& a:not(.rcx-box) {
font-weight: 300;
}
@ -3430,7 +3430,7 @@
vertical-align: middle;
}
& header {
& header:not(.rcx-box) {
position: relative;
z-index: 1;

@ -1,6 +1,4 @@
import './routes';
import './reset-password/resetPassword.html';
import './reset-password/resetPassword';
import './login/footer.html';
import './login/form.html';
import './login/header.html';

@ -1,37 +0,0 @@
<template name="resetPassword">
<form id="login-card" class="content-background-color color-primary-font-color" action="/">
<div class="fields">
<header>
{{#if requirePasswordChange}}
{{#if requirePasswordChangeReason}}
<p>{{_ requirePasswordChangeReason}}</p>
{{else}}
<p>{{_ 'You_need_to_change_your_password'}}</p>
{{/if}}
{{else}}
<p>{{_ "Please_enter_your_new_password_below"}}</p>
{{/if}}
</header>
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Type_your_new_password"}}</div>
<div class="rc-input__wrapper">
<input type="password" name="newPassword" id="newPassword" dir="auto" class="rc-input__element" autocomplete="off">
</div>
</label>
</div>
<div class="submit">
<button data-loading-text="{{_ "Please_wait"}}..." class="rc-button rc-button--primary resetpass" {{disabled}} >{{_ "Reset"}}</button>
</div>
</div>
{{#if passwordPolicyEnabled}}
<div class="wrapper">
{{#each passwordPolicy}}
<p>* {{_ this}}</p>
{{/each}}
</div>
{{/if}}
</form>
</template>

@ -1,123 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import toastr from 'toastr';
import { ReactiveDict } from 'meteor/reactive-dict';
import { ReactiveVar } from 'meteor/reactive-var';
import { modal, call } from '../../../ui-utils/client';
import { t } from '../../../utils';
import { callbacks } from '../../../callbacks';
Template.resetPassword.helpers({
disabled() {
return Template.instance().state.get('password') ? '' : 'disabled';
},
requirePasswordChange() {
const user = Meteor.user();
if (user) {
return user.requirePasswordChange;
}
},
requirePasswordChangeReason() {
const user = Meteor.user();
if (user) {
return user.requirePasswordChangeReason;
}
},
passwordPolicyEnabled() {
return Template.instance().passwordPolicyEnabled.get();
},
passwordPolicy() {
return Template.instance().passwordPolicyRules.get();
},
});
const resetPassword = (token, password) => new Promise((resolve, reject) => {
Accounts.resetPassword(token, password, function(error, result) {
if (!error) {
FlowRouter.go('home');
toastr.success(t('Password_changed_successfully'));
callbacks.run('userPasswordReset');
resolve(result);
}
if (error.error !== 'totp-required') {
reject(error);
}
toastr.success(t('Password_changed_successfully'));
callbacks.run('userPasswordReset');
FlowRouter.go('login');
resolve(result);
});
});
async function setUserPassword(password) {
try {
const result = await call('setUserPassword', password);
if (!result) {
return toastr.error(t('Error'));
}
Meteor.users.update({ _id: Meteor.userId() }, {
$set: {
requirePasswordChange: false,
},
});
toastr.remove();
toastr.success(t('Password_changed_successfully'));
} catch (e) {
console.error(e);
toastr.error(t('Error'));
}
}
Template.resetPassword.events({
'input #newPassword'(e, i) {
i.state.set('password', e.currentTarget.value);
},
async 'submit #login-card'(event, i) {
event.preventDefault();
const password = i.state.get('password');
const token = FlowRouter.getParam('token');
if (!password || !password.trim()) {
return;
}
i.state.set('loading', true);
try {
if (Meteor.userId() && !token) {
return setUserPassword(password);
}
await resetPassword(token, password);
} catch (error) {
modal.open({
title: t('Error_changing_password'),
type: 'error',
});
} finally {
i.state.set('loading', false);
}
},
});
Template.resetPassword.onRendered(function() {
this.find('[name=newPassword]').focus();
});
Template.resetPassword.onCreated(function() {
this.state = new ReactiveDict({ password: '' });
this.passwordPolicyEnabled = new ReactiveVar(false);
this.passwordPolicyRules = new ReactiveVar();
Meteor.call('getPasswordPolicy', (error, result) => {
if (result.enabled) {
this.passwordPolicyEnabled.set(true);
this.passwordPolicyRules.set(result.policy);
}
});
});

@ -56,9 +56,9 @@ export enum AsyncState {
ERROR = 'error',
}
export const useMethodData = <T>(methodName: string, args: any[] = []): [T | null, AsyncState, () => void] => {
export const useMethodData = <T>(methodName: string, args: any[] = []): [T | undefined, AsyncState, () => void] => {
const getData: (...args: unknown[]) => Promise<T> = useMethod(methodName);
const [[data, state], updateState] = useState<[T | null, AsyncState]>([null, AsyncState.LOADING]);
const [[data, state], updateState] = useState<[T | undefined, AsyncState]>([undefined, AsyncState.LOADING]);
const isMountedRef = useRef(true);
@ -94,7 +94,7 @@ export const useMethodData = <T>(methodName: string, args: any[] = []): [T | nul
return [data, state, fetchData];
};
export const usePolledMethodData = <T>(methodName: string, args: any[] = [], intervalMs: number): [T | null, AsyncState, () => void] => {
export const usePolledMethodData = <T>(methodName: string, args: any[] = [], intervalMs: number): [T | undefined, AsyncState, () => void] => {
const [data, state, fetchData] = useMethodData<T>(methodName, args);
useEffect(() => {

@ -0,0 +1,112 @@
import { Meteor } from 'meteor/meteor';
import React, { useState, useCallback, useMemo } from 'react';
import { Button, TextInput, Field, Modal, Box, Throbber } from '@rocket.chat/fuselage';
import { useSafely } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '../../contexts/TranslationContext';
import { useUser } from '../../contexts/UserContext';
import { useMethodData, useMethod } from '../../contexts/ServerContext';
import { useRouteParameter, useRoute } from '../../contexts/RouterContext';
const getChangePasswordReason = ({
requirePasswordChange,
requirePasswordChangeReason = requirePasswordChange
? 'You_need_to_change_your_password'
: 'Please_enter_your_new_password_below',
} = {}) => requirePasswordChangeReason;
const ResetPassword = () => {
const user = useUser();
const t = useTranslation();
const setUserPassword = useMethod('setUserPassword');
const resetPassword = useMethod('resetPassword');
const token = useRouteParameter('token');
const params = useMemo(() => [{
token,
}], [token]);
const [{ enabled: policyEnabled, policy: policies } = {}] = useMethodData('getPasswordPolicy', params);
const router = useRoute('home');
const changePasswordReason = getChangePasswordReason(user || {});
const [newPassword, setNewPassword] = useState('');
const [isLoading, setIsLoading] = useSafely(useState(false));
const [error, setError] = useSafely(useState());
const handleOnChange = useCallback((event) => setNewPassword(event.currentTarget.value), [setNewPassword]);
const isSubmitDisabled = !newPassword.trim() || isLoading;
const handleSubmit = useCallback(async (e) => {
e.preventDefault();
if (isSubmitDisabled) {
return;
}
setIsLoading(true);
try {
if (token && resetPassword) {
const result = await resetPassword(token, newPassword);
await Meteor.loginWithToken(result.token);
router.push({});
} else {
await setUserPassword(newPassword);
}
} catch ({ error, reason = error }) {
setError(reason);
} finally {
setIsLoading(false);
}
}, [isSubmitDisabled, setIsLoading, token, resetPassword, newPassword, router, setUserPassword, setError]);
return (
<Modal is='form' onSubmit={handleSubmit}>
<Modal.Header>
<Modal.Title textAlign='start'>{t('Password')}</Modal.Title>
</Modal.Header>
<Modal.Content>
<Field>
<Field.Label>
{t(changePasswordReason)}
</Field.Label>
<Field.Row>
<TextInput
placeholder={t('Type_your_new_password')}
type='password'
name='newPassword'
id='newPassword'
dir='auto'
onChange={handleOnChange}
autoComplete='off'
value={newPassword}
/>
</Field.Row>
{error && <Field.Error>{error}</Field.Error>}
{policyEnabled && (
<Field.Hint>
{policies.map((policy, index) => (
<Box is='p' textAlign='start' key={index}>{t(...policy)}</Box>
))}
</Field.Hint>
)}
</Field>
</Modal.Content>
<Modal.Footer>
<Field>
<Field.Row>
<Button
primary
disabled={isSubmitDisabled}
type='submit'
>
{isLoading ? <Throbber size='x12' inheritColor /> : t('Reset')}
</Button>
</Field.Row>
</Field>
</Modal.Footer>
</Modal>
);
};
export default ResetPassword;

@ -0,0 +1,12 @@
import React from 'react';
import ResetPassword from './ResetPassword';
export default {
title: 'components/Login/ResetPassword',
component: ResetPassword,
};
export const Basic = () => (
<ResetPassword/>
);

@ -0,0 +1,12 @@
import { HTML } from 'meteor/htmljs';
import { createTemplateForComponent } from '../reactAdapters';
createTemplateForComponent(
'resetPassword',
() => import('./ResetPassword/ResetPassword'),
{
// eslint-disable-next-line new-cap
renderContainerView: () => HTML.DIV({ style: 'display: flex;' }),
},
);

@ -30,4 +30,5 @@ import './startup/unread';
import './startup/userSetUtcOffset';
import './startup/usersObserve';
import './admin';
import './login';
import './channel';

@ -1764,10 +1764,10 @@
"get-password-policy-forbidRepeatingCharactersCount": "The password should not contain more than __forbidRepeatingCharactersCount__ repeating characters",
"get-password-policy-maxLength": "The password should be maximum __maxLength__ characters long",
"get-password-policy-minLength": "The password should be minimum __minLength__ characters long",
"get-password-policy-mustContainAtLeastOneLowercase": "The password should contain atleast one lowercase letter",
"get-password-policy-mustContainAtLeastOneLowercase": "The password should contain at least one lowercase letter",
"get-password-policy-mustContainAtLeastOneNumber": "The password should contain atleast one number",
"get-password-policy-mustContainAtLeastOneSpecialCharacter": "The password should contain atleast one special character",
"get-password-policy-mustContainAtLeastOneUppercase": "The password should contain atleast one uppercase letter",
"get-password-policy-mustContainAtLeastOneSpecialCharacter": "The password should contain at least one special character",
"get-password-policy-mustContainAtLeastOneUppercase": "The password should contain at least one uppercase letter",
"Generate_New_Link": "Generate New Link",
"github_no_public_email": "You don't have any email as public email in your GitHub account",
"Give_a_unique_name_for_the_custom_oauth": "Give a unique name for the custom oauth",

@ -1,10 +1,12 @@
import { Meteor } from 'meteor/meteor';
import { Users } from '../../app/models';
import { passwordPolicy } from '../../app/lib';
Meteor.methods({
getPasswordPolicy() {
if (!Meteor.userId()) {
getPasswordPolicy(params) {
const user = Users.findOne({ 'services.password.reset.token': params.token });
if (!user && !Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'getPasswordPolicy',
});

Loading…
Cancel
Save