Skip to content

Commit

Permalink
Added basic authentication
Browse files Browse the repository at this point in the history
For now I'm not going to use fancier validation techniques or add in
"forgotten password" functionality. This is my first full-stack app and
I can add that stuff later or in another project once I'm ready.
  • Loading branch information
SirIsaacNeutron committed Sep 4, 2020
1 parent 2e09ecd commit 3e28d88
Show file tree
Hide file tree
Showing 13 changed files with 648 additions and 5 deletions.
109 changes: 109 additions & 0 deletions client/src/actions/authActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import axios from 'axios';

import { getErrors } from './errorActions';

import {
USER_LOADING,
USER_LOADED,
AUTH_ERROR,
LOGIN_SUCCESS,
LOGIN_FAIL,
LOGOUT_SUCCESS,
REGISTER_SUCCESS,
REGISTER_FAIL
} from '../actions/types';

export const loadUser = () => (dispatch, getState) => {
dispatch({ type: USER_LOADING });

axios
.get('http://localhost:5000/api/auth/user', tokenConfig(getState))
.then(res => {
dispatch({
type: USER_LOADED,
payload: res.data
})
})
.catch(err => {
dispatch(getErrors(err.response.data, err.response.status));
dispatch({
type: AUTH_ERROR
})
})
}

export const register = ({ name, email, password }) => dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
}

// Request body
const body = JSON.stringify({ name, email, password });

axios
.post('http://localhost:5000/api/users', body, config)
.then(res => {
dispatch({
type: REGISTER_SUCCESS,
payload: res.data
})
})
.catch(err => {
dispatch(getErrors(err.response.data, err.response.status, REGISTER_FAIL));
dispatch({
type: REGISTER_FAIL
})
})
}

export const login = ({ email, password }) => dispatch => {
// Headers
const config = {
headers: {
'Content-Type': 'application/json'
}
}

// Request body
const body = JSON.stringify({ email, password });

axios
.post('http://localhost:5000/api/auth', body, config)
.then(res => {
dispatch({
type: LOGIN_SUCCESS,
payload: res.data
})
})
.catch(err => {
dispatch(getErrors(err.response.data, err.response.status, LOGIN_FAIL));
dispatch({
type: LOGIN_FAIL
})
})
}

export const logout = () => {
return {
type: LOGOUT_SUCCESS
}
}

export const tokenConfig = getState => {
// Get token from local storage
const token = getState().auth.token;

const config = {
headers: {
"Content-Type": "application/json"
}
}

if (token) {
config.headers['x-auth-token'] = token;
}

return config
}
11 changes: 10 additions & 1 deletion client/src/actions/types.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
export const GET_ERRORS = 'GET_ERRORS';
export const CLEAR_ERRORS = 'CLEAR_ERRORS';
export const CLEAR_ERRORS = 'CLEAR_ERRORS';

