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 15, 2019
1 parent 6e60bb7 commit bffa2d6
Show file tree
Hide file tree
Showing 23 changed files with 696 additions and 26 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
15 changes: 11 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.18.0",
"bootstrap": "^4.3.1",
"dotenv": "^7.0.0",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.12.1",
"i": "^0.3.6",
"jsonwebtoken": "^8.5.1",
"node-sass": "^4.11.0",
"prop-types": "^15.7.2",
"react": "^16.8.6",
Expand All @@ -14,7 +19,9 @@
"react-router-dom": "^5.0.0",
"react-scripts": "2.1.8",
"redux": "^4.0.1",
"redux-thunk": "^2.3.0"
"redux-mock-store": "^1.5.3",
"redux-thunk": "^2.3.0",
"sinon": "^7.3.1"
},
"scripts": {
"start": "react-scripts start",
Expand All @@ -36,13 +43,13 @@
"devDependencies": {
"coverage": "^0.1.0",
"coveralls": "^3.0.3",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.11.2",
"enzyme-to-json": "^3.3.5",
"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",
"jest-localstorage-mock": "^2.4.0",
"moxios": "^0.4.0"
},
"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;
51 changes: 36 additions & 15 deletions src/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,52 @@ import { NavLink } from 'react-router-dom';
import AuthenticationModal from './AuthenticationModal';
import store from '../store/store';
import { LOGIN, REGISTER } from '../store/actions/actionTypes';

import { isLoggedIn } from '../utils/tokenValidator';

class Header extends React.Component {

state = {
LoggedIn: isLoggedIn
}

dispatchLogin = () => {
store.dispatch({ type: LOGIN })
}

dispatchRegister = () => {
store.dispatch({ type: REGISTER })
}
render() {
const { LoggedIn } = this.state;
return (
<Navbar expand="lg" className="navbar-custom">
<NavLink exact to="/" className="brand">
Author&apos;s Haven
</NavLink>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="ml-auto">
<Nav.Item
onClick={() => store.dispatch({ type: LOGIN })}
className="nav-link auth-btn "
>
Login
</Nav.Item>
<Nav.Item
onClick={() => store.dispatch({ type: REGISTER })}
className="nav-link auth-btn get-started"
>
Get Started
</Nav.Item>
</Nav>
{LoggedIn ? (
<Nav className="ml-auto">
<Nav.Item className="nav-link profile-dropdown">
Your Profile
</Nav.Item>
</Nav>
) : (
<Nav className="ml-auto">
<Nav.Item
onClick={this.dispatchLogin}
className="nav-link auth-btn login"
>
Login
</Nav.Item>
<Nav.Item
onClick={this.dispatchRegister}
className="nav-link auth-btn get-started"
>
Get Started
</Nav.Item>
</Nav>
)}
</Navbar.Collapse>
<AuthenticationModal />
</Navbar>
Expand Down
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;
137 changes: 137 additions & 0 deletions src/containers/LoginForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React from "react";
import { Form, Button, Alert } 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 store from "../store/store";
import { REGISTER } from '../store/actions/actionTypes';

export class LoginForm extends React.Component {
static propTypes = {
email: PropTypes.string,
password: PropTypes.any,
login_user: PropTypes.object.isRequired,
loginAction: PropTypes.func.isRequired
};

constructor(props) {
super(props);
this.state = {
email: "",
password: "",
formErrors: {},
touched: false,
show: true
};
this.onAlertClose = this.onAlertClose.bind(this);
}

handleChange = event => {
this.setState({ [event.target.name]: event.target.value, touched: true });
};


onAlertClose() {
this.setState({
show: false
});
}

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

swapModal = () => {
store.dispatch({ type: REGISTER })
}

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

return (
<div>
<Form className="main-container" onSubmit={this.handleSubmit}>
<Form.Group name="email">
{errors && (
<Alert
dismissible
variant="danger"
show={this.state.show}
onClick={this.onAlertClose}
>
<Alert.Heading>
<small>{errors}</small>
</Alert.Heading>
</Alert>
)}
<Form.Label htmlFor="email">Email address</Form.Label>
<div className="text-danger email-warning">
{(formErrors[0] === "email" && !touched) && formErrors[1]}
</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" && !touched) && formErrors[1]}
</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" className="btn-one">
Sign In
</Button>
<p className="swap-modal-text">Don't have an account? <span className="swap-modal-span" onClick={this.swapModal}>Click here</span> to register.</p>
</Form>
</div>
);
}
}

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

export default connect(
mapStateToProps,
{ loginAction }
)(LoginForm);
34 changes: 34 additions & 0 deletions src/css/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,40 @@ body {
font-weight: 500;
}

.swap-modal-span {
cursor: pointer;
text-decoration: chocolate;
color: chocolate;
}

.swap-modal-span:hover {
opacity: 0.8;
text-decoration: underline;
}

.swap-modal-text {
font-size: 14px;
}

.modal-dialog {
height: 100%;
}
.red-form-border {
border: 1px solid red;
}

.password-input {
margin-top: 0;
}

.btn-one{
background: #792525;
border: none;
color: #fcc810;
}
.btn-one:hover{
opacity: 0.7;
background: #792525;
border: none;
color: #fcc810;
}
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/setupTests.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

require('jest-localstorage-mock');

configure({ adapter: new Adapter() });
2 changes: 2 additions & 0 deletions src/store/actions/actionTypes.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const SHOW_MODAL = 'SHOW_MODAL';
export const REGISTER = 'REGISTER';
export const LOGIN = 'LOGIN';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAIL = 'LOGIN_FAIL';
29 changes: 29 additions & 0 deletions src/store/actions/authActions/LoginAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import axiosConfig from '../../../axiosConfig';
import { LOGIN_SUCCESS, LOGIN_FAIL } from '../actionTypes';
import ShowModal from '../changeFormAction';

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(ShowModal({
modalShow: false,
}));
dispatch({
type: LOGIN_SUCCESS,
});
})
.catch((error) => {
dispatch({
type: LOGIN_FAIL,
payload: error.response.data.errors.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;
Loading

0 comments on commit bffa2d6

Please sign in to comment.