Introducing Table Pro, the successor to Table! Customers will now need to make an account and log in before they are able to make reservations for a table at a restaurant of their choice.
The solution branch contains documented solution code. The commit history of that branch follows the instructions below.
textual representation of schema in DBML
The schema has already been defined for you. An initial migration has also been created, along with the seed script. In this section, you'll be adding some code to ensure that a customer's plaintext password is never stored in the database.
-
npm install bcrypt, which we will be using to hash customer passwords. -
In
prisma/index.js, we will extend the Prisma Client to add some methods to our customer model.-
Add a method named
registerto thecustomermodel, which takesemailandpasswordas parameters. It will hash the given password usingbcryptwith 10 salt rounds. Then, it will create a new customer with the provided email and the hashed password. This newly created customer is returned.See Solution
/** * Creates a new customer with the provided credentials. * The password is hashed with bcrypt before the customer is saved. */ async register(email, password) { const hashedPassword = await bcrypt.hash(password, 10); const customer = await prisma.customer.create({ data: { email, password: hashedPassword }, }); return customer; }
-
Add a method named
loginto thecustomermodel, which takesemailandpasswordas parameters. It will find the customer with the provided email. Then, it will compare the given password to the hashed password saved in the database. If the password does not match, it will throw an error. Otherwise, it returns the found customer.See Solution
/** * Finds the customer with the provided email, * as long as the provided password matches what's saved in the database. */ async login(email, password) { const customer = await prisma.customer.findUniqueOrThrow({ where: { email }, }); const valid = await bcrypt.compare(password, customer.password); if (!valid) throw Error("Invalid password"); return customer; }
-
-
Rename
example.envto.envand update theDATABASE_URLwith your Postgres credentials. -
Apply the migration and seed your local database with
npx prisma migrate reset. This will also generate a new Prisma Client with your newly defined customer methods.
We can now use these custom methods to handle our API's register and login routes!
-
npm install dotenv -
In your
.envfile, change theJWT_SECRETto something secure. Anyone who knows this string will be able to decrypt any token this backend generates. A good minimum length is 32 characters.- Example:
mn8i1PhN97IJVcpo1nESf38FFZCiqHiT(don't actually use this!)
- Example:
-
Add this line to the top of
server.js. This will allow the rest of your app to access the variables defined in your.envfile.require("dotenv").config();
-
npm install jsonwebtoken -
Near the top of
api/auth.js, importjsonwebtokenand grab theJWT_SECRETfromprocess.env.const jwt = require("jsonwebtoken"); const JWT_SECRET = process.env.JWT_SECRET;
-
Write a function
createTokenthat takes anidas a parameter. We will be calling this function later. Usejwt.signto create a token with{ id }as the payload andJWT_SECRETas the key. The token should expire in 1 day. Return the token.- Note:
idis wrapped in an object to preventjwtfrom coercing it into a string
See Solution
function createToken(id) { return jwt.sign({ id }, JWT_SECRET, { expiresIn: "1d" }); }
- Note:
-
Continue to the token-checking middleware. It has been partially defined for you. Make sure to read how the token is grabbed from the request headers.
- Use
jwt.verifywithJWT_SECRETto get theidfrom the token. - Find the customer with that
id. - Set
req.customerto that customer. - Continue to the next middleware.
See Solution
try { const { id } = jwt.verify(token, JWT_SECRET); const customer = await prisma.customer.findUniqueOrThrow({ where: { id }, }); req.customer = customer; next(); } catch (e) { next(e); }
- Use
-
Create the
POST /registerroute.- Pass the
emailandpasswordfrom the request body intoprisma.customer.register, which is the custom method that you defined earlier. Save the returned customer in a variable namedcustomer. - Pass the
idof thatcustomerintocreateToken, which you also defined earlier. Save the returned token in a variable namedtoken. - Respond with
{ token }and a status of 201.
See Solution
router.post("/register", async (req, res, next) => { const { email, password } = req.body; try { const customer = await prisma.customer.register(email, password); const token = createToken(customer.id); res.status(201).json({ token }); } catch (e) { next(e); } });
- Pass the
-
Create the
POST /loginroute.- Pass the
emailandpasswordintoprisma.customer.login. Save the returned customer in a variable namedcustomer. - Pass the
idof thatcustomerintocreateToken. Save the returned token in a variable namedtoken. - Respond with
{ token }.
See Solution
router.post("/login", async (req, res, next) => { const { email, password } = req.body; try { const customer = await prisma.customer.login(email, password); const token = createToken(customer.id); res.json({ token }); } catch (e) { next(e); } });
- Pass the
-
Read the
authenticatefunction, which has already been written. Why does it check forreq.customer?See Solution
The first token-checking middleware earlier in the file will look in the request headers for a token. It will try to grab a customer id from that token. If a customer is found with that id, it is attached to
req.customer.So, if
req.customerexists, that means the customer is successfully logged in and we can proceed to the next middleware (whatever that might happen to be). Otherwise, we will skip directly to sending a 401 error.
To recap: we now have routes for registering a new customer and logging in as an existing customer. Both routes will send a token if successful. We also have middleware to associate a request with a specific customer according to the attached token.
In this section, we'll define some routes that will only work if the customer is logged in. This allows us to protect our routes and limit who is allowed to access our database.
/reservations router
-
Notice how
authenticateis used in theGET /route. What do you think it's doing?See Solution
Any requests to
GET /reservationswill first go through theauthenticatemiddleware. If the customer is not logged in, then the request will automatically send an error. The customer can only access the rest of this route if they are logged in. -
Write the rest of the
GET /route. Send all of the reservations made by the customer stored inreq.customer. Include therestaurantof each reservation.See Solution
try { const reservations = await prisma.reservation.findMany({ where: { customerId: req.customer.id }, include: { restaurant: true }, }); res.json(reservations); } catch (e) { next(e); }
-
Create the
POST /route. It should only be accessible to a customer that is logged in. It will create a new reservation under the logged in customer, according to thepartySizeandrestaurantIdspecified in the request body. It then sends the newly created reservation with status 201.See Solution
router.post("/", authenticate, async (req, res, next) => { const { partySize, restaurantId } = req.body; try { const reservation = await prisma.reservation.create({ data: { partySize: +partySize, restaurantId: +restaurantId, customerId: req.customer.id, }, }); res.status(201).json(reservation); } catch (e) { next(e); } });
/restaurants router
-
Read the
GET /:idroute. What changes if a customer is logged in?See Solution
The value of
includeReservationschanges. If a customer is not logged in, then it's simplyfalse, which means that the restaurant will not include any reservations in the response.If a customer is logged in, then the response will include any reservations that the logged-in customer has made for that specific restaurant.