Implement login action using matrix-js-sdk
Change-Id: Ie8e69cc995c86a90bf73e74b13e063f957aa2bce
This commit is contained in:
parent
e52bf651bb
commit
be7b761fff
@ -7,6 +7,7 @@
|
|||||||
// NOTE: initialState constant is necessary so that Rekit could auto add initial state when creating async actions.
|
// NOTE: initialState constant is necessary so that Rekit could auto add initial state when creating async actions.
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
mtx: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default initialState;
|
export default initialState;
|
||||||
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-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 matrixLogo from '../../images/matrix-logo.svg';
|
||||||
import * as actions from './redux/actions';
|
import * as actions from './redux/actions';
|
||||||
|
|
||||||
@ -12,25 +12,96 @@ export class Login extends Component {
|
|||||||
actions: PropTypes.object.isRequired,
|
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() {
|
render() {
|
||||||
|
const { mtx } = this.props;
|
||||||
|
const { loginPending } = this.props.home;
|
||||||
|
const { homeserver, username, password, submitted } = this.state;
|
||||||
return (
|
return (
|
||||||
<div className="home-login">
|
<div className="home-login">
|
||||||
|
{mtx && mtx.clientRunning &&
|
||||||
|
<Redirect to="/user-admin/list" />
|
||||||
|
}
|
||||||
<header className="app-header">
|
<header className="app-header">
|
||||||
<img src={matrixLogo} className="app-logo" alt="logo" />
|
<img src={matrixLogo} className="app-logo" alt="logo" />
|
||||||
<h1 className="app-title">Welcome to Synapse Admin</h1>
|
<h1 className="app-title">Welcome to Synapse Admin</h1>
|
||||||
</header>
|
</header>
|
||||||
<form className="app-login">
|
<form className="app-login" onSubmit={this.handleSubmit}>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<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>
|
||||||
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -40,6 +111,7 @@ export class Login extends Component {
|
|||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
|
mtx: state.common.mtx,
|
||||||
home: state.home,
|
home: state.home,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export { login, dismissLoginError } from './login';
|
@ -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';
|
@ -1,4 +1,6 @@
|
|||||||
const initialState = {
|
const initialState = {
|
||||||
|
loginPending: false,
|
||||||
|
loginError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default initialState;
|
export default initialState;
|
||||||
|
91
src/features/home/redux/login.js
Normal file
91
src/features/home/redux/login.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import initialState from './initialState';
|
import initialState from './initialState';
|
||||||
|
import { reducer as loginReducer } from './login';
|
||||||
|
|
||||||
const reducers = [
|
const reducers = [
|
||||||
|
loginReducer,
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function reducer(state = initialState, action) {
|
export default function reducer(state = initialState, action) {
|
||||||
|
97
tests/features/home/redux/login.test.js
Normal file
97
tests/features/home/redux/login.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user