I've decided to use Passport (Local strategy) for authentication on my React app (First time trying it). I got it working to the point where the user can create a new account, then log in and then it is redirected to the protected page. So far, it looks good, I can also logout with no issues. However, when I try to access the protected route without entering the right credentials, or when going to the protected route manually, the whole app breaks and I get a React Error: TypeError: clients.map is not a function, which refers to the code I use in the protected route to get and display data from the database.
To me, it looks like React is trying to render the component first without having the data from the database and therefore, the error? Because if I remove the authentication part the component renders just fine. But at the same time, I'm new to Passport, so I'm not sure if it has something to do with the passport configuration itself.
Users.js api routes
const express = require('express');
const router = express.Router();
const passport = require('passport');
const bcrypt = require('bcryptjs');
const User = require('../models').User;
const salt = bcrypt.genSaltSync(10);
// Register
router.post('/signup', (req, res) => {
console.log(req.body);
const { username, email, password, password2 } = req.body;
if (!username || !email || !password || !password2) {
throw 'Please enter all fields';
}
if (password != password2) {
throw 'Passwords do not match'
};
if (password.length < 6) {
throw 'Password must be at least 6 characters';
}
else {
User.findOne({
where: {
email
}
}).then(user => {
if (user) {
res.send("Email already exists!")
} else {
const encryptedPassword = bcrypt.hashSync(password, salt);
let newUser = {
username,
email,
password: encryptedPassword
};
User.create(newUser)
.then(() => {
delete newUser.password;
res.send(newUser)
})
.catch(function (err) {
console.log(err);
res.json(err);
});
}
});
}
});
// Login
router.post('/login', (req, res, next) => {
const { email, password } = req.body;
if (!email || !password) {
throw 'Please enter all fields';
}
if (email === null || password === null) {
throw 'Please enter the right credentials';
}
passport.authenticate('local', {
successRedirect: '/admin',
failureRedirect: '/auth/login',
failureFlash: true
})(req, res, next);
});
// Logout
router.get('/logout', function (req, res) {
req.logOut();
req.session.destroy(function (err) {
res.redirect('/auth/login');
});
});
module.exports = router;
Passport.js passport config
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcryptjs');
const User = require('../models').User;
module.exports = function(passport) {
passport.use(new LocalStrategy(
// Our user will sign in using an email, rather than a "username"
{
usernameField: "email"
},
function (email, password, done) {
// When a user tries to sign in this code runs
User.findOne({
where: {
email: email
}
})
.then(user => {
if (!user) {
return done(null, false, { message: 'No user found
under those credentials' });
}
bcrypt.compare(password, user.password, (err, isMatch) => {
// if (err) throw err;
if (err) {
return done(err, null)
}
if (isMatch) {
return done(null, user);
} else {
return done(null, { message: 'Email or Password not
valid' });
}
});
})
}
));
passport.serializeUser(function (user, done) {
done(null, user.id);
});
passport.deserializeUser(function (id, done) {
User.findOne({
where: {
id: id
}
}).then(function (user) {
if (user) {
done(null, user.get());
} else {
done(user.errors, null);
}
});
});
};
And finally, my react component that is rendered properly when authorized, but breaks when there's no authorization:
import React, { Component } from 'react';
import API from '../../utils/API';
import { Link } from 'react-router-dom';
import {
Button,
Modal,
ModalHeader,
ModalBody,
Form,
FormGroup,
Input
} from 'reactstrap';
import './style.css';
import Axios from 'axios';
class AdminComp extends Component {
state = {
clients: [],
lastName: '',
firstName: '',
phone: '',
petName: '',
breed: '',
notes: '',
modal: false,
clientSearch: ''
}
componentDidMount() {
this.getAllClients()
}
getAllClients = () => {
API.getClients()
.then(res => {
if (res.data.status === "error") {
throw new Error(res.data.message);
}
this.setState({ clients: res.data })
}
)
.catch(err => console.log(err));
};
//Logout User
handleLogOut(e) {
e.preventDefault();
Axios.get("/auth/logout")
.then(response => {
if (response.data.status === "error") {
throw new Error(response.data.message);
}
window.location.href = "/auth/login"
console.log("logged out", response.data)
})
.catch(err => {
window.location.href = "/auth/login"
console.log(err)
})
}
//Modal Functions
toggle = () => {
this.setState({
modal: !this.state.modal
});
}
onSubmitModal = e => {
e.preventDefault();
if (!this.state.clientSearch || isNaN(this.state.clientSearch)
) {
return;
}
this.getSingleClient();
this.toggle();
}
onChangeModal = (e) => {
this.setState({ [e.target.name]: e.target.value });
};
getSingleClient = () => {
let clientSearchValue = this.state.clientSearch;
API.getClient(clientSearchValue)
.then(res => {
if (res.data) {
this.setState({
clientSearch: res.data
}, () => console.log(this.state.clientSearch))
} else {
this.setState({
modal: false
})
alert("Client ID number does not exist, please try again")
}
})
.catch(error => console.log(error))
}
handleChange = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
handleDeleteClient = id => {
API.deleteClient(id)
.then(alert("Client with Id number: " + id + " has been
successfully deleted!"))
.then(res => this.getAllClients())
.catch(err => console.log(err));
}
handleFormSubmit = (e) => {
e.preventDefault();
if (!this.state.lastName ||
!this.state.firstName ||
!this.state.phone ||
!this.state.petName ||
!this.state.breed ||
!this.state.notes) {
return;
}
API.addClient({
lastName: this.state.lastName.toLowerCase(),
firstName: this.state.firstName.toLowerCase(),
phone: this.state.phone.toLowerCase(),
petName: this.state.petName.toLowerCase(),
breed: this.state.breed.toLowerCase(),
notes: this.state.notes.toLowerCase()
})
.then(alert("New Client added to list!"))
.then(this.setState({ petName: "", breed: "", notes: "", lastName: "", firstName: "", phone: "" }))
.then(res => this.getAllClients())
.catch(err => console.log(err));
};
render() {
const clients = this.state.clients;
const clientsList = clients.length ? (
clients.map(client => {
return (
<div key={client.id}>
<div className="card-content">
<table style={{ width: "100%", tableLayout: "fixed", border: "1px", background: "white" }}>
<tbody>
<tr>
<td style={{ width: "50px", textAlign: "center" }}>{client.id}</td>
<td style={{ width: "100px", textAlign: "center" }}>{client.lastName}</td>
<td style={{ width: "100px", textAlign: "center" }}>{client.firstName}</td>
<td style={{ width: "100px", textAlign: "center" }}>{client.phone}</td>
<td style={{ width: "160px", textAlign: "center" }}>{client.petName}</td>
<td style={{ width: "120px", textAlign: "center" }}>{client.breed}</td>
<td style={{ width: "367px", textAlign: "center" }}>{client.notes}</td>
<td><Link style={{ width: "70px", border: "1px solid white" }} className="btn btn-info" to={'api/clients/' + client.id}>Edit
</Link>
<button style={{ background: "red", color: "white", width: "70px" }} className="btn btn-warning" onClick={(e) => { if (window.confirm(`Are you sure you wish to delete ${client.firstName} ${client.lastName} permanently?`)) this.handleDeleteClient(client.id) }}>
Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
)
})
) : (
<div >No clients in database</div>
);
return (
<div className="container">
<div className="row">
<div className="col-md-12">
<hr style={{ background: "white" }}></hr>
<h1 style={{ textAlign: 'center' }}><b>Welcome to the Admin Panel Paola</b></h1>
<button onClick={this.handleLogOut}>Logout</button>
<Link to={"/auth/signup"}><button>Add an employee</button></Link>
<hr style={{ background: "white" }}></hr>
</div>
</div>
<div className="row">
<div className="col-md-4 bg-dark" style={{
color: 'white',
marginBottom: '30px',
padding: '15px',
textAlign: 'center',
border: '1px solid white'
}}>
<h3>Search for a Client</h3>
<Form onSubmit={this.onSubmitModal}>
<FormGroup>
<Input
type="text"
name="clientSearch"
id="clientSearch"
placeholder="Enter Client ID Number"
onChange={this.onChangeModal}
></Input>
<Button
color="info"
style={{ marginTop: '1rem' }}
block>
Submit
</Button>
</FormGroup>
</Form>
<Modal
isOpen={this.state.modal}
toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>These Records were found</ModalHeader>
<ModalBody>
<table>
<tbody>
<tr style={{ textAlign: 'center', padding: '7px' }}>
<th>ID</th>
<th>Last Name</th>
<th>First Name</th>
<th>Phone</th>
<th>Pet Name</th>
<th>Breed</th>
<th>Notes</th>
</tr>
<tr style={{ textAlign: 'center', padding: '7px' }}>
<td>{this.state.clientSearch.id}</td>
<td>{this.state.clientSearch.lastName}</td>
<td>{this.state.clientSearch.firstName}</td>
<td>{this.state.clientSearch.phone}</td>
<td>{this.state.clientSearch.petName}</td>
<td>{this.state.clientSearch.breed}</td>
<td>{this.state.clientSearch.notes}</td>
<td><Link style={{ width: "60px", border: "1px solid white" }} className="btn btn-info" to={'api/clients/' + this.state.clientSearch.id}>Edit
</Link>
</td>
</tr>
</tbody>
</table>
</ModalBody>
</Modal>
</div>
<div className="col-md-8 bg-dark" style={{ border: '1px solid white', color: 'white', marginBottom: "30px" }}>
<form className="white" onSubmit={this.handleFormSubmit.bind(this)} style={{ marginBottom: "50px" }}>
<h2 className="grey-text text-darken-3" style={{ textAlign: "center", marginTop: "15px" }}>Add a New Client</h2>
<p>* Fields required</p>
<hr style={{ background: "white" }}></hr>
<div className="input-field">
<label htmlFor="lastName">* Last Name</label>
<input type="text" id='lastName' value={this.state.lastName} onChange={this.handleChange} />
</div>
<div className="input-field">
<label htmlFor="firstName">* First Name</label>
<input type="text" id='firstName' value={this.state.firstName} onChange={this.handleChange} />
</div>
<div className="input-field">
<label htmlFor="phone">* Phone</label>
<input type="text" id='phone' value={this.state.phone} onChange={this.handleChange} />
</div>
<div className="input-field">
<label htmlFor="petName">* Pet Name</label>
<input type="text" id='petName' value={this.state.petName} onChange={this.handleChange} />
</div>
<div className="input-field">
<label htmlFor="breed">* Breed</label>
<input type="text" id='breed' value={this.state.breed} onChange={this.handleChange} />
</div>
<div className="input-field">
<label htmlFor="notes">Notes</label>
<input type="text" id='notes' value={this.state.notes} onChange={this.handleChange} />
</div>
<div className="input-field">
<button className="btn-primary lighten-1 z-depth-0" onClick={this.handleFormSubmit}
>Add Client</button>
</div>
</form>
</div>
<div className="row">
<div className="col-md-12">
<div style={{ background: "white", paddingTop: "12px", marginBottom: "100px" }}>
<h6><b>
<span style={{ border: "1px solid", padding: "8px 17px 8px 17px" }}>Id</span>
<span style={{ border: "1px solid", padding: "8px 11px 8px 12px" }}>Last Name</span>
<span style={{ border: "1px solid", padding: "8px 8px 8px 8px" }}> First Name</span>
<span style={{ border: "1px solid", padding: "8px 27px 8px 25px" }}>Phone</span>
<span style={{ border: "1px solid", padding: "8px 44px 8px 44px" }}>Pet Name</span>
<span style={{ border: "1px solid", padding: "8px 37px 8px 38px" }}>Breed</span>
<span style={{ border: "1px solid", padding: "8px 200px 8px 200px", background: "white" }}>Notes / Actions</span>
</b></h6>
{clientsList}
</div>
</div>
</div>
</div>
</div >
)
}
}
export default AdminComp;
This is also how I protected the route:
// Requiring our models
var db = require("../models");
// const passport = require('../passport');
const { ensureAuthenticated } = require('../passport/auth');
module.exports = function (app) {
//Authentication Routes
app.get("/auth/signup", ensureAuthenticated, (req, res) => {
res.send("Passed!")
})
app.get("/admin", ensureAuthenticated, (req, res) => {
res.send("Admin Passed!")
})
//Client routes
app.get("/api/clients", ensureAuthenticated, (req, res) => {
db.Client.findAll({}).then(function (dbClient) {
res.json(dbClient);
});
});
}:
Auth.js file:
module.exports = {
ensureAuthenticated: function(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
req.flash('error_msg', 'Please log in to view that resource');
res.redirect('/auth/login');
},
forwardAuthenticated: function(req, res, next) {
if (!req.isAuthenticated()) {
return next();
}
res.redirect('/admin');
}
};
Thanks a lot in advance
I made some adjustments to the /login route to make it work:
// Login router.post("/login", function (req, res, next) {
// generate the authenticate method and pass the req/res
passport.authenticate('local', function (err, user, info) {
if (err) {
return res.status(401).json(err);
}
if (!user) {
return res.status(401).json(info);
}
req.logIn(user, function (err) {
if (err) { return next(err); }
return res.send(user);
});
})(req, res, next);
});