The purpose of this project is to implement secure password authentication using the Bcrypt library. Bcrypt is used to salt and hash user passwords, so that we avoid storing plain-text passwords in the database. We will also practice front to back data transmission, from axios calls on the front-end, to express endpoints on the server, to SQL commands in the database.
fork
andclone
this repositorycd
into the root of the project- Run
npm install
- Run
npm start
At this point the app should load in the browser and you should see the view of three treasure hoards and the login bar at the top. None of the buttons are connected to functions at this point.
Here we will set up the server and get it listening on a port. We will install and require the necessary npm packages in order to do this.
Here we will create the server file.
- Create a folder called
server
on the root. - Create a file called
index.js
in the server folder. - Install and require the following packages and store them as
const
variables- express
- express-session
- massive
- dotenv
- When you require dotenv, immediately invoke the config method from this module.
- Define a const variable called
app
equal to express invoked. - Define a const variable called
PORT
equal to 4000 - Use the json method of the express package as top level middleware.
- Make the server listen on the previously mentioned port number using
app.listen
- Run
nodemon
and make sure there are no bugs.
server/index.js
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const massive = require('massive');
const PORT = 4000;
const app = express()
app.use(express.json());
app.listen(PORT, ()=>console.log(`Listening on port ${PORT}`))
Here we will connect the server to the database using massive. We will create a .env file on the root of the project to store our personal data such as the database connection string. We will also set up session top-level middleware.
Here we will create a .env file to store the database connection string.
- Create a file in the root directory called
.env
- Store a value here called
CONNECTION_STRING
, the value should come from your heroku db settings. - Store a value here called
SESSION_SECRET
and make up any value for this. - In
index.js
, destructureCONNECTION_STRING
andSESSION_SECRET
fromprocess.env
, storing it on aconst
variable. - Create the database connection by invoking massive and passing in the
CONNECTION_STRING
. - Add a
.then
on the massive invocation passing in a function, and store the resulting database connection usingapp.set
. - Set up session as top-level middleware by invoking
app.use
and passing in session invoked with a configuration object.- The session configuration object should have properties
resave
set to true,saveUninitialized
set to false, andsecret
set toSESSION_SECRET
.
- The session configuration object should have properties
- Take the contents of
seed.sql
in the DB folder and run that in your database using SQL tabs. This will create the necessary user and treasure tables.
.env
CONNECTION_STRING=string_from_heroku_db
SESSION_SECRET=asdfa12341234
server/index.js
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const massive = require('massive');
const PORT = 4000;
const { SESSION_SECRET, CONNECTION_STRING } = process.env;
const app = express();
app.use(express.json());
massive({
connectionString: CONNECTION_STRING,
ssl: { rejectUnauthorized: false }
}).then(db => {
app.set('db', db);
console.log('db connected');
});
app.use(
session({
resave: true,
saveUninitialized: false,
secret: SESSION_SECRET,
})
);
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));
Here we will create an auth controller file and import the bcryptjs package. We will also build the server endpoint that we will use to register a new user.
-
Create a folder called
controllers
in your server directory. -
Create a file called
authController.js
in your controllers directory. -
Open
server/index.js
and require theauthController.js
file storing it on a const variable called authCtrl. -
In
server/index.js
we will create the register endpoint. -
Create a POST endpoint with '/auth/register' as the URL and authCtrl.register as the controller function.
-
Install
bcryptjs
. -
Go back to authController.js and require
bcryptjs
as a variable calledbcrypt
. -
Next, create a register method with parameters req and res. We will use
async
andawait
, so make sure to use theasync
keyword before the function.- Destructure username, password and isAdmin from req.body.
- Get the database instance and run the sql file
get_user
, passing in username. This query will check the database to see if the username is already taken. Since this query is asynchronous, make sure to use theawait
keyword to ensure that the promise resolves before the rest of the code executes. - Set the value of this SQL query to a variable called
result
.- Remember that SQL queries come back in an array, so take the first item of the array and set it to another const variable called
existingUser
.
- Remember that SQL queries come back in an array, so take the first item of the array and set it to another const variable called
- If
existingUser
is defined, send a response with status 409 and the text 'Username taken'); - Otherwise, create a const variable called salt, equal to
bcrypt.genSaltSync(10)
. - Create a const variable called
hash
, equal tobcrypt.hashSync(password, salt)
. - Asynchronously (using await) run the
register_user
SQL file, passing in isAdmin, username, and hash as parameters (in that order). - Store the result to a variable called
registeredUser
. - Store the first item of the
registeredUser
array to a variable calleduser
.- This is our newly created user object.
- Set req.session.user to be an object with properties isAdmin, id, and username, equal to user.is_admin, user.id, and user.username.
- Do a
res.status
andsend
with the status being201
for created and send the user object on session we just created.
-
Now let's test our endpoint with postman.
- Open postman and enter
http://localhost:4000/auth/register
in the URL input and send the following as raw JSON on the body of your request:
- Open postman and enter
Postman JSON Body - Register
{
"username": "mrsmee",
"password": "1stM84Lyfe",
"isAdmin": false
}
You should receive the following as a response if your request was successful. If nothing went wrong, we have now registered a non-admin user and you should be able to see the new user in the users
table of your database.
Postman JSON Response - Register
{
"isAdmin": false,
"username": "mrsmee",
"id": 4
}
server/controllers/authController.js
const bcrypt = require('bcryptjs');
module.exports = {
register: async (req, res) => {
const { username, password, isAdmin } = req.body;
const db = req.app.get('db');
const result = await db.get_user([username]);
const existingUser = result[0];
if (existingUser) {
return res.status(409).send('Username taken');
}
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync(password, salt);
const registeredUser = await db.register_user([isAdmin, username, hash]);
const user = registeredUser[0];
req.session.user = { isAdmin: user.is_admin, username: user.username, id: user.id };
return res.status(201).send(req.session.user);
},
};
server/index.js
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const massive = require('massive');
const authCtrl = require('./controllers/authController');
const PORT = 4000;
const { SESSION_SECRET, CONNECTION_STRING } = process.env;
const app = express();
app.use(express.json());
massive({
connectionString: CONNECTION_STRING,
ssl: { rejectUnauthorized: false }
}).then(db => {
app.set('db', db);
console.log('db connected');
});
app.use(
session({
resave: true,
saveUninitialized: false,
secret: SESSION_SECRET,
})
);
app.post('/auth/register', authCtrl.register);
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));
In this step, we will add logic to our front end that will interact with the register endpoint that we just created. By the end of this step, a new user will be able to register for the application using the form inputs in the header.
Since the user object is stored on state in App.js
, but the input boxes and the methods that update state with their value are stored in Header.js
, we will make the HTTP request in Header.js
and call a method passed as a prop from App.js
with the response from the request as an argument.
- Open
Header.js
- Install and import axios.
- In the
register
method, use axios to send a POST request to/auth/register
.- Destructure
username
,password
, andisAdmin
properties from state. - Send along an object with
username
,password
, andisAdmin
properties with the correct values from state as the body of the request. - In the .then of the axios request, set username and password on state to empty strings, using setState.
- Also in the .then, invoke this.props.updateUser passing in the response data from our request, so that we can update the user object on App.js.
- Chain a
.catch
onto the.then
method with a callback function that contains the error as a parameter, andalert
theresponse.request.response
. Unfortunately, that path is the only way to get access to the error string that we sent as a response if the status code is not a 201. Dont forget to clear the input boxes usingsetState
as well.
- Destructure
.catch(err => {
this.setState({ username: '', password: '' })
alert(err.response.request.response)
})
- Test your application by entering a username and a password and clicking the register button. The header should switch to display a welcome message and a logout button. The logout button shouldn't work yet, so to test the error handling of our endpoint, refresh the page to get the input boxes back.
- Try registering again with the same username. You should see an alert that says 'Username taken'.
src/Components/Header/Header.js
register() {
const { username, password, isAdmin } = this.state;
axios
.post('/auth/register', { username, password, isAdmin })
.then(user => {
this.setState({ username: '', password: '' });
this.props.updateUser(user.data);
})
.catch(err => {
this.setState({ username: '', password: '' });
alert(err.response.request.response);
});
}
Now that we are successfully able to register, lets create the login endpoint so an existing user can now log in.
This endpoint will take a username and password off of the body and check if there is a user already exists by that username in the database. If the user is not in the database, our endpoint should respond with a 401 status code (unauthorized) since we were not able to authorize the user based on the fact that the username isn't registered. The endpoint should then compare the plain text password sent by the user to the salted and hashed version in our database. At that point, the endpoint should respond with a 200 (OK) status code if the password matches, or a 403 (Forbidden) status code if the password doesn't match.
-
In server/index.js we will create the login endpoint.
-
Create a POST endpoint with '/auth/login' as the URL and authCtrl.login as the controller function.
-
Create a property called login on the
authController
exports object, with the value of an async function that takes a req and res parameter.- Destructure username and password from
req.body
, storing them on const variables. - Get the database instance using
req.app.get('db')
- Using the
get_user
SQL file, query the database for a user with a username matching the username from req.body. Make sure to use theawait
keyword to ensure the promise resolves before referencing the data. - Store the result of the SQL query on a const variable called foundUser.
- Remember that SQL queries come back in an array, so take the first item of the foundUser array and set it to another const variable called
user
.
- Remember that SQL queries come back in an array, so take the first item of the foundUser array and set it to another const variable called
- If there is no user found, send a response with status 401, and the string 'User not found. Please register as a new user before logging in.'
- Otherwise, create a const variable called
isAuthenticated
and set it equal tobcrypt.compareSync(password, user.hash)
. This method compares the password entered by the user at login to the hashed and salted version stored in the database. - If isAuthenticated is false, send a response with status code 403, and the string 'Incorrect password'.
- Otherwise, set
req.session.user
to be an object with the same properties as the user object from the register endpoint, but using the data retrieved from theget_user
query. - Then send
req.session.user
as a response with status code 200.
- Destructure username and password from
-
Now test your endpoint with postman. Paste the following into the body section of the request as raw JSON.
Postman JSON Body - Login
{
"username": "mrsmee",
"password": "1stM84Lyfe"
}
You should receive the following response.
Postman Response - Login
{
"isAdmin": false,
"username": "mrsmee",
"id": 4
}
Also, try modifying the password so that it is incorrect. You should receive Incorrect password
as a response.
server/controllers/authController.js
const bcrypt = require('bcryptjs');
module.exports = {
register: async (req, res) => {
const { username, password, isAdmin } = req.body;
const db = req.app.get('db');
const result = await db.get_user([username]);
const existingUser = result[0];
if (existingUser) {
return res.status(409).send('Username taken');
}
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync(password, salt);
const registeredUser = await db.register_user([isAdmin, username, hash]);
const user = registeredUser[0];
req.session.user = { isAdmin: user.is_admin, username: user.username, id: user.id };
return res.status(201).send(req.session.user);
},
login: async (req, res) => {
const { username, password } = req.body;
const foundUser = await req.app.get('db').get_user([username]);
const user = foundUser[0];
if (!user) {
return res.status(401).send('User not found. Please register as a new user before logging in.');
}
const isAuthenticated = bcrypt.compareSync(password, user.hash);
if (!isAuthenticated) {
return res.status(403).send('Incorrect password');
}
req.session.user = { isAdmin: user.is_admin, id: user.id, username: user.username };
return res.send(req.session.user);
},
};
server/index.js
app.post('/auth/register', authCtrl.register);
app.post('/auth/login', authCtrl.login);
Now that we are successfully able to login, lets code out our front end to take advantage of the endpoint.
- Open
src/Components/Header/Header.js
- Our
login()
method is connected to the login button's onClick event handler, we just need to complete the logic that will make the POST request to our server's login endpoint. - Begin by destructuring the
username
andpassword
values from state. - Use axios to make a POST request to
/auth/login
with a body object with theusername
andpassword
values from state. - Chain a
.then
onto the end of the.post
method and provide a function that takes in a parameter called user. - The
Header
component has access to anupdateUser
method passed as a prop from theApp
component that will update theuser
property on state inApp
. Execute theupdateUser
method from props withuser.data
as an argument. - Also in the
.then
, make sure to clear the input boxes by setting theusername
andpassword
properties to empty strings usingsetState
. - Chain a
.catch
onto the.then
with an arrow function that references the error as a parameter. - Alert the error using the
alert()
function, passing inerror.response.request.response
. That chain of data leads to the string response from our server endpoint if there is an error. - You should now be able to test the login functionality. In your browser, enter a username and password that you have already registered, or register a new user with a memorable username and password.
- Click the
Log In
button. You should now see the welcome message. - Since the logout button doesn't work yet, refresh your browser to get the input boxes back. Try logging in with a username that hasn't been used yet. You should get an alert that says 'User not found. Please register as a new user before logging in.'
- Now try logging in with a registered user, but use an incorrect password. You should see 'Incorrect password' alerted.
src/Components/Header/Header.js
login() {
const { username, password } = this.state;
axios
.post('/auth/login', { username, password })
.then(user => {
this.props.updateUser(user.data);
this.setState({ username: '', password: '' });
})
.catch(err => alert(err.response.request.response));
}
Now that our users can register and login, lets build the endpoint and controller method that will handle destroying the user session, effectively logging the user out.
-
Go to
server/index.js
and create a GET endpoint with url '/auth/logout' and method authCtrl.logout -
Go back to authController.js and create a logout property with the value of an async function with parameters req and res
-
This function should run
req.session.destroy()
. As the name implies, this destroys the data stored on the user's session object, effectively logging the user out. -
Then send a response with a status of 200.
-
Test your endpoint with Postman. Send a GET request to
http://localhost:4000/auth/logout
. You should receiveOK
as a response.
server/index.js
app.post('/auth/register', authCtrl.register);
app.post('/auth/login', authCtrl.login);
app.get('/auth/logout', authCtrl.logout);
server/controllers/authController.js
logout: (req, res) => {
req.session.destroy();
return res.sendStatus(200);
}
We will now add log out functionality to our front end.
- Open
src/Components/Header/Header.js
- In the
logout()
method, use axios to make a GET request to/auth/logout
. - Chain a
.then
onto the end of the.get
method. We don't need use of the response, since it is just the string 'OK' because we used the 'sendStatus' method on the backend, so just pass an arrow function without a parameter into the.then
. - Once the response comes back from our GET request, we know the user is logged out. We now just need to modify the
user
object stored on state inApp.js
by calling theupdateUser
method passed through props from theApp
component with an empty object so that it clears all user data off of state. - Chain a
.catch
onto the.then
. Since we don't need to alert the user of any errors, justconsole.log
the error for debugging purposes. - Test the logout functionality by logging in, and then clicking the logout button. The header should switch from the welcome message back tot he username and password inputs.
src/components/Header.js
logout() {
axios
.get('/auth/logout')
.then(() => {
this.props.updateUser({});
})
.catch(err => console.log(err));
}
At this point, our user can register, log in, and log out which is all well and good, but our application still hasn't done anything useful yet. In this step, we will set up a get endpoint to retrieve the dragon's treasure. This endpoint is meant to be accessible to any user of the application regardless of whether they are logged in or not.
-
Create a file called
treasureController.js
in the controllers folder. -
Set
module.exports
to an object that will store our methods. -
Create an async method called
dragonTreasure
with parameters req and res- This should get the database instance and run the
get_dragon_treasure
SQL file, passing in the number '1'. - Use the await keyword on the database query and store the result on a variable.
- Return the result of this database query as the response with status 200.
- This should get the database instance and run the
-
Now go to
server/index.js
and requiretreasureController.js
storing it on a const variable calledtreasureCtrl
. -
Create a get endpoint, '/api/treasure/dragon', with the function
treasureCtrl.dragonTreasure
. -
Now test your endpoint in postman by making a GET request to
http://localhost:4000/api/treasure/dragon
. -
If everything is working correctly, you should get the following for the response:
Postman Response - Get Dragon Treasure
[
{
"id": 1,
"image_url": "http://www.theholidayspot.com/easter/treasure_hunt/images/treasure-chest.png",
"user_id": 1
}
]
server/controllers/treasureController.js
module.exports = {
dragonTreasure: async (req, res) => {
const treasure = await req.app.get('db').get_dragon_treasure(1);
return res.status(200).send(treasure);
},
};
server/index.js
const authCtrl = require('./controllers/authController');
const treasureCtrl = require('./controllers/treasureController')
...
app.post('/auth/register', authCtrl.register);
app.post('/auth/login', authCtrl.login);
app.get('/auth/logout', authCtrl.logout);
app.get('/api/treasure/dragon', treasureCtrl.dragonTreasure)
We will now set up our React application to request the dragon's treasure data from the back end.
- Open
src/Components/Container/Container.js
- Import
axios
at the top. - In the
getDragonTreasure()
method, make an axios GET request to/api/treasure/dragon
. - Chain a
.then
and set the data from the response to thetreasures
object on state on a property calleddragon
.- Make sure to use the spread operator to keep the treasures object immutable.
- Chain a
.catch
onto the.then
with aconsole.log
of the error - You should now be able to open your browser and click on the 'See Dragon's Treasure' button and see an image of the treasure display.
src/components/Container.js
getDragonTreasure() {
axios
.get('/api/treasure/dragon')
.then(treasure => {
this.setState({
treasures: {
...this.state.treasures,
dragon: treasure.data,
},
});
})
.catch(error => console.log(error));
}
At this point, a user should be able to register, log in, log out, and view the dragon's treasure without logging in. In this step, we will now build an endpoint for a user to view their treasure, as well as an endpoint to add new treasure to their collection. We will also write middleware that will protect these endpoint by responding with an error if a user is not logged in.
- Go to treasureController.js and create an async method called getUserTreasure with parameters req and res.
- This should get the database instance and run the
get_user_treasure
SQL file, passing in theid
fromreq.session.user
. - Use the await keyword on the database query, and store the result on a variable.
- Send the result of this database query as the response with status 200.
- Now go to
server/index.js
and create a get endpoint, '/api/treasure/user', with the functiontreasureCtrl.getUserTreasure
. - Before we can test this endpoint, you will need to log in with postman to set up our session. send a POST request to
http://localhost:4000/auth/login
with the following raw JSON request body:
Postman JSON Body - Login
{
"username": "mrsmee",
"password": "1stM84Lyfe"
}
- Now test your GET endpoint using postman by making a GET request to
http://localhost:4000/api/treasure/user
.- You should receive
[]
as a response since the user currently has no treasure in the database.
- You should receive
server/controllers/treasureController.js
module.exports = {
dragonTreasure: async (req, res) => {
const treasure = await req.app.get('db').get_dragon_treasure(1);
return res.status(200).send(treasure);
},
getUserTreasure: async (req, res) => {
const userTreasure = await req.app.get('db').get_user_treasure([req.session.user.id]);
return res.status(200).send(userTreasure);
},
};
server/index.js
const authCtrl = require('./controllers/authController');
const treasureCtrl = require('./controllers/treasureController');
...
app.post('/auth/register', authCtrl.register);
app.post('/auth/login', authCtrl.login);
app.get('/auth/logout', authCtrl.logout);
app.get('/api/treasure/dragon', treasureCtrl.dragonTreasure);
app.get('/api/treasure/user', treasureCtrl.getUserTreasure);
Remember how you had to first make a POST request to your /auth/login
endpoint before testing the GET /api/treasure/user
endpoint? Had you attempted to make the GET request to /api/treasure/user
before hitting the login endpoint first, there would have been an error in the console because req.session.user.id
is not defined until the user logs in. In this step, we are going to build top-level middleware that will ensure that each endpoint that requires the user to be logged in instead of getting an error in the console, our server will respond to the request with an error.
-
Create a
middleware
folder in the server folder. -
Create a
authMiddleware.js
file. -
Here set module.exports to an object.
- Create a method called
usersOnly
with the parameters req, res, and next. - The
usersOnly
function should check if there is a user object onreq.session
. - If there is not, send a response with status
401
and the string 'Please log in'. - Otherwise invoke next.
- Create a method called
-
In
server/index.js
requireauthMiddleware.js
and store it on a const variable calledauth
. -
On the '/api/treasure/user' endpoint, apply the usersOnly middleware that we have just created by referencing the middleware function between the endpoint path and the controller function.
-
Now test your middleware using postman. Since your server restarted as you have been developing, the server's session data has been cleared. Send a GET request to
http://localhost:4000/api/treasure/user
WITHOUT hitting the/auth/login
endpoint first.- You should see 'Please log in' as a response.
-
Now 'log in' with postman by sending a POST request to
http://localhost:4000/auth/login
with the following raw JSON request body:
Postman JSON Body - Login
{
"username": "mrsmee",
"password": "1stM84Lyfe"
}
- You should then test your GET endpoint again using postman by making a GET request to
http://localhost:4000/api/treasure/user
.- You should receive
[]
as a response instead of 'Please log in'.
- You should receive
server/middleware/authMiddleware.js
module.exports = {
usersOnly: (req, res, next) => {
if (!req.session.user) {
return res.status(401).send('Please log in');
}
next();
},
};
server/index.js
const authCtrl = require('./controllers/authController');
const treasureCtrl = require('./controllers/treasureController');
const auth = require('./middleware/authMiddleware');
...
app.post('/auth/register', authCtrl.register);
app.post('/auth/login', authCtrl.login);
app.get('/auth/logout', authCtrl.logout);
app.get('/api/treasure/dragon', treasureCtrl.dragonTreasure);
app.get('/api/treasure/user', auth.usersOnly, treasureCtrl.getUserTreasure);
Now that our getUserTreasure
endpoint is working, we will code the front end to request the user's treasure data from the server.
- Open
src/Components/Container/Container.js
- In the
getMyTreasure()
method, use axios to make a GET request to/api/treasure/user
.- Chain a
.then
on to the end of the.get
method and provide an arrow function withtreasure
as a parameter. - Call
this.setState
and pass in an object with atreasures
property with the value of an object. - Inside that object, make sure to spread the current properties and values of
this.state.treasures
before setting theuser
property totreasure.data
.
- Chain a
- Chain a
.catch
on to the.then
that takes an arrow function witherror
as a parameter and using analert()
, alertserror.response.request.response
. This will alert the message that we respond with on the server. - You should now test your application by doing the following.
- Open your browser.
- Log in
- Click the 'See My Treasure' button.
- You should now see the panel change to display ''s treasure and an input box with an 'add' button next to it.
src/Components/Container/Container.js
getMyTreasure() {
axios
.get('/api/treasure/user')
.then(treasure => {
this.setState({
treasures: {
...this.state.treasures,
user: treasure.data,
},
});
})
.catch(error => alert(error.response.request.response));
}
Now we will code the add treasure functionality on the server side. We will need to create a controller function and a POST endpoint for '/api/treasure/user'. We will need to apply the correct authentication middleware to this endpoint, so that it only works for logged-in users.
-
Open the treasureController.js file and create another method called addUserTreasure. This is going to be asynchronous, so be sure to use
async
andawait
. -
Destructure
treasureURL
fromreq.body
andid
fromreq.session.user
. -
Get the database connection and invoke the
add_user_treasure
SQL file passing in treasureURL and id as arguments. -
Set the result of your query to a variable named
userTreasure
. -
Send the results of this SQL query as the response with a
200
status code. -
Create a POST endpoint for this function in index.js.
- The endpoint url should be '/api/treasure/user' and
treasureCtrl.addUserTreasure
should be the controller function that runs when this endpoint is hit.
- The endpoint url should be '/api/treasure/user' and
-
Apply the
usersOnly
auth middleware function between the URL path and the controller function (treasureCtrl.addUserTreasure
). -
You should now be able to test your endpoint using Postman. Note: this shouldn't work, initially because we don't have an active session, but we need to make sure it won't work if the user isn't logged in.
-
send the following body as a POST request to
http://localhost:4000/api/treasure/user
.JSON Treasure POST body
{ "treasureURL": "https://bible2blog.files.wordpress.com/2015/06/treasurechest.png" }
-
You should receive the following as a response:
-
Please log in
- Now 'log in' with postman by sending a POST request to
http://localhost:4000/auth/login
with the following raw JSON request body:
Postman JSON Body - Login
{
"username": "mrsmee",
"password": "1stM84Lyfe"
}
- You should receive the following as a response. The id on your response may be different.
{
"isAdmin": false,
"id": 4,
"username": "mrsmee"
}
- You should now be able to test this endpoint again by sending a POST request to
http://localhost:4000/api/treasure/user
.
JSON Treasure POST body
{
"treasureURL": "https://bible2blog.files.wordpress.com/2015/06/treasurechest.png"
}
- You should receive the following as a response:
[
{
"id": 4,
"image_url": "https://bible2blog.files.wordpress.com/2015/06/treasurechest.png",
"user_id": 4
}
]
treasureController.js
module.exports = {
dragonTreasure: async (req, res) => {
const treasure = await req.app.get('db').get_dragon_treasure(1);
return res.status(200).send(treasure);
},
getUserTreasure: async (req, res) => {
const userTreasure = await req.app.get('db').get_user_treasure([req.session.user.id]);
return res.status(200).send(userTreasure);
},
addUserTreasure: async (req, res) => {
const { treasureURL } = req.body;
const { id } = req.session.user;
const userTreasure = await req.app.get('db').add_user_treasure([treasureURL, id]);
return res.status(200).send(userTreasure);
},
};
server/index.js
app.post('/auth/register', authCtrl.register);
app.post('/auth/login', authCtrl.login);
app.get('/auth/logout', authCtrl.logout);
app.get('/api/treasure/dragon', treasureCtrl.dragonTreasure);
app.get('/api/treasure/user', auth.usersOnly, treasureCtrl.getUserTreasure);
app.post('/api/treasure/user', auth.usersOnly, treasureCtrl.addUserTreasure);
Here we will make our application capable of adding new treasure from the front end.
- Open
src/Components/AddTreasure.js
. - In the AddTreasure component there is a method called addTreasure. This method should use
axios
to make a POST request to '/api/treasure/user', with an object containing the treasureURL value from state. Be sure to import axios at the top. - In the
.then
, callthis.props.addMyTreasure
, which was has already been passed as a prop from<Treasure />
and pass inres.data
. Then callthis.setState
with treasureURL set to an empty string to clear the input field. - Chain a
.catch
on to the.then
and pass an arrow function referencing theerror
as a parameter that uses analert()
to alerterror.response.request.response
. - You should now be able to test your application.
- Log in with your username and password.
- You can use username:
mrsmee
and password:1stM84Lyfe
if you would like, or register another user.
- You can use username:
- Click the 'See My Treasure' button.
- Type a known image URL into the input box. Feel free to use
https://bible2blog.files.wordpress.com/2015/06/treasurechest.png
if you would like. - You should see the image added below the input box after a short pause and the input box should clear.
- Log in with your username and password.
src/Components/AddTreasure.js
import React, { Component } from 'react';
import axios from 'axios';
export default class AddTreasure extends Component {
constructor() {
super();
this.state = {
treasureURL: '',
};
}
handleInput(e) {
this.setState({ treasureURL: e.target.value });
}
addTreasure() {
const { treasureURL } = this.state;
axios
.post('/api/treasure/user', { treasureURL: treasureURL })
.then(res => {
this.props.addMyTreasure(res.data);
this.setState({ treasureURL: '' });
})
.catch(error => {
console.log(error);
alert(error.response.request.response)
});
}
render() {
return (
<div className="addTreasure">
<input
type="text"
placeholder="Add image URL"
onChange={e => this.handleInput(e)}
value={this.state.treasureURL}
/>
<button onClick={() => this.addTreasure()}>Add</button>
</div>
);
}
}
Now that we are able to view our treasure and add treasure to our horde, we will build the endpoint that will make it possible for an admin to view every user's treasure.
-
Go to treasureController.js and create an async method called getAllTreasure with parameters req and res.
- Get the database instance and run the
get_all_treasure
SQL file. - Use the await keyword on the database query and store the result on a variable.
- Send a response with the result of this database query with status
200
.
- Get the database instance and run the
-
Now go to
server/index.js
, create a get endpoint, '/api/treasure/all', with the functiontreasureCtrl.getAllTreasure
. -
Since we only want users that are logged in to interact with this endpoint, add the
usersOnly
middleware function between the route path and the controller. In the next step, we will add another middleware function to ensure that will check whether the user is an admin. -
You should now be able to test your endpoint using Postman. Note: this shouldn't work, initially because we don't have an active session, but we need to make sure it won't work if the user isn't logged in.
- Send a GET request to
http://localhost:4000/api/treasure/all
. - You should receive the following as a response:
Please log in
- Now 'log in' with postman by sending a POST request to
http://localhost:4000/auth/login
with the following raw JSON request body:
- Send a GET request to
Postman JSON Body
{
"username": "mrsmee",
"password": "1stM84Lyfe"
}
- Now use postman to make a GET request to
http://localhost:4000/api/treasure/all
NOTE: Currently, this endpoint will provide unrestricted access to all of the treasure information to any user type. In the next section we will build middleware that will protect the data from non-admin users.
server/controllers/treasureController.js
module.exports = {
dragonTreasure: async (req, res) => {
const treasure = await req.app.get('db').get_dragon_treasure(1);
return res.status(200).send(treasure);
},
getUserTreasure: async (req, res) => {
const userTreasure = await req.app.get('db').get_user_treasure([req.session.user.id]);
return res.status(200).send(userTreasure);
},
addUserTreasure: async (req, res) => {
const { treasureURL } = req.body;
const { id } = req.session.user;
const userTreasure = await req.app.get('db').add_user_treasure([treasureURL, id]);
return res.status(200).send(userTreasure);
},
getAllTreasure: async (req, res) => {
const allTreasure = await req.app.get('db').get_all_treasure();
return res.status(200).send(allTreasure)
}
};
server/index.js
const authCtrl = require('./controllers/authController');
const treasureCtrl = require('./controllers/treasureController');
const auth = require('./middleware/authMiddleware');
...
app.post('/auth/register', authCtrl.register);
app.post('/auth/login', authCtrl.login);
app.get('/auth/logout', authCtrl.logout);
app.get('/api/treasure/dragon', treasureCtrl.dragonTreasure);
app.get('/api/treasure/user', auth.usersOnly,treasureCtrl.getUserTreasure);
app.post('/api/treasure/user', auth.usersOnly, treasureCtrl.addUserTreasure);
app.get('/api/treasure/all', auth.usersOnly, treasureCtrl.getAllTreasure);
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));
Now that we have an endpoint that will retrieve treasure data for all users, we need to build a middleware function that will prevent this data from being accessed by non-admin users.
-
Open
server/middleware/authMiddleware.js
-
Create a new method on the
module.exports
object calledadminsOnly
with the propertiesreq
,res
, andnext
. -
Use an
if
statement to check if theisAdmin
property onreq.session.user
isfalse
. -
Inside the
if
block, respond with a status of403
and a string of 'You are not an admin'. -
Outside of the
if
statement, callnext()
-
Now that the middleware has been defined, go to
server/index.js
and add the find theGET
request to/api/treasure/all
. -
Add the
auth.adminsOnly
middleware function after theauth.usersOnly
middleware function, but before thetreasureCtrl.getAllTreasure
controller function. * This middleware function should now be able to ensure that a user is an admin before the request gets passed on to the final controller function.-
We should test our admin middleware by attempting to hit the endpoint as a user that isn't an admin. To do this, we will need to register a non-admin user.
- Open postman and enter
http://localhost:4000/auth/register
in the URL input and send the following as raw JSON on the body of your request:
Postman JSON Body - login
{ "username": "mrsmee", "password": "1stM84Lyfe" }
You should receive the following as a response if your request was successful.
Postman JSON Response - login
{ "isAdmin": false, "username": "mrsmee", "id": 4 }
- Open postman and enter
-
Now that our non-admin user is loggedin (which also puts their data on the session), send another GET request to
http://localhost:4000/api/treasure/all
.- You should receive
You are not an admin
as a response.
- You should receive
-
Now that we know our admin middleware is denying non-admins, log in as an admin by sending a POST request to
http://localhost:4000/auth/login
with the following raw JSON body:
Postman JSON Body - Register
{ "username": "captainjack", "password": "P1r@t3sLyfe", "isAdmin": true }
* You should receive the following response ```js { "isAdmin": true, "id": 4, "username": "captainjack" } ```
- FINALLY, send a GET request to 'http://localhost:4000/api/treasure/all'.
- You should receive an array with several treasure objects with various
user_id
properties, since we are seeing all treasures from all users.
- You should receive an array with several treasure objects with various
-
server/index.js
const authCtrl = require('./controllers/authController');
const treasureCtrl = require('./controllers/treasureController');
const auth = require('./middleware/authMiddleware');
...
app.post('/auth/register', authCtrl.register);
app.post('/auth/login', authCtrl.login);
app.get('/auth/logout', authCtrl.logout);
app.get('/api/treasure/dragon', treasureCtrl.dragonTreasure);
app.get('/api/treasure/user', auth.usersOnly, treasureCtrl.getUserTreasure);
app.post('/api/treasure/user', auth.usersOnly, treasureCtrl.addUserTreasure);
app.get('/api/treasure/all', auth.usersOnly, auth.adminsOnly, treasureCtrl.getAllTreasure)
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));
server/middleware/authMiddleware.js
module.exports = {
usersOnly: (req, res, next) => {
if (!req.session.user) {
return res.status(401).send('Please log in');
}
next();
},
adminsOnly: (req, res, next) => {
if (!req.session.user.isAdmin) {
return res.status(403).send('You are not an admin');
}
next();
}
};
Now that our getAllTreasure
endpoint is working including responding with appropriate errors if the user isn't logged in or an admin, it is now time to add the code to make the request from our front end.
- Open
src/Components/Container/Container.js
. - Find the
getAllTreasure
method and useaxios
to make aGET
request to/api/treasure/all
. - Chain a
.then()
on the end of.get()
method and callthis.setState()
passing in an object with a treasures property.-
Since all treasures including
dragon
,user
, andall
will be on the same object, we need to use the spread operator to add all current properties to our new object and then define a newall
property that will be added to the end of the object. -
Your
setState
should look like this:this.setState({ treasures: { ...this.state.treasures, all: treasure.data, }, });
-
- Chain a
.catch()
on to the end of the.then()
and use an pass in an arrow function witherror
as a parameter. You should thenalert
theerror.response.request.response
.- referencing
.response.request.response
on theerror
object allows us to drill down to the string response that we sent on the server.
- referencing
- Now we will test our application by attempting to click the 'See All Treasure' button under three different conditions: with no user logged in, with a non-admin logged in, and with an admin logged in.
- With no user logged in, you should see 'Please log in' alerted to the screen.
- With a non-admin user logged in, you should see
You are not an admin
alerted to the screen. Feel free to use username:mrsmee
and password:1stM84Lyfe
. After you verify the alert appears, log out. - With an admin user logged in, you should see multiple images appear in the all treasure section. Feel free to use username:
captainjack
password:P1r@t3sLyfe
src/Components/Container/Container.js
getAllTreasure() {
axios.get('/api/treasure/all')
.then( treasure => {
this.setState({
treasures: {
...this.state.treasures,
all: treasure.data
}
})
}).catch( error => alert(error.response.request.response))
}
Bcrypt
If you see a problem or a typo, please fork, make the necessary changes, and create a pull request so we can review your changes and merge them into the master repo and branch.
© DevMountain LLC, 2018. Unauthorized use and/or duplication of this material without express and written permission from DevMountain, LLC is strictly prohibited. Excerpts and links may be used, provided that full and clear credit is given to DevMountain with appropriate and specific direction to the original content.