export const USER_LOADING = 'USER_LOADING';
export const USER_LOADED = 'USER_LOADED';
export const AUTH_ERROR = 'AUTH_ERROR';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAIL = 'LOGIN_FAIL';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const REGISTER_SUCCESS = 'REGISTER_SUCCESS';
export const REGISTER_FAIL = 'REGISTER_FAIL';
49 changes: 46 additions & 3 deletions client/src/components/AppNavbar.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import {
Collapse,
Navbar,
Expand All @@ -13,8 +13,14 @@ import {
// how to use React Router with reactstrap
import { NavLink as RRNavLink } from 'react-router-dom';

import { connect } from 'react-redux';

import PropTypes from 'prop-types';

import RegisterModal from './auth/RegisterModal';
import LoginModal from './auth/LoginModal';
import Logout from './auth/Logout';

class AppNavbar extends React.Component {
state = {
isOpen: false // for the collapsible menu
Expand All @@ -27,6 +33,33 @@ class AppNavbar extends React.Component {
}

render() {
const { isAuthenticated, user } = this.props.auth;

const guestLinks = (
<>
<NavItem>
<RegisterModal />
</NavItem>
<NavItem>
<LoginModal />
</NavItem>
</>
);

const authLinks = (
<>
<NavItem>
<span className="navbar-text mr-3">
<strong>{user ? `Welcome, ${user.name}` : ''}</strong>
</span>
</NavItem>
<NavItem>
<Logout />
</NavItem>
</>
);


return (
<div>
<Navbar color="dark" dark expand="sm" className="mb-3">
Expand All @@ -35,7 +68,9 @@ class AppNavbar extends React.Component {
<NavbarToggler onClick={this.toggle} />
<Collapse isOpen={this.state.isOpen} navbar>
<Nav className="ml-auto" navbar>

<NavItem>
{ isAuthenticated ? authLinks : guestLinks }
</NavItem>
</Nav>
</Collapse>
</Container>
Expand All @@ -45,4 +80,12 @@ class AppNavbar extends React.Component {
}
}

export default AppNavbar;
AppNavbar.propTypes = {
auth: PropTypes.object.isRequired
}

const mapStateToProps = state => ({
auth: state.auth // from rootReducer
});

export default connect(mapStateToProps, null)(AppNavbar);
139 changes: 139 additions & 0 deletions client/src/components/auth/LoginModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import React from 'react';
import {
Button,
Modal,
ModalHeader,
ModalBody,
Form,
FormGroup,
Label,
Input,
NavLink,
Alert
} from 'reactstrap';

import PropTypes from 'prop-types';

import { connect } from 'react-redux';

import { login } from '../../actions/authActions';
import { clearErrors } from '../../actions/errorActions';

import { LOGIN_FAIL } from '../../actions/types';

class LoginModal extends React.Component {
state = {
modal: false,
email: '',
password: '',
msg: null
}

componentDidUpdate(prevProps) {
const { error, isAuthenticated } = this.props;
if (error !== prevProps.error) {
if (error.id === LOGIN_FAIL) {
this.setState({
msg: error.msg.msg
});
}
else {
this.setState({ msg: null })
}
}

// If authenticated, close modal
if (this.state.modal) {
if (isAuthenticated) {
this.toggle();
}
}
}

toggle = () => {
// clearErrors() prevents the previous error message from showing up
// when the modal is re-opened
this.props.clearErrors();
this.setState({
modal: !this.state.modal
});
}
// Using [e.target.name] allows onChange to be used with multiple state vars
onChange = e => {
this.setState({
[e.target.name]: e.target.value
})
}

onSubmit = e => {
e.preventDefault();

const { email, password } = this.state;

const user = {
email,
password
}

this.props.login(user);
}

render() {
return (
<div>
<NavLink onClick={this.toggle} href='#'>Login</NavLink>

<Modal
isOpen={this.state.modal}
toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>Login</ModalHeader>
<ModalBody>
{ this.state.msg ? <Alert color="danger">{this.state.msg}</Alert> : null }
<Form onSubmit={this.onSubmit}>
<FormGroup>
<Label htmlFor="name">Email</Label>
<Input
className="mb-3"
type="email"
name="email"
id="email"
placeholder="Email"
onChange={this.onChange} />

<Label htmlFor="password">Password</Label>
<Input
className="mb-3"
type="password"
name="password"
id="password"
placeholder="Password"
onChange={this.onChange} />

<Button
color="dark"
className="mt-3"
block>Login</Button>
</FormGroup>
</Form>
</ModalBody>
</Modal>
</div>
);
}
}

LoginModal.propTypes = {
isAuthenticated: PropTypes.bool,
error: PropTypes.object.isRequired,
login: PropTypes.func.isRequired,
clearErrors: PropTypes.func.isRequired
}

const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated, // from authReducer
error: state.error
});

export default connect(
mapStateToProps,
{ login, clearErrors })(LoginModal);
22 changes: 22 additions & 0 deletions client/src/components/auth/Logout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types'
import { connect } from 'react-redux';

import { logout } from '../../actions/authActions';
import { NavLink } from 'reactstrap';

class Logout extends React.Component {
render() {
return (
<>
<NavLink onClick={this.props.logout} href='#'>Logout</NavLink>
</>
);
}
}

Logout.propTypes = {
logout: PropTypes.func.isRequired
}

export default connect(null, { logout })(Logout);
Loading

0 comments on commit 3e28d88

Please sign in to comment.