Automated cherry pick of #4072 (#4172)

* MM-22292 Don't use redux to store username and password

* Set login and password MFA props

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Mattermost Build 2020-04-18 05:53:48 +02:00 committed by GitHub
parent 03d406021f
commit d100c7d95d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 104 additions and 205 deletions

View file

@ -12,27 +12,12 @@ import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {ViewTypes} from 'app/constants';
import {setAppCredentials} from 'app/init/credentials';
import PushNotifications from 'app/push_notifications';
import {getDeviceTimezoneAsync} from 'app/utils/timezone';
import {setCSRFFromCookie} from 'app/utils/security';
import {loadConfigAndLicense} from 'app/actions/views/root';
export function handleLoginIdChanged(loginId) {
return {
type: ViewTypes.LOGIN_ID_CHANGED,
loginId,
};
}
export function handlePasswordChanged(password) {
return {
type: ViewTypes.PASSWORD_CHANGED,
password,
};
}
export function handleSuccessfulLogin() {
return async (dispatch, getState) => {
await dispatch(loadConfigAndLicense());
@ -122,8 +107,6 @@ export function scheduleExpiredNotification(intl) {
}
export default {
handleLoginIdChanged,
handlePasswordChanged,
handleSuccessfulLogin,
scheduleExpiredNotification,
};

View file

@ -6,13 +6,7 @@ import thunk from 'redux-thunk';
import * as GeneralActions from 'mattermost-redux/actions/general';
import {ViewTypes} from 'app/constants';
import {
handleLoginIdChanged,
handlePasswordChanged,
handleSuccessfulLogin,
} from 'app/actions/views/login';
import {handleSuccessfulLogin} from 'app/actions/views/login';
jest.mock('app/init/credentials', () => ({
setAppCredentials: () => jest.fn(),
@ -51,28 +45,6 @@ describe('Actions.Views.Login', () => {
});
});
test('handleLoginIdChanged', () => {
const loginId = 'email@example.com';
const action = {
type: ViewTypes.LOGIN_ID_CHANGED,
loginId,
};
store.dispatch(handleLoginIdChanged(loginId));
expect(store.getActions()).toEqual([action]);
});
test('handlePasswordChanged', () => {
const password = 'password';
const action = {
type: ViewTypes.PASSWORD_CHANGED,
password,
};
store.dispatch(handlePasswordChanged(password));
expect(store.getActions()).toEqual([action]);
});
test('handleSuccessfulLogin gets config and license ', async () => {
const getClientConfig = jest.spyOn(GeneralActions, 'getClientConfig');
const getLicenseConfig = jest.spyOn(GeneralActions, 'getLicenseConfig');

View file

@ -37,9 +37,6 @@ const ViewTypes = keyMirror({
DATA_CLEANUP: null,
SERVER_URL_CHANGED: null,
LOGIN_ID_CHANGED: null,
PASSWORD_CHANGED: null,
POST_DRAFT_CHANGED: null,
COMMENT_DRAFT_CHANGED: null,
SEARCH_DRAFT_CHANGED: null,

View file

@ -3,7 +3,6 @@
import {Alert, Platform} from 'react-native';
import {handleLoginIdChanged} from 'app/actions/views/login';
import {setServerUrl} from 'app/actions/views/select_server';
import {getTranslations} from 'app/i18n';
import mattermostBucket from 'app/mattermost_bucket';
@ -28,7 +27,6 @@ class EMMProvider {
this.allowOtherServers = true;
this.emmServerUrl = null;
this.emmUsername = null;
}
checkIfDeviceIsTrusted = (store) => {
@ -110,7 +108,6 @@ class EMMProvider {
const credentials = await getAppCredentials();
if (!credentials) {
this.emmServerUrl = managedConfig.serverUrl;
this.emmUsername = managedConfig.username;
if (managedConfig.allowOtherServers && managedConfig.allowOtherServers === 'false') {
this.allowOtherServers = false;
@ -124,10 +121,6 @@ class EMMProvider {
if (this.emmServerUrl) {
dispatch(setServerUrl(this.emmServerUrl));
}
if (this.emmUsername) {
dispatch(handleLoginIdChanged(this.emmUsername));
}
}
return true;

View file

@ -136,10 +136,6 @@ const state = {
i18n: {
locale: '',
},
login: {
loginId: '',
password: '',
},
root: {
deepLinkURL: '',
hydrationComplete: false,

View file

@ -8,7 +8,6 @@ import channel from './channel';
import clientUpgrade from './client_upgrade';
import extension from './extension';
import i18n from './i18n';
import login from './login';
import recentEmojis from './recent_emojis';
import root from './root';
import search from './search';
@ -25,7 +24,6 @@ export default combineReducers({
clientUpgrade,
extension,
i18n,
login,
recentEmojis,
root,
search,

View file

@ -1,33 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {UserTypes} from 'mattermost-redux/action_types';
import {ViewTypes} from 'app/constants';
function loginId(state = '', action) {
switch (action.type) {
case ViewTypes.LOGIN_ID_CHANGED:
return action.loginId.trim();
case UserTypes.LOGOUT_SUCCESS:
return '';
default:
return state;
}
}
function password(state = '', action) {
switch (action.type) {
case ViewTypes.PASSWORD_CHANGED:
return action.password;
case UserTypes.LOGOUT_SUCCESS:
return '';
default:
return state;
}
}
export default combineReducers({
loginId,
password,
});

View file

@ -24,6 +24,7 @@ import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
import {resetToChannel, goToScreen} from 'app/actions/navigation';
import mattermostManaged from 'app/mattermost_managed';
import {preventDoubleTap} from 'app/utils/tap';
import tracker from 'app/utils/time_tracker';
import {t} from 'app/utils/i18n';
@ -38,16 +39,12 @@ export const mfaExpectedErrors = ['mfa.validate_token.authenticate.app_error', '
export default class Login extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
handleLoginIdChanged: PropTypes.func.isRequired,
handlePasswordChanged: PropTypes.func.isRequired,
handleSuccessfulLogin: PropTypes.func.isRequired,
scheduleExpiredNotification: PropTypes.func.isRequired,
login: PropTypes.func.isRequired,
}).isRequired,
config: PropTypes.object.isRequired,
license: PropTypes.object.isRequired,
loginId: PropTypes.string.isRequired,
password: PropTypes.string.isRequired,
isLandscape: PropTypes.bool.isRequired,
};
@ -68,6 +65,7 @@ export default class Login extends PureComponent {
Dimensions.addEventListener('change', this.orientationDidChange);
setMfaPreflightDone(false);
this.setEmmUsernameIfAvailable();
}
componentWillUnmount() {
@ -88,8 +86,10 @@ export default class Login extends PureComponent {
const {intl} = this.context;
const screen = 'MFA';
const title = intl.formatMessage({id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'});
const loginId = this.loginId?._lastNativeText; //eslint-disable-line no-underscore-dangle
const password = this.passwd?._lastNativeText; //eslint-disable-line no-underscore-dangle
goToScreen(screen, title, {onMfaComplete: this.checkLoginResponse});
goToScreen(screen, title, {onMfaComplete: this.checkLoginResponse, loginId, password});
};
blur = () => {
@ -98,83 +98,6 @@ export default class Login extends PureComponent {
Keyboard.dismiss();
};
preSignIn = preventDoubleTap(() => {
this.setState({error: null, isLoading: true});
Keyboard.dismiss();
InteractionManager.runAfterInteractions(async () => {
if (!this.props.loginId) {
t('login.noEmail');
t('login.noEmailLdapUsername');
t('login.noEmailUsername');
t('login.noEmailUsernameLdapUsername');
t('login.noLdapUsername');
t('login.noUsername');
t('login.noUsernameLdapUsername');
// it's slightly weird to be constructing the message ID, but it's a bit nicer than triply nested if statements
let msgId = 'login.no';
if (this.props.config.EnableSignInWithEmail === 'true') {
msgId += 'Email';
}
if (this.props.config.EnableSignInWithUsername === 'true') {
msgId += 'Username';
}
if (this.props.license.IsLicensed === 'true' && this.props.config.EnableLdap === 'true') {
msgId += 'LdapUsername';
}
this.setState({
isLoading: false,
error: {
intl: {
id: msgId,
defaultMessage: '',
values: {
ldapUsername: this.props.config.LdapLoginFieldName ||
this.context.intl.formatMessage({
id: 'login.ldapUsernameLower',
defaultMessage: 'AD/LDAP username',
}),
},
},
},
});
return;
}
if (!this.props.password) {
this.setState({
isLoading: false,
error: {
intl: {
id: t('login.noPassword'),
defaultMessage: 'Please enter your password',
},
},
});
return;
}
this.signIn();
});
});
scheduleSessionExpiredNotification = () => {
const {intl} = this.context;
const {actions} = this.props;
actions.scheduleExpiredNotification(intl);
};
signIn = () => {
const {actions, loginId, password} = this.props;
const {isLoading} = this.state;
if (isLoading) {
actions.login(loginId.toLowerCase(), password).
then(this.checkLoginResponse);
}
};
checkLoginResponse = (data) => {
if (mfaExpectedErrors.includes(data?.error?.server_error_id)) { // eslint-disable-line camelcase
this.goToMfa();
@ -228,6 +151,14 @@ export default class Login extends PureComponent {
return '';
}
forgotPassword = () => {
const {intl} = this.context;
const screen = 'ForgotPassword';
const title = intl.formatMessage({id: 'password_form.title', defaultMessage: 'Password Reset'});
goToScreen(screen, title);
}
getLoginErrorMessage = (error) => {
return (
this.getServerErrorForLogin(error) ||
@ -274,6 +205,10 @@ export default class Login extends PureComponent {
this.loginId = ref;
};
orientationDidChange = () => {
this.scroll.scrollToPosition(0, 0, true);
};
passwordRef = (ref) => {
this.passwd = ref;
};
@ -282,22 +217,98 @@ export default class Login extends PureComponent {
this.passwd.focus();
};
orientationDidChange = () => {
this.scroll.scrollToPosition(0, 0, true);
preSignIn = preventDoubleTap(() => {
this.setState({error: null, isLoading: true});
Keyboard.dismiss();
InteractionManager.runAfterInteractions(async () => {
const loginId = this.loginId?._lastNativeText; //eslint-disable-line no-underscore-dangle
if (!loginId) {
t('login.noEmail');
t('login.noEmailLdapUsername');
t('login.noEmailUsername');
t('login.noEmailUsernameLdapUsername');
t('login.noLdapUsername');
t('login.noUsername');
t('login.noUsernameLdapUsername');
// it's slightly weird to be constructing the message ID, but it's a bit nicer than triply nested if statements
let msgId = 'login.no';
if (this.props.config.EnableSignInWithEmail === 'true') {
msgId += 'Email';
}
if (this.props.config.EnableSignInWithUsername === 'true') {
msgId += 'Username';
}
if (this.props.license.IsLicensed === 'true' && this.props.config.EnableLdap === 'true') {
msgId += 'LdapUsername';
}
this.setState({
isLoading: false,
error: {
intl: {
id: msgId,
defaultMessage: '',
values: {
ldapUsername: this.props.config.LdapLoginFieldName ||
this.context.intl.formatMessage({
id: 'login.ldapUsernameLower',
defaultMessage: 'AD/LDAP username',
}),
},
},
},
});
return;
}
const password = this.passwd?._lastNativeText; //eslint-disable-line no-underscore-dangle
if (!password) {
this.setState({
isLoading: false,
error: {
intl: {
id: t('login.noPassword'),
defaultMessage: 'Please enter your password',
},
},
});
return;
}
this.signIn();
});
});
scheduleSessionExpiredNotification = () => {
const {intl} = this.context;
const {actions} = this.props;
actions.scheduleExpiredNotification(intl);
};
scrollRef = (ref) => {
this.scroll = ref;
};
forgotPassword = () => {
const {intl} = this.context;
const screen = 'ForgotPassword';
const title = intl.formatMessage({id: 'password_form.title', defaultMessage: 'Password Reset'});
goToScreen(screen, title);
setEmmUsernameIfAvailable = async () => {
const managedConfig = await mattermostManaged.getConfig();
if (managedConfig?.username && this.loginId) {
this.loginId.setNativeProps({text: 'sample'});
}
}
signIn = () => {
const loginId = this.loginId?._lastNativeText; //eslint-disable-line no-underscore-dangle
const password = this.passwd?._lastNativeText; //eslint-disable-line no-underscore-dangle
const {actions} = this.props;
const {isLoading} = this.state;
if (isLoading) {
actions.login(loginId.toLowerCase(), password).
then(this.checkLoginResponse);
}
};
render() {
const {isLoading} = this.state;
@ -383,8 +394,6 @@ export default class Login extends PureComponent {
<ErrorText error={this.state.error}/>
<TextInput
ref={this.loginRef}
value={this.props.loginId}
onChangeText={this.props.actions.handleLoginIdChanged}
style={GlobalStyles.inputBox}
placeholder={this.createLoginPlaceholder()}
placeholderTextColor={changeOpacity('#000', 0.5)}
@ -399,8 +408,6 @@ export default class Login extends PureComponent {
/>
<TextInput
ref={this.passwordRef}
value={this.props.password}
onChangeText={this.props.actions.handlePasswordChanged}
style={GlobalStyles.inputBox}
placeholder={this.context.intl.formatMessage({id: 'login.password', defaultMessage: 'Password'})}
placeholderTextColor={changeOpacity('#000', 0.5)}

View file

@ -21,12 +21,8 @@ describe('Login', () => {
license: {
IsLicensed: 'false',
},
loginId: '',
password: '',
loginRequest: {},
actions: {
handleLoginIdChanged: jest.fn(),
handlePasswordChanged: jest.fn(),
handleSuccessfulLogin: jest.fn(),
scheduleExpiredNotification: jest.fn(),
login: jest.fn(),

View file

@ -8,14 +8,6 @@ import {login} from 'app/actions/views/user';
import Mfa from './mfa';
function mapStateToProps(state) {
const {loginId, password} = state.views.login;
return {
loginId,
password,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
@ -24,4 +16,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Mfa);
export default connect(null, mapDispatchToProps)(Mfa);

View file

@ -53,7 +53,7 @@ const setTransforms = [
export default function configureAppStore(initialState) {
const viewsBlackListFilter = createBlacklistFilter(
'views',
['extension', 'login', 'root'],
['extension', 'root'],
);
const typingBlackListFilter = createBlacklistFilter(

View file

@ -47,7 +47,6 @@ module.exports = [
'app/reducers/views/extension.js',
'app/reducers/views/i18n.js',
'app/reducers/views/index.js',
'app/reducers/views/login.js',
'app/reducers/views/post.js',
'app/reducers/views/recent_emojis.js',
'app/reducers/views/root.js',

View file

@ -47,7 +47,6 @@ module.exports = [
'./node_modules/app/reducers/views/extension.js',
'./node_modules/app/reducers/views/i18n.js',
'./node_modules/app/reducers/views/index.js',
'./node_modules/app/reducers/views/login.js',
'./node_modules/app/reducers/views/post.js',
'./node_modules/app/reducers/views/recent_emojis.js',
'./node_modules/app/reducers/views/root.js',