Implement login action using matrix-js-sdk

Change-Id: Ie8e69cc995c86a90bf73e74b13e063f957aa2bce
This commit is contained in:
Manuel Stahl 2019-03-12 08:47:58 +01:00
parent e52bf651bb
commit be7b761fff
8 changed files with 275 additions and 5 deletions

View File

@ -7,6 +7,7 @@
// NOTE: initialState constant is necessary so that Rekit could auto add initial state when creating async actions.
const initialState = {
mtx: null,
};
export default initialState;

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Redirect } from 'react-router-dom'
import matrixLogo from '../../images/matrix-logo.svg';
import * as actions from './redux/actions';
@ -12,25 +12,96 @@ export class Login extends Component {
actions: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
homeserver: 'https://matrix.org',
username: '',
password: '',
submitted: false
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(e) {
const { name, value } = e.target;
this.setState({ [name]: value });
}
handleSubmit(e) {
e.preventDefault();
this.setState({ submitted: true });
const { homeserver, username, password } = this.state;
const { login } = this.props.actions;
const { history } = this.props;
if (homeserver && username && password) {
login(homeserver, username, password).then(
history.push("/user-admin/list")
);
}
}
render() {
const { mtx } = this.props;
const { loginPending } = this.props.home;
const { homeserver, username, password, submitted } = this.state;
return (
<div className="home-login">
{mtx && mtx.clientRunning &&
<Redirect to="/user-admin/list" />
}
<header className="app-header">
<img src={matrixLogo} className="app-logo" alt="logo" />
<h1 className="app-title">Welcome to Synapse Admin</h1>
</header>
<form className="app-login">
<form className="app-login" onSubmit={this.handleSubmit}>
<table>
<tbody>
<tr>
<th>Username:</th><td><input name="user"/></td>
<th className={'form-group' + (submitted && !homeserver ? ' has-error' : '')}>
<label htmlFor="username">Homeserver</label>
</th>
<td>
<input type="text" className="form-control" name="homeserver" value={homeserver} onChange={this.handleChange} />
{submitted && !homeserver &&
<div className="help-block">Homeserver is required</div>
}
</td>
</tr>
<tr>
<th>Password:</th><td><input name="password" type="password"/></td>
<th className={'form-group' + (submitted && !username ? ' has-error' : '')}>
<label htmlFor="username">Username</label>
</th>
<td>
<input type="text" className="form-control" name="username" value={username} onChange={this.handleChange} />
{submitted && !username &&
<div className="help-block">Username is required</div>
}
</td>
</tr>
<tr>
<th className={'form-group' + (submitted && !password ? ' has-error' : '')}>
<label htmlFor="password">Password</label>
</th>
<td>
<input type="password" className="form-control" name="password" value={password} onChange={this.handleChange} />
{submitted && !password &&
<div className="help-block">Password is required</div>
}
</td>
</tr>
</tbody>
</table>
<Link to="/user-admin/list">Login</Link>
<div className="form-group">
<button className="btn btn-primary">Login</button>
{loginPending &&
<img alt="logging in" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" />
}
</div>
</form>
</div>
);
@ -40,6 +111,7 @@ export class Login extends Component {
/* istanbul ignore next */
function mapStateToProps(state) {
return {
mtx: state.common.mtx,
home: state.home,
};
}

View File

@ -0,0 +1 @@
export { login, dismissLoginError } from './login';

View File

@ -0,0 +1,4 @@
export const HOME_LOGIN_BEGIN = 'HOME_LOGIN_BEGIN';
export const HOME_LOGIN_SUCCESS = 'HOME_LOGIN_SUCCESS';
export const HOME_LOGIN_FAILURE = 'HOME_LOGIN_FAILURE';
export const HOME_LOGIN_DISMISS_ERROR = 'HOME_LOGIN_DISMISS_ERROR';

View File

@ -1,4 +1,6 @@
const initialState = {
loginPending: false,
loginError: null,
};
export default initialState;

View File

@ -0,0 +1,91 @@
import {
HOME_LOGIN_BEGIN,
HOME_LOGIN_SUCCESS,
HOME_LOGIN_FAILURE,
HOME_LOGIN_DISMISS_ERROR,
} from './constants';
import Matrix from 'matrix-js-sdk';
export function login(homeserver, username, password) {
return (dispatch, getState) => {
dispatch({
type: HOME_LOGIN_BEGIN,
});
// Return a promise so that you could control UI flow without states in the store.
// For example: after submit a form, you need to redirect the page to another when succeeds or show some errors message if fails.
// It's hard to use state to manage it, but returning a promise allows you to easily achieve it.
// e.g.: handleSubmit() { this.props.actions.submitForm(data).then(()=> {}).catch(() => {}); }
const promise = new Promise((resolve, reject) => {
var state = getState();
state.common.mtx = Matrix.createClient(homeserver);
const doRequest = state.common.mtx.login("m.login.password", { "user": username, "password": password });
doRequest.then(
(res) => {
state.common.mtx.startClient();
dispatch({
type: HOME_LOGIN_SUCCESS,
data: res,
});
resolve(res);
},
// Use rejectHandler as the second argument so that render errors won't be caught.
(err) => {
dispatch({
type: HOME_LOGIN_FAILURE,
data: { error: err },
});
reject(err.message);
},
);
});
return promise;
};
}
// Async action saves request error by default, this method is used to dismiss the error info.
// If you don't want errors to be saved in Redux store, just ignore this method.
export function dismissLoginError() {
return {
type: HOME_LOGIN_DISMISS_ERROR,
};
}
export function reducer(state, action) {
switch (action.type) {
case HOME_LOGIN_BEGIN:
// Just after a request is sent
return {
...state,
loginPending: true,
loginError: null,
};
case HOME_LOGIN_SUCCESS:
// The request is success
return {
...state,
loginPending: false,
loginError: null,
};
case HOME_LOGIN_FAILURE:
// The request is failed
return {
...state,
loginPending: false,
loginError: action.data.error,
};
case HOME_LOGIN_DISMISS_ERROR:
// Dismiss the request failure error
return {
...state,
loginError: null,
};
default:
return state;
}
}

View File

@ -1,6 +1,8 @@
import initialState from './initialState';
import { reducer as loginReducer } from './login';
const reducers = [
loginReducer,
];
export default function reducer(state = initialState, action) {

View File

@ -0,0 +1,97 @@
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import nock from 'nock';
import {
HOME_LOGIN_BEGIN,
HOME_LOGIN_SUCCESS,
HOME_LOGIN_FAILURE,
HOME_LOGIN_DISMISS_ERROR,
} from '../../../../src/features/home/redux/constants';
import {
login,
dismissLoginError,
reducer,
} from '../../../../src/features/home/redux/login';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('home/redux/login', () => {
afterEach(() => {
nock.cleanAll();
});
it('dispatches success action when login succeeds', () => {
const store = mockStore({});
return store.dispatch(login())
.then(() => {
const actions = store.getActions();
expect(actions[0]).toHaveProperty('type', HOME_LOGIN_BEGIN);
expect(actions[1]).toHaveProperty('type', HOME_LOGIN_SUCCESS);
});
});
it('dispatches failure action when login fails', () => {
const store = mockStore({});
return store.dispatch(login({ error: true }))
.catch(() => {
const actions = store.getActions();
expect(actions[0]).toHaveProperty('type', HOME_LOGIN_BEGIN);
expect(actions[1]).toHaveProperty('type', HOME_LOGIN_FAILURE);
expect(actions[1]).toHaveProperty('data.error', expect.anything());
});
});
it('returns correct action by dismissLoginError', () => {
const expectedAction = {
type: HOME_LOGIN_DISMISS_ERROR,
};
expect(dismissLoginError()).toEqual(expectedAction);
});
it('handles action type HOME_LOGIN_BEGIN correctly', () => {
const prevState = { loginPending: false };
const state = reducer(
prevState,
{ type: HOME_LOGIN_BEGIN }
);
expect(state).not.toBe(prevState); // should be immutable
expect(state.loginPending).toBe(true);
});
it('handles action type HOME_LOGIN_SUCCESS correctly', () => {
const prevState = { loginPending: true };
const state = reducer(
prevState,
{ type: HOME_LOGIN_SUCCESS, data: {} }
);
expect(state).not.toBe(prevState); // should be immutable
expect(state.loginPending).toBe(false);
});
it('handles action type HOME_LOGIN_FAILURE correctly', () => {
const prevState = { loginPending: true };
const state = reducer(
prevState,
{ type: HOME_LOGIN_FAILURE, data: { error: new Error('some error') } }
);
expect(state).not.toBe(prevState); // should be immutable
expect(state.loginPending).toBe(false);
expect(state.loginError).toEqual(expect.anything());
});
it('handles action type HOME_LOGIN_DISMISS_ERROR correctly', () => {
const prevState = { loginError: new Error('some error') };
const state = reducer(
prevState,
{ type: HOME_LOGIN_DISMISS_ERROR }
);
expect(state).not.toBe(prevState); // should be immutable
expect(state.loginError).toBe(null);
});
});