Skip to content

Commit

Permalink
feat(login) users can log in
Browse files Browse the repository at this point in the history
- create login form
- users can log in
- errors are displayed above the specific input fields and input fields have a red border if they are not validated
- set up validation of input fields
- configure axios
- test loginReducer
- test login validator function
- tokens are stored in localStorage on successful login
- add function for validating authentication tokens
- add function for checking whether a user is logged in with valid jwt token
- users are redirected on successful login to the homepage
- logged in users cannot see the login form
- test login form
[starts #164046210]
  • Loading branch information
MandelaK committed Apr 12, 2019
1 parent 0394c0c commit 289295b
Show file tree
Hide file tree
Showing 24 changed files with 568 additions and 20,905 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
Expand Down
20,890 changes: 0 additions & 20,890 deletions package-lock.json

This file was deleted.

13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.18.0",
"bootstrap": "^4.3.1",
"dotenv": "^7.0.0",
"i": "^0.3.6",
"jsonwebtoken": "^8.5.1",
"node-sass": "^4.11.0",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-bootstrap": "^1.0.0-beta.6",
"react-dom": "^16.8.6",
"react-redux": "^6.0.1",
"react-router-dom": "^5.0.0",
"react-scripts": "2.1.8",
"redux": "^4.0.1",
"redux-mock-store": "^1.5.3",
"redux-thunk": "^2.3.0"
},
"scripts": {
"start": "react-scripts start",
"start": "sh -ac '. .env; react-scripts start'",
"build": "react-scripts build",
"test": "react-scripts test",
"test": "sh -ac '. .env; react-scripts test'",
"coverage": "react-scripts test -u --env=jsdom --coverage --coveragePathIgnorePatterns=src/index.jsx --coveragePathIgnorePatterns=src/serviceWorker.js",
"coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls",
"eject": "react-scripts eject"
Expand All @@ -41,7 +46,9 @@
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-react": "^7.12.4"
"eslint-plugin-react": "^7.12.4",
"moxios": "^0.4.0",
"react-test-renderer": "^16.8.6"
},
"jest": {
"snapshotSerializers": [
Expand Down
11 changes: 11 additions & 0 deletions src/axiosConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import axios from 'axios';

const { REACT_APP_BASE_URL } = process.env;

const axiosConfig = axios.create({
baseURL: REACT_APP_BASE_URL,
headers: {
'content-type': 'application/json',
},
});
export default axiosConfig;
3 changes: 2 additions & 1 deletion src/components/Login.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import LoginForm from '../containers/LoginForm';

const Login = () => <div className="main-container">Login</div>;
const Login = () => <div className="main-container"><LoginForm /></div>;
export default Login;
115 changes: 115 additions & 0 deletions src/containers/LoginForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from "react";
import { Form, Button } from "react-bootstrap";
import PropTypes from "prop-types";
import loginAction from "../store/actions/authActions/LoginAction";
import { connect } from "react-redux";
import validateLoginForm from "../utils/validation";
import { Redirect } from "react-router-dom";

export class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
email: "",
password: "",
formErrors: {}
};
}
handleChange = event => {
this.setState({ [event.target.name]: event.target.value });
};

handleSubmit = event => {
event.preventDefault();
const formErrors = validateLoginForm(this.state.email, this.state.password);
if (formErrors.length > 0) {
this.setState({
formErrors: formErrors
});
return null;
} else {
this.setState({
formErrors: []
});
}
const { email, password } = this.state;
const data = { email: email, password: password};
this.props.loginAction(data);
};

render() {
const { login_user } = this.props;
const { errors } = login_user;
const { formErrors } = this.state;

return (
<div>
{login_user.logged_in ? (
<Redirect to="/" />
) : (
<Form className="container" onSubmit={this.handleSubmit}>
<h4 className="text-center">Sign In</h4>
<Form.Group name="email">
<p className="text-danger general-warning">
{errors ? errors : null}
</p>
<Form.Label htmlFor="email">Email address</Form.Label>
<div className="text-danger email-warning">
{formErrors[0] === "email" ? formErrors[1] : null}
</div>
<p className="text-danger email-warning">{errors.email}</p>
<Form.Control
className={
formErrors[0] === "email" ? "red-form-border" : "email-input"
}
name="email"
type="email"
placeholder="Enter your email"
onChange={this.handleChange}
/>
</Form.Group>
<Form.Group name="password">
<Form.Label htmlFor="password">Password</Form.Label>
<div className="text-danger password-warning">
{formErrors[0] === "password" ? formErrors[1] : null}
</div>
<p className="text-danger password-warning">{errors.password}</p>
<Form.Control
className={
formErrors[0] === "password"
? "red-form-border"
: "password-input"
}
name="password"
type="password"
placeholder="Password"
onChange={this.handleChange}
/>
</Form.Group>
<Button variant="primary" type="submit">
Sign In
</Button>
</Form>
)}
</div>
);
}
}

