Project Work
As project you should work in pairs and create a platform (a solution consisting of both a backend application handling the data on the platform and a frontend application containing the graphical user interface through which users will use the platform). The backend should expose a REST API which the frontend can use to work with the data on the platform. The platform should be implemented with the technologies taught in the course, i.e. Node.js/Express for the backend and Vue.js for the frontend.
You should not only implement the platform, you should also write a report describing the platform and its implementation, including what it can be used for and how it has been implemented. The file project-report-template.docx contains a template for the report. Your report will be a living document throughout the course, meaning that you will write on it from the start and continually improve it until the end of the course.
Part 1: Find a partner
Start by finding a classmate to work with. Then join a Project Group on Canvas to let the examiner know who you're working with. If you can't find a classmate to work with, email the course coordinator at Peter.Larsson-Green@ju.se and he will pair you with another student in the same situation.
Part 2: Platform idea
Before you start working on this part you are expected to read/view the following lectures:
Come up with the platform you want to implement. The platform may be about whatever you want, but the following requirements exist:
- Users should be able to create accounts
- Users should be able to login to accounts
- Users should be able to create at least one other type of resource (in addition to accounts) that belongs to an account
Example of what the additional resource type could be:
- Blogposts for a platform hosting blogs
- Diary entries for a platform hosting diaries
- Movies for a platform where users can register which movies they've watched
If you want to have a more fun platform to implement, you probably need to have accounts + 2-3 additional resource types, but it is OK to just have one additional resource type.
Try to be creative and come up with an idea that solves a real world problem. For example, standing in line and wait is boring, maybe you can come up with a platform that somehow makes it fun? Or maybe a platform that entirely eliminates the need to stand in line and wait? Or maybe you know someone (football coach, friend at a company, etc.) that have some problem you can help them with by creating a platform (such as keeping track of which players attended which practice sessions, or who's responsible to bring fika to work each friday, etc.).
Users should be able to apply CRUD operations on at least accounts + 1 additional resource type on the platform, but how they do that through the frontend is up to you to decide. For example, to delete an account you may have a delete button either on the View account "page" or you may have a dedicated Delete account "page" that only contains the delete button. Users should also be able to login to the account they have created using a username and a password.
Describe your platform as detailed as possible in your project report. You should at least be able to complete most of the chapters Introduction and Graphical User Interface now.
Part 3: Designing the REST API
Before you start working on this part you are expected to read/view the following lectures:
Design the REST API you will implement on the backend and through which the frontend will communicate with it. By looking at the graphical user interface in your report you should be able to figure out which type of requests the frontend needs to be able to send to the backend to be able to function properly. For example, if the frontend have a "page" showing a list of all accounts, then your REST API should send back all accounts when it receives a GET
request for /accounts
(or similar), and if the frontend have a "page" showing all information about an account with a specific id, then your REST API should send back all information about an account with a SPECIFIC_ID
when it receives a GET
request for /accounts/SPECIFIC_ID
(or similar).
Note
You do not need to worry about login functionality (authentication and authorization) yet, that comes in Part 6.
Describe the REST API in your project report. Be as detailed as possible. Other programmers should understand how to use your REST API just by reading the specification for it in your report. This means that you must mention details such as which methods, URI:s, status codes, headers etc. that are used in your REST API for each type of request it can handle.
Part 4: Implementing the REST API in Express
Before you start working on this part you are expected to read/view the following lectures:
Implement the REST API you've specified in your project report so far as an Express application that stores the resources in an SQLite database. Next you'll find some guiding steps to help you get started.
Getting started with Express
Start by creating a new folder to store the source code for your backend application:
- Open a terminal/shell/console, for example Windows PowerShell in Windows
- Navigate to the folder where you want to create the root folder of your source code. Use the following commands:
cd /projects
- Go into the sub-folder namedprojects
cd ../
- Go back to the parent folder (you will probably not need to use this one)
- Create the root folder for your project's source code files by running the following command:
mkdir my-backend
- Create a new folder namedmy-backend
(use a better name)
- Go into the root folder by running the following command:
cd my-backend
- Go into the sub-folder namedmy-backend
- Create the
package.json
file (which keep tracks of which npm packages the project is using) by running the following command:npm init --yes
- Install the
express
package by running the following command:npm install express
Then open the root folder of your project in your code editor:
- Use the following command to open the root folder in Visual Studio Code:
code .
Then create the file app.js
with the content shown below. That code is a web application sending back the text Hello, World
when you visit localhost:3000/.
const express = require('express')
const app = express()
app.get("/", function(request, response){
response.send("Hello, World")
})
app.listen(3000)
Test if everything works by running your application and then visit localhost:3000/ a web browser or using Postman to send a GET request for the URI http://localhost:3000/
. You can run your application either through the Debug section in Visual Studio Code or through the node
command in your terminal/shell/console:
- Run your web application using the following command:
node app.js
– Use Node.js to execute the code in the fileapp.js
If everything works, you should see the text Hello, World
in your web browser.
If you started the web application through a terminal/shell/console, you can stop it from running by holding CTRL and pressing C (possibly twice) in the terminal/shell/console that started it. You can then change the code in app.js
, and then run the node command to start your web application again.
Play around a little bit to learn the basics:
- Can you change the text that is sent back to the client?
- Can you add a function handling GET requests for another URI and that sends back another text to the client?
- What happens when a client sends a GET request for a URI that does not exist?
Getting started with SQLite
The resources on the platform should be stored in a database. In this course we will use the relational database SQLite as our database. SQLite is most often not the best choice for big platforms with a lot of users, but it is easy to use and get started with, and you can later replace it with a more powerful database when needed.
SQLite stores the entire database in a single file and does not require any installation in addition to an npm package exposing an API you can use to communicate with it.
Start by installing the sqlite3
package (feel free to use sqlite
instead if you want to use promises (and optionally async
/await
) to avoid callback hell, although that's not something you will learn in this course):
- Open a terminal/shell/console and navigate to your project folder
- Install the npm package
sqlite3
by running the following command:npm install sqlite3
Then use sqlite3
in your backend to create a new database to store your resources in. Here are some guiding steps helping you with the accounts resources:
- Add the line
const sqlite3 = require('sqlite3')
- Add the line
const db = new sqlite3.Database("my-database.db")
- Use
db.run("Your SQL query")
to send a query to the database creating a table that can be used to store the accounts on the platform - Start the backend again. When you do this, the web application will send the query above to the database, which in turn will create the table to store the accounts in
- Open your database file in DB Browser for SQLite and verify that the table has been created
Note
If you send a query like CREATE TABLE accounts (...)
to the database, you will get an error when you later start your backend again and this query is sent to the database again, because the table accounts
already exists in the database. Instead, you can send a query like CREATE TABLE IF NOT EXISTS accounts (...)
, which will attempt to create the accounts
table only if it does not already exist.
Note
In the end the passwords should not be stored in plain text in the database, only their hash values, but to give you a gentle start we will not care about hashing them now.
Use DB Browser for SQLite to manually insert some accounts (remember to click on the Write Changes
button in the GUI!). Then let's write the code letting clients fetch accounts (this might not be in line with what you've written in your report, so you might need to modify the code below a little bit).
The DB file might be locked!
When an app, such as DB Browser to SQLite and your Express app, opens a file, they might keep having the file open (i.e. they do not all read all the file content and then immediately close the file) for as long as they run. If so, no other app can write to the file. So, you might have problem if you try to open and work with the DB file in DB Browser for SQLite if your web app is running at the same time.
Fetching all accounts
To let clients fetch all accounts, they can send a GET request to /accounts
. When the backend receives this request, it needs to fetch all accounts from the database and send them back in JSON format. To make that happen, you can use the code below:
// ...
app.get("/accounts", function(request, response){
// TODO: You should probably not fetch the password...
const query = "SELECT * FROM accounts ORDER BY username"
db.all(query, function(error, accounts){
if(error){
// If something went wrong, send back status code 500.
response.status(500).end()
}else{
// Otherwise, send back all accounts in JSON format.
response.status(200).json(accounts)
}
})
})
// ...
Try sending a GET request to /accounts
in Postman to verify that it works. Remember that you need to restart your backend each time you change the JavaScript code in it.
Fetching a single account
To let clients fetch an account with a specific id, they can send a GET request to /accounts/THE_ID
, e.g. /accounts/5
. When the backend receives this request, it needs to fetch the account with this id from the database and send it back in JSON format. To make that happen, you can use the code below:
// ...
app.get("/accounts/:id", function(request, response){
const id = request.params.id
// TODO: You should probably not fetch the password...
const query = "SELECT * FROM accounts WHERE id = ?"
const values = [id]
db.get(query, values, function(error, account){
if(error){
// If something went wrong, send back status code 500.
response.status(500).end()
}else if(!account){
// If no account with that id existed.
response.status(404).end()
}else{
// Otherwise, send back the account in JSON format.
response.status(200).json(account)
}
})
})
// ...
Try sending a GET request to /accounts/1
and /accounts/6876868
in Postman to verify that it works. Remember that you need to restart your backend each time you change the JavaScript code in it.
Creating accounts
To let clients create new accounts, they can send a POST request to /accounts
, and in the body pass information about the account to be created in JSON format, e.g. {"username": "Alice", "password": "abc123"}
. They also need to use the Content-Type
header with the value application/json
. When the backend receives this request, it needs to read the information from the body of the request and then insert that information as a new account in the database, and then send back a response to the client.
To be able to read bodies written in JSON format, you need to add the middleware function express.json()
:
- Add the line
app.use(express.json())
Then you can let clients create new accounts using the following code:
// ...
app.post("/accounts", function(request, response){
const account = request.body
const query = "INSERT INTO accounts (username, password) VALUES (?, ?)"
const values = [account.username, account.password]
db.run(query, values, function(error){
if(error){
response.status(500).end()
}else{
const id = this.lastID
response.header("Location", "/accounts/"+id)
}
})
})
// ...
Try adding Update and Delete operations for accounts on your own:
- To update a resource, clients should send a PUT request, the URI should identify which resource that should be updated, and the body should contain the updated resource. Use
app.put("...", function(...){ ... })
to listen for PUT requests - To delete a resource, clients should send a DELETE request, and the URI should identify which resource that should be deleted. Use
app.delete("...", function(...){ ... })
to listen for DELETE requests
Then add the other operations for the other type of resources you have. Note that we yet don't bother about authentication and authorization, so all clients should for now be allowed to whatever they want.
Don't forget to describe in your project report how the backend has been implemented.
Part 5: Implementing the Frontend
Before you start working on this part you are expected to read/view the following lectures:
Implement the frontend application in Vue.js. You will not get any help with how to use Vue.js here (use what you learned from the laboratory work), but we will give you some instructions on how to communicate with the backend through its REST API.
Remember
The frontend does not yet contain login functionality, so you can't implement that part of the frontend yet.
Don't forget to describe in your project report how the frontend has been implemented.
Enabling CORS on the backend
To start with, web browsers will forbid the frontend to communicate with the backend due to the Same-Origin Policy (at least forbid many of the requests it needs to send). To allow it, you need to add support for CORS to your backend application. In Express it's as easy as this (using the *
value in the CORS headers to allow any clients to do anything):
const express = require('express')
//...
const app = express()
// Enable CORS.
app.use(function(request, response, next){
// Allow client-side JS from the following websites to send requests to us:
// (not optimal, for better security, change * to the URI of your frontend)
response.setHeader("Access-Control-Allow-Origin", "*")
// Allow client-side JS to send requests with the following methods:
response.setHeader("Access-Control-Allow-Methods", "*")
// Allow client-side JS to send requests with the following headers:
// (needed for the Authorization and Content-Type headers)
response.setHeader("Access-Control-Allow-Headers", "*")
// Allow client-side JS to read the following headers in the response:
// (in addition to Cache-Control, Content-Language, Content-Type
// Expires, Last-Modified, Pragma).
// (needed for the Location header)
response.setHeader("Access-Control-Expose-Headers", "*")
next()
})
// ...
Note!
FireFox did before support the *
value only in the Access-Control-Allow-Origin
header, so if you wanted to support Firefox you needed to list the values you want to allow in the other 3 headers, e.g. response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type")
instead of using *
, but support for the *
value was added in FireFox 69. If you want to support older versions of FireFox, you need to list the supported values instead of using *
.
Sending HTTP requests from the frontend
To send HTTP requests from your Vue.js application you can either use the old XMLHttpRequest object or the new fetch()
function. Below you find example of how to use each of them.
XMLHttpRequest
const request = new XMLHttpRequest()
// Specify method and URI.
request.open("POST", "http://localhost:3000/accounts")
// Add headers to the request.
request.setRequestHeader("Content-Type", "application/json")
// ...
// Add a callback function that will be called when
// we receive back the response.
request.addEventListener('load', function(){
const statusCode = request.status
const bodyAsString = request.responseText
const bodyAsJsObject = JSON.parse(bodyAsString)
const locationHeader = request.getResponseHeader("Location")
// ...
})
// Add a callback function that will be called if
// the communication with the server fails.
request.addEventListener("error", function(){
// Request failed :(
})
// Specify body and send it.
const accountToBeCreated = {
username: "Alice",
password: "abc123"
}
const bodyAsString = JSON.stringify(accountToBeCreated)
request.send(bodyAsString)
fetch()
XMLHttpRequest
is built on callback functions. The newer fetch()
function is instead built on promises. A major benefit with using promises instead of callback functions is that you can chain them, which will make the code much more readable (you avoid callback hell). You can learn about how to chain promises by reading the article Promises chaining.
To learn how the fetch()
function works, read the article Introduction to fetch().
Below is an example of how to use fetch()
without chaining promises.
const accountToBeCreated = {
username: "Alice",
password: "abc123"
}
const bodyAsString = JSON.stringify(accountToBeCreated)
fetch("http://localhost:3000/accounts", {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json'
}),
body: bodyAsString
}).then(function(response){
const statusCode = response.status
const locationHeader = response.headers.get("Location")
response.json().then(function(bodyAsJsObject){
// Handle the body in the response here ...
}).catch(function(error){
// Handle errors with the body of the response here...
})
}).catch(function(error){
// Handle errors with sending the request/receiving the response here...
})
Below is an example of how to use fetch()
with chaining promises.
const accountToBeCreated = {
username: "Alice",
password: "abc123"
}
const bodyAsString = JSON.stringify(accountToBeCreated)
fetch("http://localhost:3000/accounts", {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json'
}),
body: bodyAsString
}).then(function(response){
const statusCode = response.status
const locationHeader = response.headers.get("Location")
return response.json()
).then(function(bodyAsJsObject){
// Handle the body in the response here ...
}).catch(function(error){
// Handle all different type of errors here...
})
fetch()
with async and await
The biggest benefit with promises is that we can use the async
and await
keywords in JavaScript instead of chaining them. This way, we end up writing code that looks to run synchronously (and hence very easy to read ☺), but it will run asynchronously (hence not blocking ☺).
To learn how to use async
/await
you can read the article How to use Async Await in JavaScript.. Using promises with async
/await
is probably easier than learning how to chain promises, so don't be afraid of trying.
async function createAccount(accountToBeCreated){
const bodyAsString = JSON.stringify(accountToBeCreated)
const response = await fetch("http://localhost:3000/accounts", {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json'
}),
body: bodyAsString
})
const statusCode = response.status
const locationHeader = response.headers.get("Location")
const bodyAsJsObject = await response.json()
return bodyAsObject
}
try{
const bodyAsObject = await createAccount({
username: "Alice",
password: "abc123"
})
// ...
}catch(error){
// Called when something goes wrong :(
}
Part 6: Adding Login to the REST API
Before you start working on this part you are expected to read/view the following lectures:
Add authentication and authorization to the REST API in your project report as described by the OAuth 2.0 Framework and OpenID Connect. You basically need to:
- Add one request clients can use to "login" to an account (to obtain an Access Token and an ID Token)
- Clients also need to know which claims the ID Token contains
- Describe how clients can pass the Access Token to the backend in requests
- Describe which clients that are allowed to perform which operations in the API
You only need to support the Authorization Grant called Resource Owner Password Credentials. Follow the details in the specification as much as possible.
Report Feedback
The quality of your report will affect the grade on your project quite much. From one point of view, your grade should only be based on what is written in the report, and your source code will only be used to verify that you actually have implemented it as it is described in the report. So it's important that your report is of good quality.
During the lab sessions, we encourage you to discuss your report with the teacher. You can't expect the teacher to read all your text, but we strongly advice you to have higher level of discussions about the report (such as structure and discussions around figures).
Part 7: Implementing Login in Express
Implement authentication and authorization in the Express application the way you describe it in the previous part. You should also change the code to store hash values of the users passwords, instead of storing them as plain text. Here we give you some hints about how to accomplish this.
Adding login/Creating tokens
OAuth 2.0 implementation not required!
It is good to implement authorization per a specification, such as OAuth 2.0. But to simplify for the students that take this course this year, following the OAuth 2.0 specification is not a must. For example:
- OAuth 2.0 specifies that the login request should use the data format
application/x-www-form-urlencoded
, but usingapplication/json
, as you do for all other POST request, is OK - You don't need to worry about
grant_type
in the login request (let the client just send a username and a password) - You don't need to worry about sending back responses that conform to the OAuth 2.0 specification (use whichever status codes and response bodies you think make most sense, but you must send back an access token and an ID token on a successful login)
- When the client sends the Access Token to the server, the client can pass it directly in the Authorization header (skip
Bearer
)
The instructions in the rest of this sub-chapter is for implementing authorization per the OAuth 2.0 specification, and they are left as they are for the students who want implement it that way. But those who prefer to do it with the simplifications mentioned above, that is OK too.
According to the OAuth 2.0 specification, when a user logs in with a username and password, they should send it to the server in the body of the request in the data format application/x-www-form-urlencoded
. This is the same data format that is used in the querystring, e.g. variable1=value1&variable2=value2&...
. When the backend receives such a request, it needs to parse the body written in that data format. This can be added to Express using the middleware function express.urlencoded()
:
app.use(express.urlencoded({
extended: false
}))
When you have added this middleware, request.body
will be populated with information from the body, e.g.:
// Body: variable1=value1&variable2=value2
app.post('/tokens', function(request, response){
const variable1 = request.body.variable1 // value1
const variable2 = request.body.variable2 // value2
// ...
})
You need to check that the body contains a variable called grant_type
with the value password
. If that's not the case, then the user tries to login in one of the other ways described in OAuth 2.0 that we don't support, and we should send back an error response (see the specification for the details).
If grant_type
has the value password
, then the body should also contain the variables username
and password
. If that's not the case, then something is wrong with the request and we should send back an error response (see the specification for the details).
If grant_type
has the value password
and the body also contains the variables username
and password
, then we need to fetch the account from the database with the given username
and see if the password
matches. If no account with that username exists, or if the password is wrong, we should send back an error response (see the specification for the details).
Otherwise, if everything is OK and the user should be signed in, we need to create an Access Token the user can send to the backend in the future as a proof of being signed in to a specific account. We can implement these Access Tokens as JSON Web Tokens (JWT). To create a new JWT, we can use the npm package jsonwebtoken
. To install it, run the command npm install jsonwebtoken
in the root folder of your backend application. Then you can use it like this:
const jwt = require('jsonwebtoken')
const jwtSecret = "some_random_characters"
const dataToPutInTheToken = { // AKA "claims" and "payload".
country: "Sweden"
}
const accessToken = jwt.sign(dataToPutInTheToken, jwtSecret) // "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb3VudHJ5IjoiU3dlZGVuIn0.k6rz1VHMIg3YvFpm4JMy78RUnFBUCPQPRoRXa2HlRjs"
In the Access Token you probably want to put something that identifies the user, such as the user's account id, and then send it back to the client (see the specification for the details).
When you're done you can use Postman to test if you can login and get back an Access Token. If you do, you can then use the debugger at jwt.io to verify that the token contains expected data.
Receiving and extracting tokens
When a client in the future sends requests to the backend and need to prove that she's the owner of a specific account, she can pass the Access Token in the Authorization
header, e.g.:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb3VudHJ5IjoiU3dlZGVuIn0.k6rz1VHMIg3YvFpm4JMy78RUnFBUCPQPRoRXa2HlRjs
When the backend receives that request it needs to extract the Access Token from this header and then extract the data from the token that you put inside of it before. You can do that like this:
const jwt = require('jsonwebtoken')
const jwtSecret = "some_random_characters" // Same secret as before.
app.get("/some-protected-resource", function(request, response){
let dataInToken = null
try{
const authorizationHeader = request.get("Authorization") // E.g. "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb3VudHJ5IjoiU3dlZGVuIn0.k6rz1VHMIg3YvFpm4JMy78RUnFBUCPQPRoRXa2HlRjs"
const accessToken = authorizationHeader.substring("Bearer ".length) // E.g. "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb3VudHJ5IjoiU3dlZGVuIn0.k6rz1VHMIg3YvFpm4JMy78RUnFBUCPQPRoRXa2HlRjs"
dataInToken = jwt.verify(accessToken, jwtSecret) // E.g. {country: "Sweden"}
}catch(error){
// Access token not present or invalid.
}
if(dataInToken){
// We received a valid Access Token :D
}else{
// We didn't receive an Access Token or the received Access Token was invalid :(
}
})
After we have extracted the data from the token we can figure out to which account the user logged in to before, and then figure out if the user is authorized to make the request or not.
When you have added authorization checks to your code you can use Postman to send requests with the Authorization header containing the Access Token and see if it works as it should. You can also use the debugger at jwt.io to create invalid Access Tokens and see if your backend properly detects them as invalid.
Avoiding copy-pasting code
Extracting the Access Token from the Authorization
header like that and then extract the data from the Access Token is something you want to do in many of the requests your backend receives, so instead of copy-pasting all of this code it is better to put it in a function and then call the function when you need to extract it (or even better: use a middleware function).
Note!
One should rather use jwt.sign()
and jwt.verify()
asynchronously by providing a callback function, but to simplify we used them synchronously instead (they send back a return value). By using them synchronously they are blocking, so concurrent incoming HTTP requests are queued instead of handled immediately (the calls to jwt.sign()
and jwt.verify()
takes many milliseconds to execute). The call to these functions with a callback function are not blocking (the long running operations are executed in the background/another thread), and therefor better to use.
You can probably also find another JWT package that are built on promises instead of callback functions. That would be even better to use.
Adding OpenID Connect
When a client logs in and receives back an Access Token, the client does probably also want to know to which account the user logged into, so the client knows the username of the account, the id of the account, etc. For that we can use OpenID Connect. It specifies that when the client logs in, we do not only send back an Access Token, but also an ID Token that contains information about who the user is. Unlike Access Tokens, ID Tokens have to be implemented as JWT.
You get to decide what you want to put in your ID Token, but follow the specification as much as possible.
When you're done you can use Postman and see if you also get back an ID Token when you login. If you do you can use the debugger at jwt.io to verify that the token contains expected information.
Hashing passwords
Not required!
Storing passwords in plain text in the database is not good practice. If a hacker comes over the database, she can find all out users' passwords in plain text. To avoid that, one should hash the passwords, and only store the hash value of them. BUT, to facilitate for you who take this course this year, you don't have to do this part of the project. So what's written in this sub-chapter (Hashing passwords
) is optional to implement.
Storing passwords in plain text is a bad idea. Users often use the same password on different platforms, and if their passwords on our platform are leaked (by accident or by a hacker that has manage to hack our platform), anyone can login on their accounts on the other platforms they are using. Quite bad!
Instead, passwords should be hashed, and we should only store the hash value of their passwords. There exists many different hashing algorithms, but one of the best ones to use for hashing passwords is bcrypt. Many hashing algorithms are designed to be fast, so the hash value quickly can be computed, but that is not suitable for passwords, because then hackers can use brute-force to figure out what the original password was. bcrypt on the other hand has intentionally been designed to be slow to prevent this, and you can control how slow it should be (so you can make it even slower in the future when computers have become faster).
To use bcrypt in Node.js you can use the npm package bcryptjs:
- Download the npm package to your backend application:
- In the root folder of your backend application, run the command
npm install bcryptjs
- In the root folder of your backend application, run the command
- When creating a new account, hash the user's password using:
const bcrypt = require('bcryptjs')
const hashingRounds = 8 // How slow it should be (the higher number the slower).
const passwordToHash = "abc123"
const hashValue = bcrypt.hashSync(passwordToHash, hashingRounds) // "$2y$08$qc1V89V0GAstCI/NAMM4HO4DcP9Jwgk/h/WX2JsgvTIZqXRw6vxAK"
// Store hashValue with the user's account in the database instead of passwordToHash.
- When the user logs in, fetch the user's
hashValue
from the database and see if the provided password matches that one:
const bcrypt = require('bcryptjs')
const usersEnteredPassword = "abc123"
const storedHashValue = "$2y$08$qc1V89V0GAstCI/NAMM4HO4DcP9Jwgk/h/WX2JsgvTIZqXRw6vxAK" // Fetched from database.
if(bcrypt.compareSync(usersEnteredPassword, storedHashValue)){
// Correct password.
}else{
// Wrong password.
}
When you're done, use Postman to create some new accounts and then try to login to these.
Remember
The old accounts in your database contains the password in plain text, so you should not be able to login to them anymore. Feel free to delete these.
Note!
To simplify, many things you should think of for a real platform has been ignored in the instructions above, but here are short descriptions of these things for the curious ones:
One should rather use the npm package bcrypt
instead of bcryptjs
. The JavaScript you write to use them is the same, but bcryptjs
has been implemented in JavaScript, making it 30% slower than bcrypt
, which is implemented in C, so it is better to use bcrypt
. But bcrypt
has some dependencies making it a bit harder to use.
One should rather use the asynchronous functions hash()
and compare()
instead of the synchronous hashSync()
and compareSync()
. The synchronous functions are easier to use (return values instead of callback functions), but they are blocking, so concurrent incoming HTTP requests are queued instead of handled immediately. The asynchronous functions compute the hash values in the background/in another thread, so they don't have this shortage.
Both bcrypt
and bcryptjs
supports using promises, so rather use these instead of using callbacks.
You should most likely not use 8
as the number of hashing rounds (too low), but it is a bit complicated to find out the optimal number of rounds to use, and to do that you also need to know which server your backend will be running on in the end, and since deploying a backend on a server is not part of this course, we don't have all the details to compute it.
Part 8: Adding Login to the Frontend
Now that the backend have login functionality through Access Tokens and ID Tokens, use this login functionality in the frontend application. When you're done, users should only be able to do what they should be able to. For example, a user should not be able to delete another user's account, or similar.
You are recommended to keep track of whether the user is logged in or not the same way as you did in the lab, i.e. in add a user
object to App.vue
and pass this as a props to your other Vue components.
When the user successfully logs in you get back an ID Token with information about the user (it's id, username, etc.). You need to open up this JWT and read out the information from it. You can't do that with the npm package jsonwebtoken
(that you used in your backend), because it only works in Node.js, and not in web browsers. Instead, you can use the npm package jwt-decode
:
- In the root folder of your frontend application, run the command
npm install jwt-decode
. - To read out the data from the ID Token, use the function this npm package consists of:
const jwtDecode = require('jwt-decode')
const idToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjQsInByZWZlcnJlZF91c2VybmFtZSI6IkFsaWNlIn0.3Xp7iQkttgTE6hpuT28LFdZ7EYWHlPndqdaWoIzTr9A"
const dataInIdToken = jwtDecode(idToken) // E.g. {sub: 4, preferred_username: "Alice"}
Part 9: Presentation
Present your platform to the rest of the class in smaller groups. To distribute you, ONE in your Project Group should join the corresponding Project Presentation Group on Canvas.
The reason for the presentation is two-folded:
- You get some practice in presenting your work, which is a very important skill in your future professional career
- You get to see other platforms that you should be able to implement yourself
At the presentation you should simply tell the audience what problem you try to solve by letting users use your platform, and then show how users would solve their problem by using the platform (i.e. demonstrate how the platform can be used). You should not show any code nor explain any implementation details, but feel free to show slides if you want.
The best way to show what a user can do on the platform is by having one of you speaking and telling the other one what to do (e.g. Create a new account for me), and then the other shows how to do that.
To be allowed to present your work your you must at least be done with Part 5.
You may use at most 10 minutes for your presentation. If you need more than this to show all features, then skip some of them. If your presentation takes just 3 minutes, that could be OK as well. Don't start talking about irrelevant things just to make the presentation longer.
If you can't present your work on your own laptop, email the course coordinator and he will help you.
Your presentation will not be graded; consider it as (mandatory) practice.
Part 10: Grade 4 and 5
Be sure to read through Project Grading Guidelines to see that you follow the guidelines for grade you're aiming for. To be able get grade 4 and 5 you also need to implement the extra functionality described next.
SDK (required for grade 4)
Create an SDK other programmers can use to communicate with your backend from their client-side JavaScript code. Then also use it in your own frontend. Also update the report to reflect this.
Supporting Multiple Data Formats (required for grade 4)
Not required!
To facilitate for you who take this course this year, you don't have to do this part of the project to be able to get grade 4 and 5. So what's written in this sub-chapter (Supporting Multiple Data Formats (required for grade 4)
) is optional to implement.
Add support for another data format in addition to JSON (+ www-form-urlencoded for logging in). This should work in both requests and responses, so when you receive a request you need to look at the Content-Type
header to figure out which format the body in the request is written in, and you also need to look at the Accept
header to figure out in which format you should send back the body in the response in.
Example of data formats you can support:
- XML
- YAML
Try to find a suitable npm package doing most of the work for you.
Also update your report to reflect this.
Guidelines parsing the body of incoming HTTP request
Each time you receive an HTTP request with a body, check the Content-Type
of the incoming HTTP request. If the content type is application/yaml
, then the resource in the body of the request has been written in YAML format. Then you need to read the body of the quest into a string, and then parse it as YAML code, then you have your resource as a JavaScript object. How to read the body into a strings is described in the lecture Web Applications in Node.js. Find an npm package you can use to do the parsing of YAML code for you.
Tips!
You need to do this for all incoming HTTP requests with a body, so the best way to implement it is as a middleware function. In your middleware function, after you have obtained the resource from the body as a JavaScript object, assign it to request.body
so your ordinary request handler functions in app.post(...)
and app.put(...)
can obtain the resource through request.body
.
Or, maybe you can find an npm package that exports a middleware function doing all of this for you already?
Sending back responses in correct data format
You can't always call response.json(theResource)
to send back the resource. You should only use response.json(...)
if the Accept
header in the request is application/json
. If the Accept
header instead is application/yaml
, you should send it back in YAML format. To convert the resource to a string with YAML code, use a suitable npm package. To send back the string with the YAML code, you can use response.end("THE YAML CODE")
.
Tips!
You need to do this for all outgoing HTTP responses with a body, so one way to implement it is as a function doing something like this:
if(/* Accept header in request is application/json */){
// Send back the resource in JSON format.
}else if(/* Accept header in request is application/yaml */){
// Send back the resource in YAML format.
}else{
// Send back the resource in your chosen default format.
}
Third-Party Authentication (required for grade 5)
Add third-party authentication to your application so users can login on your platform with their Google account (or whichever third-party you choose to use) instead of using a username and password. You need to implement this in your backend application and then use it in your frontend application as well.
Also update your report to reflect this.
Part 11: Final submission
When you are done with your project, upload it for grading by submitting the Canvas assignment Project Final Submission. Upload:
- Your report as a PDF file
- The source code for the frontend app as a ZIP file:
- Delete the
node_modules
folder in the app's root folder - Create a ZIP file of the app's root folder
- Delete the
- The source code for the backend app as a ZIP file:
- Delete the
node_modules
folder in the app's root folder - Create a ZIP file of the app's root folder
- Delete the
The examiner will only look at your latest submission here, so be sure to upload all files in one and the same submission, and not as three separate submissions.
It is very important that your two apps can be started by:
- Unzipping the ZIP files
- In respective app's root folder running the following two commands:
npm install
node app.js
ornpm run dev
(or whatever)
If this does not work, the teacher will grade your work Revision required, and you need to submit your work again at the next examination occasion, so double check that this work yourself before you submit your work!