LoginForm.propTypes = {
email: PropTypes.string,
password: PropTypes.any,
login_user: PropTypes.object.isRequired,
loginAction: PropTypes.func.isRequired
};

export const mapStateToProps = state => {
return {
login_user: state.login_user
};
};

export default connect(
mapStateToProps,
{ loginAction }
)(LoginForm);
3 changes: 3 additions & 0 deletions src/css/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ body {
text-align: right;
padding: 10px;
}
.red-form-border {
border: 1px solid red;
}
2 changes: 2 additions & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Routes from './routes';
import * as serviceWorker from './serviceWorker';
import store from './store/store';

require('dotenv').config();

ReactDOM.render(
<Provider store={store}>
<Routes />
Expand Down
2 changes: 2 additions & 0 deletions src/store/actions/actionTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAIL = 'LOGIN_FAIL';
2 changes: 2 additions & 0 deletions src/store/actions/actionTypes.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAIL = 'LOGIN_FAIL';
25 changes: 25 additions & 0 deletions src/store/actions/authActions/LoginAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import axiosConfig from '../../../axiosConfig';
import { LOGIN_SUCCESS, LOGIN_FAIL } from '../actionTypes';

const loginAction = loginData => dispatch => axiosConfig.request(
{
method: 'post',
url: '/user/login/',
data: { user: loginData },
},
)
.then((response) => {
const { token } = response.data.user;
window.localStorage.setItem('token', token);
dispatch({
type: LOGIN_SUCCESS,
});
})
.catch((error) => {
dispatch({
type: LOGIN_FAIL,
payload: error.response.data.errors.error,
});
});

export default loginAction;
29 changes: 29 additions & 0 deletions src/store/actions/authActions/LoginAction.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { LOGIN_SUCCESS, LOGIN_FAIL } from '../actionTypes';

const { REACT_APP_BASE_URL } = process.env;

const loginAction = loginData => dispatch => fetch('http://localhost:8000/api/user/login/', {
mode: 'cors',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(loginData),
})
.then(res => res.json())
.then((data) => {
if (data.errors) {
dispatch({
type: LOGIN_FAIL,
payload: data.errors,
});
} else {
dispatch({
type: LOGIN_SUCCESS,
payload: data.user,
});
}
})
.catch(error => error);

export default loginAction;
28 changes: 28 additions & 0 deletions src/store/reducers/LoginReducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { LOGIN_SUCCESS, LOGIN_FAIL } from '../actions/actionTypes';
import { isLoggedIn } from '../../utils/tokenValidator';

const initialState = {
logged_in: !!isLoggedIn,
errors: '',
};

const loginReducer = (state = initialState, action) => {
switch (action.type) {
case LOGIN_SUCCESS:
return {
...state,
logged_in: true,
errors: '',
};
case LOGIN_FAIL:
return {
...state,
logged_in: false,
errors: action.payload,
};
default:
return state;
}
};

export default loginReducer;
16 changes: 5 additions & 11 deletions src/store/reducers/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
const initialState = {
articles: [],
notifications: [],
followers: [],
}
import { combineReducers } from 'redux';
import loginReducer from './LoginReducer';

function rootReducer(state = initialState, action){

return state;
};

export default rootReducer;
export default combineReducers({
login_user: loginReducer,
});
10 changes: 10 additions & 0 deletions src/tests/components/Login.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from "react";
import { shallow } from "enzyme";
import Login from "../../components/Login";

describe("Login", () => {
it("Should render correctly", () => {
const wrapper = shallow(<Login />);
expect(wrapper).toMatchSnapshot;
});
});
Loading

0 comments on commit 289295b

Please sign in to comment.